From: pathetic meowmeow Date: Tue, 20 Jan 2026 07:07:53 +0000 (-0500) Subject: Visual nubody (humanoid appearance refactor) (#42476) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=8cf744ec55fa19968ae5a7bc279d5b3862e3f878;p=space-station-14.git Visual nubody (humanoid appearance refactor) (#42476) * initial visual nubody * oops overlay * im so pheeming rn * conversion... * tests * comeback of the underwear * oops eyes * blabbl * zeds * yaml linted * search and visible count constraints * reordering * preserve previously selected markings colors * fix test * some ui niceties * ordering * make DB changes backwards-compatible/downgrade-friendly * fix things again * fix migration * vulpkanin markings limit increase * wrapping * code cleanup and more code cleanup and more code cleanup and more code cleanup and * fix slop ports * better sampling API * make filter work + use the method i made for its intended purpose * fix test fails real quick * magic mirror cleanup, remove TODO * don't 0-init the organ profile data * remove deltastates --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- diff --git a/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs b/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs index d96980fb1d..a9dcfaf2b0 100644 --- a/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs +++ b/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs @@ -25,8 +25,8 @@ public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem var index = _sprite.LayerMapReserve((ent.Owner, sprite), ent.Comp.LayerMap); - if (TryComp(ent, out var humanoidAppearance) && - ent.Comp.SpeciesSprites.TryGetValue(humanoidAppearance.Species, out var speciesSprite)) + if (TryComp(ent, out var humanoid) && + ent.Comp.SpeciesSprites.TryGetValue(humanoid.Species, out var speciesSprite)) { _sprite.LayerSetSprite((ent.Owner, sprite), index, speciesSprite); } diff --git a/Content.Client/Body/VisualBodySystem.cs b/Content.Client/Body/VisualBodySystem.cs new file mode 100644 index 0000000000..724dd22017 --- /dev/null +++ b/Content.Client/Body/VisualBodySystem.cs @@ -0,0 +1,261 @@ +using System.Linq; +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 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, EntityUid target) + { + 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; + + 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 layer = _sprite.AddLayer(target, sprite, index + i + 1); + _sprite.LayerMapSet(target, layerId, layer); + _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); + } + + applied.Add(marking); + } + ent.Comp.AppliedMarkings = applied; + } + + private void RemoveMarkings(Entity ent, EntityUid target) + { + 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 (!_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); + } + } + } + } +} diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs index 417e540d4a..1a6db7a1b6 100644 --- a/Content.Client/Clothing/ClientClothingSystem.cs +++ b/Content.Client/Clothing/ClientClothingSystem.cs @@ -271,7 +271,7 @@ public sealed class ClientClothingSystem : ClothingSystem // Select displacement maps var displacementData = inventory.Displacements.GetValueOrDefault(slot); //Default unsexed map - var equipeeSex = CompOrNull(equipee)?.Sex; + var equipeeSex = CompOrNull(equipee)?.Sex; if (equipeeSex != null) { switch (equipeeSex) diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs index 949b4770c4..92079542bd 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs @@ -79,9 +79,9 @@ public sealed partial class HealthAnalyzerControl : BoxContainer NameLabel.SetMessage(name); SpeciesLabel.Text = - _entityManager.TryGetComponent(target.Value, - out var humanoidAppearanceComponent) - ? Loc.GetString(_prototypes.Index(humanoidAppearanceComponent.Species).Name) + _entityManager.TryGetComponent(target.Value, + out var humanoidComponent) + ? Loc.GetString(_prototypes.Index(humanoidComponent.Species).Name) : Loc.GetString("health-analyzer-window-entity-unknown-species-text"); // Basic Diagnostic diff --git a/Content.Client/Humanoid/HideableHumanoidLayersSystem.cs b/Content.Client/Humanoid/HideableHumanoidLayersSystem.cs new file mode 100644 index 0000000000..4feb48cbda --- /dev/null +++ b/Content.Client/Humanoid/HideableHumanoidLayersSystem.cs @@ -0,0 +1,74 @@ +using Content.Shared.Humanoid; +using Content.Shared.Inventory; +using Robust.Client.GameObjects; + +namespace Content.Client.Humanoid; + +public sealed class HideableHumanoidLayersSystem : SharedHideableHumanoidLayersSystem +{ + [Dependency] private readonly SpriteSystem _sprite = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnHandleState); + } + + private void OnComponentInit(Entity ent, ref ComponentInit args) + { + UpdateSprite(ent); + } + + private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + UpdateSprite(ent); + } + + public override void SetLayerVisibility( + Entity ent, + HumanoidVisualLayers layer, + bool visible, + SlotFlags source) + { + base.SetLayerVisibility(ent, layer, visible, source); + + if (Resolve(ent, ref ent.Comp)) + UpdateSprite((ent, ent.Comp)); + } + + private void UpdateSprite(Entity ent) + { + foreach (var item in ent.Comp.LastHiddenLayers) + { + if (ent.Comp.HiddenLayers.ContainsKey(item)) + continue; + + var evt = new HumanoidLayerVisibilityChangedEvent(item, true); + RaiseLocalEvent(ent, ref evt); + + if (!_sprite.LayerMapTryGet(ent.Owner, item, out var index, true)) + continue; + + _sprite.LayerSetVisible(ent.Owner, index, true); + } + + foreach (var item in ent.Comp.HiddenLayers.Keys) + { + if (ent.Comp.LastHiddenLayers.Contains(item)) + continue; + + var evt = new HumanoidLayerVisibilityChangedEvent(item, false); + RaiseLocalEvent(ent, ref evt); + + if (!_sprite.LayerMapTryGet(ent.Owner, item, out var index, true)) + continue; + + _sprite.LayerSetVisible(ent.Owner, index, false); + } + + ent.Comp.LastHiddenLayers.Clear(); + ent.Comp.LastHiddenLayers.UnionWith(ent.Comp.HiddenLayers.Keys); + } +} diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs deleted file mode 100644 index 54c2801e33..0000000000 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ /dev/null @@ -1,445 +0,0 @@ -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)); - } - } - } -} diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs b/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs index f900eec1eb..ae32534f64 100644 --- a/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs +++ b/Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs @@ -1,5 +1,4 @@ using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Markings; using Robust.Client.UserInterface; namespace Content.Client.Humanoid; @@ -13,6 +12,8 @@ public sealed class HumanoidMarkingModifierBoundUserInterface : BoundUserInterfa [ViewVariables] private HumanoidMarkingModifierWindow? _window; + private readonly MarkingsViewModel _markingsModel = new(); + public HumanoidMarkingModifierBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } @@ -22,11 +23,11 @@ public sealed class HumanoidMarkingModifierBoundUserInterface : BoundUserInterfa base.Open(); _window = this.CreateWindowCenteredLeft(); - _window.OnMarkingAdded += SendMarkingSet; - _window.OnMarkingRemoved += SendMarkingSet; - _window.OnMarkingColorChange += SendMarkingSetNoResend; - _window.OnMarkingRankChange += SendMarkingSet; - _window.OnLayerInfoModified += SendBaseLayer; + _window.MarkingPickerWidget.SetModel(_markingsModel); + _window.RespectLimits.OnPressed += args => _markingsModel.EnforceLimits = args.Button.Pressed; + _window.RespectGroupSex.OnPressed += args => _markingsModel.EnforceGroupAndSexRestrictions = args.Button.Pressed; + + _markingsModel.MarkingsChanged += (_, _) => SendMarkingSet(); } protected override void UpdateState(BoundUserInterfaceState state) @@ -34,26 +35,16 @@ public sealed class HumanoidMarkingModifierBoundUserInterface : BoundUserInterfa base.UpdateState(state); if (_window == null || state is not HumanoidMarkingModifierState cast) - { return; - } - - _window.SetState(cast.MarkingSet, cast.Species, cast.Sex, cast.SkinColor, cast.CustomBaseLayers); - } - private void SendMarkingSet(MarkingSet set) - { - SendMessage(new HumanoidMarkingModifierMarkingSetMessage(set, true)); - } - - private void SendMarkingSetNoResend(MarkingSet set) - { - SendMessage(new HumanoidMarkingModifierMarkingSetMessage(set, false)); + _markingsModel.OrganData = cast.OrganData; + _markingsModel.OrganProfileData = cast.OrganProfileData; + _markingsModel.Markings = cast.Markings; } - private void SendBaseLayer(HumanoidVisualLayers layer, CustomBaseLayerInfo? info) + private void SendMarkingSet() { - SendMessage(new HumanoidMarkingModifierBaseLayersSetMessage(layer, info, true)); + SendMessage(new HumanoidMarkingModifierMarkingSetMessage(_markingsModel.Markings)); } } diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml index 1f543cf402..3ea8d1433d 100644 --- a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml +++ b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml @@ -1,18 +1,10 @@ - - - - - - - - - - - - - + + + + - + + diff --git a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs index 4d9d6a90ba..6fd78a4250 100644 --- a/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs +++ b/Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs @@ -14,147 +14,8 @@ namespace Content.Client.Humanoid; [GenerateTypedNameReferences] public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow { - public Action? OnMarkingAdded; - public Action? OnMarkingRemoved; - public Action? OnMarkingColorChange; - public Action? OnMarkingRankChange; - public Action? OnLayerInfoModified; - private readonly IPrototypeManager _protoMan = default!; - - private readonly Dictionary _modifiers = new(); - public HumanoidMarkingModifierWindow() { RobustXamlLoader.Load(this); - _protoMan = IoCManager.Resolve(); - - foreach (var layer in Enum.GetValues()) - { - var modifier = new HumanoidBaseLayerModifier(layer); - BaseLayersContainer.AddChild(modifier); - _modifiers.Add(layer, modifier); - - modifier.OnStateChanged += () => OnStateChanged(layer, modifier); - } - - MarkingPickerWidget.OnMarkingAdded += set => OnMarkingAdded!(set); - MarkingPickerWidget.OnMarkingRemoved += set => OnMarkingRemoved!(set); - MarkingPickerWidget.OnMarkingColorChange += set => OnMarkingColorChange!(set); - MarkingPickerWidget.OnMarkingRankChange += set => OnMarkingRankChange!(set); - MarkingForced.OnToggled += args => MarkingPickerWidget.Forced = args.Pressed; - MarkingIgnoreSpecies.OnToggled += args => MarkingPickerWidget.Forced = args.Pressed; - - MarkingPickerWidget.Forced = MarkingForced.Pressed; - MarkingPickerWidget.IgnoreSpecies = MarkingForced.Pressed; - } - - private void OnStateChanged(HumanoidVisualLayers layer, HumanoidBaseLayerModifier modifier) - { - if (!modifier.Enabled) - { - OnLayerInfoModified?.Invoke(layer, null); - return; - } - - 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, - Color skinColor, - Dictionary info - ) - { - foreach (var (layer, modifier) in _modifiers) - { - if (!info.TryGetValue(layer, out var layerInfo)) - { - modifier.SetState(false, string.Empty, Color.White); - continue; - } - - modifier.SetState(true, layerInfo.Id ?? string.Empty, layerInfo.Color ?? Color.White); - } - - var eyesColor = Color.White; - if (info.TryGetValue(HumanoidVisualLayers.Eyes, out var eyes) && eyes.Color != null) - { - eyesColor = eyes.Color.Value; - } - - MarkingPickerWidget.SetData(markings, species, sex, skinColor, eyesColor); - } - - private sealed class HumanoidBaseLayerModifier : BoxContainer - { - private CheckBox _enable; - private LineEdit _lineEdit; - private ColorSelectorSliders _colorSliders; - private BoxContainer _infoBox; - - public bool Enabled => _enable.Pressed; - public string Text => _lineEdit.Text; - public Color Color => _colorSliders.Color; - - public Action? OnStateChanged; - - public HumanoidBaseLayerModifier(HumanoidVisualLayers layer) - { - HorizontalExpand = true; - Orientation = LayoutOrientation.Vertical; - var labelBox = new BoxContainer - { - MinWidth = 250, - HorizontalExpand = true - }; - AddChild(labelBox); - - labelBox.AddChild(new Label - { - HorizontalExpand = true, - Text = layer.ToString() - }); - _enable = new CheckBox - { - Text = Loc.GetString("humanoid-marking-modifier-enable"), - HorizontalAlignment = HAlignment.Right - }; - - labelBox.AddChild(_enable); - _infoBox = new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Visible = false - }; - _enable.OnToggled += args => - { - _infoBox.Visible = args.Pressed; - OnStateChanged!(); - }; - - var lineEditBox = new BoxContainer { SeparationOverride = 4 }; - lineEditBox.AddChild(new Label { Text = Loc.GetString("humanoid-marking-modifier-prototype-id") }); - - // TODO: This line edit should really be an options / dropdown selector, not text. - _lineEdit = new() { MinWidth = 200 }; - _lineEdit.OnTextEntered += args => OnStateChanged!(); - lineEditBox.AddChild(_lineEdit); - _infoBox.AddChild(lineEditBox); - - _colorSliders = new(); - _colorSliders.OnColorChanged += color => OnStateChanged!(); - _infoBox.AddChild(_colorSliders); - AddChild(_infoBox); - } - - public void SetState(bool enabled, string state, Color color) - { - _enable.Pressed = enabled; - _infoBox.Visible = enabled; - _lineEdit.Text = state; - _colorSliders.Color = color; - } } } diff --git a/Content.Client/Humanoid/LayerMarkingItem.xaml b/Content.Client/Humanoid/LayerMarkingItem.xaml new file mode 100644 index 0000000000..11695a321f --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingItem.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + diff --git a/Content.Client/Humanoid/LayerMarkingItem.xaml.cs b/Content.Client/Humanoid/LayerMarkingItem.xaml.cs new file mode 100644 index 0000000000..0e16efcd0d --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingItem.xaml.cs @@ -0,0 +1,212 @@ +using System.Linq; +using Content.Client.Guidebook.Controls; +using Content.Shared.Body; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.UserInterface; +using Robust.Shared.Input; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client.Humanoid; + +[GenerateTypedNameReferences] +public sealed partial class LayerMarkingItem : BoxContainer, ISearchableControl +{ + [Dependency] private readonly IEntityManager _entity = default!; + + private readonly SpriteSystem _sprite; + + private readonly MarkingsViewModel _markingsModel; + private readonly MarkingPrototype _markingPrototype; + private readonly ProtoId _organ; + private readonly HumanoidVisualLayers _layer; + private bool _interactive; + + private List? _colorSliders; + + public event Action? Pressed; + public event Action? Unpressed; + public ProtoId MarkingId => _markingPrototype.ID; + + public LayerMarkingItem(MarkingsViewModel model, ProtoId organ, HumanoidVisualLayers layer, MarkingPrototype prototype, bool interactive) + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + _sprite = _entity.System(); + + _markingsModel = model; + _markingPrototype = prototype; + _organ = organ; + _layer = layer; + _interactive = interactive; + + UpdateData(); + UpdateSelection(); + + SelectButton.OnPressed += SelectButtonPressed; + ColorsButton.OnPressed += ColorsButtonPressed; + + OnKeyBindDown += OnPressed; + OnKeyBindUp += OnUnpressed; + + if (!interactive) + { + SelectButton.MouseFilter = Control.MouseFilterMode.Ignore; + } + } + + protected override void EnteredTree() + { + base.EnteredTree(); + + _markingsModel.MarkingsReset += UpdateSelection; + _markingsModel.MarkingsChanged += MarkingsChanged; + } + + protected override void ExitedTree() + { + base.ExitedTree(); + + _markingsModel.MarkingsReset -= UpdateSelection; + _markingsModel.MarkingsChanged -= MarkingsChanged; + } + + private void MarkingsChanged(ProtoId organ, HumanoidVisualLayers layer) + { + if (_organ != organ || _layer != layer) + return; + + UpdateSelection(); + } + + private void UpdateData() + { + MarkingTexture.Textures = _markingPrototype.Sprites.Select(layer => _sprite.Frame0(layer)).ToList(); + SelectButton.Text = Loc.GetString($"marking-{_markingPrototype.ID}"); + } + + private void UpdateSelection() + { + var selected = _markingsModel.IsMarkingSelected(_organ, _layer, _markingPrototype.ID); + SelectButton.Pressed = selected && _interactive; + ColorsButton.Visible = selected && _interactive && _markingsModel.IsMarkingColorCustomizable(_organ, _layer, _markingPrototype.ID); + + if (!selected || !_interactive) + { + ColorsButton.Pressed = false; + ColorsContainer.Visible = false; + } + + if (_markingsModel.TryGetMarking(_organ, _layer, _markingPrototype.ID) is { } marking && + _colorSliders is { } sliders) + { + for (var i = 0; i < _markingPrototype.Sprites.Count; i++) + { + sliders[i].Color = marking.MarkingColors[i]; + } + } + } + + private void SelectButtonPressed(BaseButton.ButtonEventArgs args) + { + if (!_interactive) + { + SelectButton.Pressed = false; + return; + } + + if (_markingsModel.IsMarkingSelected(_organ, _layer, _markingPrototype.ID)) + { + if (!_markingsModel.TryDeselectMarking(_organ, _layer, _markingPrototype.ID)) + { + SelectButton.Pressed = true; + } + } + else + { + if (!_markingsModel.TrySelectMarking(_organ, _layer, _markingPrototype.ID)) + { + SelectButton.Pressed = false; + } + } + } + + private void ColorsButtonPressed(BaseButton.ButtonEventArgs args) + { + ColorsContainer.Visible = ColorsButton.Pressed; + + if (_colorSliders is not null) + return; + + if (_markingsModel.TryGetMarking(_organ, _layer, _markingPrototype.ID) is not { } marking) + return; + + _colorSliders = new(); + + for (var i = 0; i < _markingPrototype.Sprites.Count; i++) + { + var container = new BoxContainer() + { + Orientation = LayoutOrientation.Vertical, + HorizontalExpand = true, + }; + + ColorsContainer.AddChild(container); + + var selector = new ColorSelectorSliders(); + selector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; + + var label = _markingPrototype.Sprites[i] switch + { + SpriteSpecifier.Rsi rsi => Loc.GetString($"marking-{_markingPrototype.ID}-{rsi.RsiState}"), + SpriteSpecifier.Texture texture => Loc.GetString($"marking-{_markingPrototype.ID}-{texture.TexturePath.Filename}"), + _ => throw new InvalidOperationException("SpriteSpecifier not of known type"), + }; + + container.AddChild(new Label { Text = label }); + container.AddChild(selector); + + selector.Color = marking.MarkingColors[i]; + + _colorSliders.Add(selector); + + var colorIndex = i; + selector.OnColorChanged += _ => + { + _markingsModel.TrySetMarkingColor(_organ, _layer, _markingPrototype.ID, colorIndex, selector.Color); + }; + } + } + + public bool CheckMatchesSearch(string query) + { + return Loc.GetString($"marking-{_markingPrototype.ID}").Contains(query, StringComparison.OrdinalIgnoreCase); + } + + public void SetHiddenState(bool state, string query) + { + Visible = CheckMatchesSearch(query) ? state : !state; + } + + private void OnPressed(GUIBoundKeyEventArgs args) + { + if (args.Function != EngineKeyFunctions.UIClick) + return; + + Pressed?.Invoke(args, this); + } + + private void OnUnpressed(GUIBoundKeyEventArgs args) + { + if (args.Function != EngineKeyFunctions.UIClick) + return; + + Unpressed?.Invoke(args, this); + } +} diff --git a/Content.Client/Humanoid/LayerMarkingOrderer.xaml b/Content.Client/Humanoid/LayerMarkingOrderer.xaml new file mode 100644 index 0000000000..c25a29296e --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingOrderer.xaml @@ -0,0 +1,3 @@ + + + diff --git a/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs b/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs new file mode 100644 index 0000000000..c3275ca885 --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs @@ -0,0 +1,192 @@ +using System.Linq; +using System.Numerics; +using Content.Client.Interaction; +using Content.Client.Stylesheets; +using Content.Shared.Body; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Humanoid; + +[GenerateTypedNameReferences] +public sealed partial class LayerMarkingOrderer : BoxContainer +{ + private readonly ProtoId _organ; + private readonly HumanoidVisualLayers _layer; + private readonly MarkingsViewModel _markingsModel; + private readonly DragDropHelper _dragDropHelper; + private readonly List _beacons = new(); + private LayerDragDropBeacon? _dragTarget; + + [Dependency] private readonly IPrototypeManager _prototype = default!; + + public LayerMarkingOrderer(MarkingsViewModel markingsModel, ProtoId organ, HumanoidVisualLayers layer) + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + _markingsModel = markingsModel; + _organ = organ; + _layer = layer; + _dragDropHelper = new(OnBeginDrag, OnContinueDrag, OnEndDrag); + + UpdateItems(); + } + + protected override void EnteredTree() + { + base.EnteredTree(); + + _markingsModel.MarkingsReset += UpdateItems; + _markingsModel.MarkingsChanged += MarkingsChanged; + } + + protected override void ExitedTree() + { + base.ExitedTree(); + + _markingsModel.MarkingsReset -= UpdateItems; + _markingsModel.MarkingsChanged -= MarkingsChanged; + } + + private void MarkingsChanged(ProtoId organ, HumanoidVisualLayers layer) + { + if (_organ != organ || _layer != layer) + return; + + UpdateItems(); + } + + private void UpdateItems() + { + Items.RemoveAllChildren(); + _beacons.Clear(); + + if (_markingsModel.SelectedMarkings(_organ, _layer) is not { } markings) + return; + + for (var idx = 0; idx < markings.Count; idx++) + { + var marking = markings[idx]; + + var container = new LayerMarkingItemContainer(); + container.Margin = new(4); + + var item = new LayerMarkingItem(_markingsModel, _organ, _layer, _prototype.Index(marking.MarkingId), false); + item.DefaultCursorShape = CursorShape.Hand; + item.Pressed += (args, control) => OnItemPressed(args, control, container); + item.Unpressed += OnItemUnpressed; + + container.AddChild(item); + + var before = new LayerDragDropBeacon(CandidatePosition.Before, idx); + var after = new LayerDragDropBeacon(CandidatePosition.After, idx); + _beacons.Add(before); + _beacons.Add(after); + + Items.AddChild(before); + Items.AddChild(container); + Items.AddChild(after); + } + } + + private void OnItemPressed(GUIBoundKeyEventArgs args, LayerMarkingItem control, LayerMarkingItemContainer container) + { + _dragDropHelper.MouseDown(new(control, container)); + } + + private void OnItemUnpressed(GUIBoundKeyEventArgs args, LayerMarkingItem control) + { + _dragDropHelper.EndDrag(); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + _dragDropHelper.Update(args.DeltaSeconds); + } + + private bool OnBeginDrag() + { + var (item, container) = _dragDropHelper.Dragged; + + container.Visible = false; + item.Orphan(); + item.DefaultCursorShape = CursorShape.Move; + UserInterfaceManager.PopupRoot.AddChild(item); + LayoutContainer.SetPosition(item, UserInterfaceManager.MousePositionScaled.Position - new Vector2(32, 32)); + return true; + } + + private bool OnContinueDrag(float frameTime) + { + var (item, container) = _dragDropHelper.Dragged; + + LayoutContainer.SetPosition(item, UserInterfaceManager.MousePositionScaled.Position - new Vector2(32, 32)); + + var closestBeacon = + _beacons.MinBy(beacon => + (UserInterfaceManager.MousePositionScaled.Position - beacon.GlobalPosition).LengthSquared()); + + if (closestBeacon != _dragTarget) + { + _dragTarget?.UnbecomeTarget(); + _dragTarget = closestBeacon; + _dragTarget?.BecomeTarget(); + } + + return true; + } + + private void OnEndDrag() + { + var (item, container) = _dragDropHelper.Dragged; + + container.Visible = true; + item.Orphan(); + container.AddChild(item); + _dragTarget?.UnbecomeTarget(); + + if (_dragTarget != null) + { + _markingsModel.ChangeMarkingOrder(_organ, _layer, item.MarkingId, _dragTarget.CandidatePosition, _dragTarget.Index); + } + } +} + +internal readonly record struct LayerMarkingDragged(LayerMarkingItem Item, LayerMarkingItemContainer Container); + +internal sealed class LayerMarkingItemContainer : PanelContainer +{ + public LayerMarkingItemContainer() + { + SetHeight = 64; + HorizontalExpand = true; + } +} + +internal sealed class LayerDragDropBeacon(CandidatePosition position, int index) : PanelContainer +{ + public readonly CandidatePosition CandidatePosition = position; + public readonly int Index = index; + + public void BecomeTarget() + { + SetHeight = 64; + HorizontalExpand = true; + SetOnlyStyleClass(StyleClass.PanelDropTarget); + } + + public void UnbecomeTarget() + { + SetHeight = float.NaN; + RemoveStyleClass(StyleClass.PanelDropTarget); + } +} diff --git a/Content.Client/Humanoid/LayerMarkingPicker.xaml b/Content.Client/Humanoid/LayerMarkingPicker.xaml new file mode 100644 index 0000000000..c5804584ac --- /dev/null +++ b/Content.Client/Humanoid/LayerMarkingPicker.xaml @@ -0,0 +1,12 @@ + + + + + + + + +