]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Visual nubody (humanoid appearance refactor) (#42476)
authorpathetic meowmeow <uhhadd@gmail.com>
Tue, 20 Jan 2026 07:07:53 +0000 (02:07 -0500)
committerGitHub <noreply@github.com>
Tue, 20 Jan 2026 07:07:53 +0000 (07:07 +0000)
* 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>
204 files changed:
Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs
Content.Client/Body/VisualBodySystem.cs [new file with mode: 0644]
Content.Client/Clothing/ClientClothingSystem.cs
Content.Client/HealthAnalyzer/UI/HealthAnalyzerControl.xaml.cs
Content.Client/Humanoid/HideableHumanoidLayersSystem.cs [new file with mode: 0644]
Content.Client/Humanoid/HumanoidAppearanceSystem.cs [deleted file]
Content.Client/Humanoid/HumanoidMarkingModifierBoundUserInterface.cs
Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml
Content.Client/Humanoid/HumanoidMarkingModifierWindow.xaml.cs
Content.Client/Humanoid/LayerMarkingItem.xaml [new file with mode: 0644]
Content.Client/Humanoid/LayerMarkingItem.xaml.cs [new file with mode: 0644]
Content.Client/Humanoid/LayerMarkingOrderer.xaml [new file with mode: 0644]
Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs [new file with mode: 0644]
Content.Client/Humanoid/LayerMarkingPicker.xaml [new file with mode: 0644]
Content.Client/Humanoid/LayerMarkingPicker.xaml.cs [new file with mode: 0644]
Content.Client/Humanoid/MarkingPicker.xaml
Content.Client/Humanoid/MarkingPicker.xaml.cs
Content.Client/Humanoid/MarkingsViewModel.cs [new file with mode: 0644]
Content.Client/Humanoid/OrganMarkingPicker.xaml [new file with mode: 0644]
Content.Client/Humanoid/OrganMarkingPicker.xaml.cs [new file with mode: 0644]
Content.Client/Humanoid/SingleMarkingPicker.xaml [deleted file]
Content.Client/Humanoid/SingleMarkingPicker.xaml.cs [deleted file]
Content.Client/Interaction/DragDropHelper.cs
Content.Client/Lobby/LobbyUIController.cs
Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs
Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
Content.Client/MagicMirror/MagicMirrorBoundUserInterface.cs
Content.Client/MagicMirror/MagicMirrorSystem.cs [deleted file]
Content.Client/MagicMirror/MagicMirrorWindow.xaml
Content.Client/MagicMirror/MagicMirrorWindow.xaml.cs
Content.Client/Stylesheets/Sheetlets/PanelSheetlet.cs
Content.Client/Stylesheets/StyleClass.cs
Content.Client/Zombies/ZombieSystem.cs
Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs
Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.Designer.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs
Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.Designer.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs
Content.Server.Database/Model.cs
Content.Server.Database/ModelSqlite.cs
Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
Content.Server/Antag/AntagSelectionSystem.cs
Content.Server/Body/VisualBodySystem.cs [new file with mode: 0644]
Content.Server/Cloning/CloningSystem.cs
Content.Server/Clothing/Systems/OutfitSystem.cs
Content.Server/Database/DataNodeJsonExtensions.cs [new file with mode: 0644]
Content.Server/Database/ServerDbBase.cs
Content.Server/Database/ServerDbManager.cs
Content.Server/Database/ServerDbPostgres.cs
Content.Server/Database/ServerDbSqlite.cs
Content.Server/Destructible/DestructibleSystem.cs
Content.Server/GameTicking/GameTicker.Spawning.cs
Content.Server/GameTicking/Rules/AntagLoadProfileRuleSystem.cs
Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
Content.Server/GameTicking/Rules/ThiefRuleSystem.cs
Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
Content.Server/Humanoid/Components/RandomHumanoidAppearanceComponent.cs
Content.Server/Humanoid/HideableHumanoidLayersSystem.cs [new file with mode: 0644]
Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.Modifier.cs [deleted file]
Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs [deleted file]
Content.Server/Humanoid/Systems/RandomHumanoidAppearanceSystem.cs
Content.Server/Humanoid/Systems/RandomHumanoidSystem.cs
Content.Server/MagicMirror/MagicMirrorSystem.cs [deleted file]
Content.Server/Materials/MaterialReclaimerSystem.cs
Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs
Content.Server/Objectives/Systems/HijackShuttleConditionSystem.cs
Content.Server/Objectives/Systems/SpeciesRequirementSystem.cs
Content.Server/Polymorph/Systems/PolymorphSystem.cs
Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs
Content.Server/Salvage/SalvageSystem.Runner.cs
Content.Server/Speech/EntitySystems/VocalSystem.cs
Content.Server/Station/Systems/StationSpawningSystem.cs
Content.Server/StationEvents/Events/MassHallucinationsRule.cs
Content.Server/Store/Conditions/BuyerSpeciesCondition.cs
Content.Server/Toolshed/Commands/Misc/CloneCommand.cs
Content.Server/Wagging/WaggingSystem.cs
Content.Server/Xenoarchaeology/Artifact/XAE/XAEPolymorphSystem.cs
Content.Server/Zombies/ZombieSystem.Transform.cs
Content.Server/Zombies/ZombieSystem.cs
Content.Shared/Body/BodySystem.Relay.cs
Content.Shared/Body/InitialBodyComponent.cs [new file with mode: 0644]
Content.Shared/Body/InitialBodySystem.cs [new file with mode: 0644]
Content.Shared/Body/SharedVisualBodySystem.Initial.cs [new file with mode: 0644]
Content.Shared/Body/SharedVisualBodySystem.Modifiers.cs [new file with mode: 0644]
Content.Shared/Body/SharedVisualBodySystem.cs [new file with mode: 0644]
Content.Shared/Body/VisualBodyComponent.cs [new file with mode: 0644]
Content.Shared/Body/VisualOrganComponent.cs [new file with mode: 0644]
Content.Shared/Body/VisualOrganMarkingsComponent.cs [new file with mode: 0644]
Content.Shared/Changeling/Components/ChangelingDevourComponent.cs
Content.Shared/Changeling/Systems/ChangelingClonerSystem.cs
Content.Shared/Changeling/Systems/ChangelingDevourSystem.cs
Content.Shared/Changeling/Systems/ChangelingTransformSystem.cs
Content.Shared/Changeling/Systems/SharedChangelingIdentitySystem.cs
Content.Shared/Clothing/EntitySystems/HideLayerClothingSystem.cs
Content.Shared/Clothing/LoadoutSystem.cs
Content.Shared/Humanoid/HideableHumanoidLayersComponent.cs [new file with mode: 0644]
Content.Shared/Humanoid/HumanoidAppearanceComponent.cs [deleted file]
Content.Shared/Humanoid/HumanoidCharacterAppearance.cs
Content.Shared/Humanoid/HumanoidProfileComponent.cs [new file with mode: 0644]
Content.Shared/Humanoid/HumanoidProfileExportV1.cs [new file with mode: 0644]
Content.Shared/Humanoid/HumanoidProfileExportV2.cs [moved from Content.Shared/Humanoid/HumanoidProfileExport.cs with 80% similarity]
Content.Shared/Humanoid/HumanoidProfileSystem.cs [new file with mode: 0644]
Content.Shared/Humanoid/HumanoidVisualLayers.cs
Content.Shared/Humanoid/Markings/ColoringTypes/CategoryColoring.cs
Content.Shared/Humanoid/Markings/ColoringTypes/EyeColoring.cs
Content.Shared/Humanoid/Markings/ColoringTypes/SimpleColoring.cs
Content.Shared/Humanoid/Markings/ColoringTypes/SkinColoring.cs
Content.Shared/Humanoid/Markings/ColoringTypes/TattooColoring.cs
Content.Shared/Humanoid/Markings/Marking.cs
Content.Shared/Humanoid/Markings/MarkingCategories.cs [deleted file]
Content.Shared/Humanoid/Markings/MarkingColoring.cs
Content.Shared/Humanoid/Markings/MarkingManager.cs
Content.Shared/Humanoid/Markings/MarkingPoints.cs [deleted file]
Content.Shared/Humanoid/Markings/MarkingPrototype.cs
Content.Shared/Humanoid/Markings/MarkingsComponent.cs [deleted file]
Content.Shared/Humanoid/Markings/MarkingsGroupPrototype.cs [new file with mode: 0644]
Content.Shared/Humanoid/Markings/MarkingsSet.cs [deleted file]
Content.Shared/Humanoid/Prototypes/HumanoidProfilePrototype.cs
Content.Shared/Humanoid/Prototypes/HumanoidSpritePrototypes.cs [deleted file]
Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs
Content.Shared/Humanoid/SharedHideableHumanoidLayersSystem.cs [new file with mode: 0644]
Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs [deleted file]
Content.Shared/Humanoid/SharedHumanoidMarkingModifierSystem.cs
Content.Shared/IdentityManagement/IdentitySystem.cs
Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs
Content.Shared/MagicMirror/MagicMirrorComponent.cs
Content.Shared/MagicMirror/MagicMirrorSystem.cs [new file with mode: 0644]
Content.Shared/MagicMirror/SharedMagicMirrorSystem.cs [deleted file]
Content.Shared/Mind/SharedMindSystem.cs
Content.Shared/Preferences/HumanoidCharacterProfile.cs
Content.Shared/Trigger/Systems/DnaScrambleOnTriggerSystem.cs
Content.Shared/Wagging/WaggingComponent.cs
Content.Shared/Whistle/WhistleSystem.cs
Content.Shared/Zombies/ZombieComponent.cs
Resources/Locale/en-US/preferences/ui/markings-picker.ftl
Resources/Prototypes/Body/Species/arachnid.yml
Resources/Prototypes/Body/Species/diona.yml
Resources/Prototypes/Body/Species/dwarf.yml
Resources/Prototypes/Body/Species/gingerbread.yml
Resources/Prototypes/Body/Species/human.yml
Resources/Prototypes/Body/Species/moth.yml
Resources/Prototypes/Body/Species/reptilian.yml
Resources/Prototypes/Body/Species/skeleton.yml
Resources/Prototypes/Body/Species/slime.yml
Resources/Prototypes/Body/Species/vox.yml
Resources/Prototypes/Body/Species/vulpkanin.yml
Resources/Prototypes/Body/base_organs.yml
Resources/Prototypes/Body/species_appearance.yml
Resources/Prototypes/Body/species_base.yml
Resources/Prototypes/Entities/Clothing/Back/smuggler.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/Vulpkanin/vulpkanin_chest.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/Vulpkanin/vulpkanin_ears.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/Vulpkanin/vulpkanin_hair.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/Vulpkanin/vulpkanin_head.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/Vulpkanin/vulpkanin_limbs.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/Vulpkanin/vulpkanin_snout.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/Vulpkanin/vulpkanin_tail.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/arachnid.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/cat_parts.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/diona.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/ears.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/gauze.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/human_facial_hair.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/human_hair.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/human_noses.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/moth.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/reptilian.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/scars.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/slime.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/tattoos.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/undergarments.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_facial_hair.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_hair.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_parts.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_scars.yml
Resources/Prototypes/Entities/Mobs/Customization/Markings/vox_tattoos.yml
Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml
Resources/Prototypes/Entities/Mobs/Player/clone.yml
Resources/Prototypes/Entities/Mobs/Player/dragon.yml
Resources/Prototypes/Entities/Mobs/Player/human.yml
Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml
Resources/Prototypes/Entities/Objects/Specific/Service/barber.yml
Resources/Prototypes/Entities/Structures/Furniture/toilet.yml
Resources/Prototypes/Entities/Structures/Wallmounts/Misc/mirror.yml
Resources/Prototypes/NPCs/hugbot.yml
Resources/Prototypes/Species/arachnid.yml
Resources/Prototypes/Species/diona.yml
Resources/Prototypes/Species/dwarf.yml
Resources/Prototypes/Species/gingerbread.yml
Resources/Prototypes/Species/human.yml
Resources/Prototypes/Species/moth.yml
Resources/Prototypes/Species/reptilian.yml
Resources/Prototypes/Species/skeleton.yml
Resources/Prototypes/Species/slime.yml
Resources/Prototypes/Species/vox.yml
Resources/Prototypes/Species/vulpkanin.yml
Resources/Textures/Interface/palette.svg [new file with mode: 0644]
Resources/Textures/Interface/palette.svg.png [new file with mode: 0644]

index d96980fb1d20ae6925c28dd96abfd0ddb47eca2a..a9dcfaf2b06022dd42d4794b7cebf7040606e680 100644 (file)
@@ -25,8 +25,8 @@ public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
 
         var index = _sprite.LayerMapReserve((ent.Owner, sprite), ent.Comp.LayerMap);
 
-        if (TryComp<HumanoidAppearanceComponent>(ent, out var humanoidAppearance) &&
-            ent.Comp.SpeciesSprites.TryGetValue(humanoidAppearance.Species, out var speciesSprite))
+        if (TryComp<HumanoidProfileComponent>(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 (file)
index 0000000..724dd22
--- /dev/null
@@ -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<VisualOrganComponent, OrganGotInsertedEvent>(OnOrganGotInserted);
+        SubscribeLocalEvent<VisualOrganComponent, OrganGotRemovedEvent>(OnOrganGotRemoved);
+        SubscribeLocalEvent<VisualOrganComponent, AfterAutoHandleStateEvent>(OnOrganState);
+
+        SubscribeLocalEvent<VisualOrganMarkingsComponent, OrganGotInsertedEvent>(OnMarkingsGotInserted);
+        SubscribeLocalEvent<VisualOrganMarkingsComponent, OrganGotRemovedEvent>(OnMarkingsGotRemoved);
+        SubscribeLocalEvent<VisualOrganMarkingsComponent, AfterAutoHandleStateEvent>(OnMarkingsState);
+
+        SubscribeLocalEvent<VisualOrganMarkingsComponent, BodyRelayedEvent<HumanoidLayerVisibilityChangedEvent>>(OnMarkingsChangedVisibility);
+
+        Subs.CVar(_cfg, CCVars.AccessibilityClientCensorNudity, OnCensorshipChanged, true);
+        Subs.CVar(_cfg, CCVars.AccessibilityServerCensorNudity, OnCensorshipChanged, true);
+    }
+
+    private void OnCensorshipChanged(bool value)
+    {
+        var query = AllEntityQuery<OrganComponent, VisualOrganMarkingsComponent>();
+        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<VisualOrganComponent> ent, ref OrganGotInsertedEvent args)
+    {
+        ApplyVisual(ent, args.Target);
+    }
+
+    private void OnOrganGotRemoved(Entity<VisualOrganComponent> ent, ref OrganGotRemovedEvent args)
+    {
+        RemoveVisual(ent, args.Target);
+    }
+
+    private void OnOrganState(Entity<VisualOrganComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        if (Comp<OrganComponent>(ent).Body is not { } body)
+            return;
+
+        ApplyVisual(ent, body);
+    }
+
+    private void ApplyVisual(Entity<VisualOrganComponent> 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<VisualOrganComponent> 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<VisualOrganMarkingsComponent> ent, ref OrganGotInsertedEvent args)
+    {
+        ApplyMarkings(ent, args.Target);
+    }
+
+    private void OnMarkingsGotRemoved(Entity<VisualOrganMarkingsComponent> ent, ref OrganGotRemovedEvent args)
+    {
+        RemoveMarkings(ent, args.Target);
+    }
+
+    private void OnMarkingsState(Entity<VisualOrganMarkingsComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        if (Comp<OrganComponent>(ent).Body is not { } body)
+            return;
+
+        RemoveMarkings(ent, body);
+        ApplyMarkings(ent, body);
+    }
+
+    protected override void SetOrganColor(Entity<VisualOrganComponent> ent, Color color)
+    {
+        base.SetOrganColor(ent, color);
+
+        if (Comp<OrganComponent>(ent).Body is not { } body)
+            return;
+
+        ApplyVisual(ent, body);
+    }
+
+    protected override void SetOrganMarkings(Entity<VisualOrganMarkingsComponent> ent, Dictionary<HumanoidVisualLayers, List<Marking>> markings)
+    {
+        base.SetOrganMarkings(ent, markings);
+
+        if (Comp<OrganComponent>(ent).Body is not { } body)
+            return;
+
+        RemoveMarkings(ent, body);
+        ApplyMarkings(ent, body);
+    }
+
+    protected override void SetOrganAppearance(Entity<VisualOrganComponent> ent, PrototypeLayerData data)
+    {
+        base.SetOrganAppearance(ent, data);
+
+        if (Comp<OrganComponent>(ent).Body is not { } body)
+            return;
+
+        ApplyVisual(ent, body);
+    }
+
+    private IEnumerable<Marking> AllMarkings(Entity<VisualOrganMarkingsComponent> 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<VisualOrganMarkingsComponent> ent, EntityUid target)
+    {
+        var applied = new List<Marking>();
+        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<VisualOrganMarkingsComponent> 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<VisualOrganMarkingsComponent> ent, ref BodyRelayedEvent<HumanoidLayerVisibilityChangedEvent> 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);
+                }
+            }
+        }
+    }
+}
index 417e540d4aa6927611106ddbe9d5a9921952f17f..1a6db7a1b655c5c9acb40ef480ed525f6be3e941 100644 (file)
@@ -271,7 +271,7 @@ public sealed class ClientClothingSystem : ClothingSystem
         // Select displacement maps
         var displacementData = inventory.Displacements.GetValueOrDefault(slot); //Default unsexed map
 
-        var equipeeSex = CompOrNull<HumanoidAppearanceComponent>(equipee)?.Sex;
+        var equipeeSex = CompOrNull<HumanoidProfileComponent>(equipee)?.Sex;
         if (equipeeSex != null)
         {
             switch (equipeeSex)
index 949b4770c4c8041ac3933427aee4547e81927536..92079542bd3ff295c64867c41fadf6d9057f31e1 100644 (file)
@@ -79,9 +79,9 @@ public sealed partial class HealthAnalyzerControl : BoxContainer
         NameLabel.SetMessage(name);
 
         SpeciesLabel.Text =
-            _entityManager.TryGetComponent<HumanoidAppearanceComponent>(target.Value,
-                out var humanoidAppearanceComponent)
-                ? Loc.GetString(_prototypes.Index<SpeciesPrototype>(humanoidAppearanceComponent.Species).Name)
+            _entityManager.TryGetComponent<HumanoidProfileComponent>(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 (file)
index 0000000..4feb48c
--- /dev/null
@@ -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<HideableHumanoidLayersComponent, ComponentInit>(OnComponentInit);
+        SubscribeLocalEvent<HideableHumanoidLayersComponent, AfterAutoHandleStateEvent>(OnHandleState);
+    }
+
+    private void OnComponentInit(Entity<HideableHumanoidLayersComponent> ent, ref ComponentInit args)
+    {
+        UpdateSprite(ent);
+    }
+
+    private void OnHandleState(Entity<HideableHumanoidLayersComponent> ent, ref AfterAutoHandleStateEvent args)
+    {
+        UpdateSprite(ent);
+    }
+
+    public override void SetLayerVisibility(
+        Entity<HideableHumanoidLayersComponent?> 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<HideableHumanoidLayersComponent> 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 (file)
index 54c2801..0000000
+++ /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<HumanoidAppearanceComponent, AfterAutoHandleStateEvent>(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<SpriteComponent>(uid)));
-    }
-
-    private void OnCvarChanged(bool value)
-    {
-        var humanoidQuery = AllEntityQuery<HumanoidAppearanceComponent, SpriteComponent>();
-        while (humanoidQuery.MoveNext(out var uid, out var humanoidComp, out var spriteComp))
-        {
-            UpdateSprite((uid, humanoidComp, spriteComp));
-        }
-    }
-
-    private void UpdateSprite(Entity<HumanoidAppearanceComponent, SpriteComponent> 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<HumanoidAppearanceComponent, SpriteComponent> entity)
-    {
-        var component = entity.Comp1;
-        var sprite = entity.Comp2;
-
-        var oldLayers = new HashSet<HumanoidVisualLayers>(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<HumanoidAppearanceComponent, SpriteComponent> 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<HumanoidSpeciesSpriteLayer>(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);
-    }
-
-    /// <summary>
-    ///     Loads a profile directly into a humanoid.
-    /// </summary>
-    /// <param name="uid">The humanoid entity's UID</param>
-    /// <param name="profile">The profile to load.</param>
-    /// <param name="humanoid">The humanoid entity's humanoid component.</param>
-    /// <remarks>
-    ///     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.
-    /// </remarks>
-    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<HumanoidVisualLayers, CustomBaseLayerInfo>();
-
-        var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(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<Marking, MarkingPrototype>();
-        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<HumanoidVisualLayers>();
-        humanoid.HiddenLayers = new Dictionary<HumanoidVisualLayers, SlotFlags>();
-        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<SpriteComponent>(uid)));
-    }
-
-    private void ApplyMarkingSet(Entity<HumanoidAppearanceComponent, SpriteComponent> 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<HumanoidAppearanceComponent, SpriteComponent> 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<SpriteComponent> 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<HumanoidAppearanceComponent, SpriteComponent> entity, bool undergarmentTop, bool undergarmentBottom)
-    {
-        var humanoid = entity.Comp1;
-
-        if (undergarmentTop && humanoid.UndergarmentTop != null)
-        {
-            var marking = new Marking(humanoid.UndergarmentTop, new List<Color> { 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> { marking });
-                ApplyMarking(prototype, null, true, entity);
-            }
-        }
-
-        if (undergarmentBottom && humanoid.UndergarmentBottom != null)
-        {
-            var marking = new Marking(humanoid.UndergarmentBottom, new List<Color> { new Color() });
-            if (_markingManager.TryGetMarking(marking, out var prototype))
-            {
-                humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentBottom, new List<Marking> { marking });
-                ApplyMarking(prototype, null, true, entity);
-            }
-        }
-    }
-
-    private void ApplyMarking(MarkingPrototype markingPrototype,
-        IReadOnlyList<Color>? colors,
-        bool visible,
-        Entity<HumanoidAppearanceComponent, SpriteComponent> 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<HumanoidAppearanceComponent> ent,
-        HumanoidVisualLayers layer,
-        bool visible,
-        SlotFlags? slot,
-        ref bool dirty)
-    {
-        base.SetLayerVisibility(ent, layer, visible, slot, ref dirty);
-
-        var sprite = Comp<SpriteComponent>(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));
-            }
-        }
-    }
-}
index f900eec1eb4a1895ee2b7046e503800e12b115a7..ae32534f643905228c93afd412f735595dbf7f4a 100644 (file)
@@ -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<HumanoidMarkingModifierWindow>();
-        _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));
     }
 }
 
index 1f543cf402737506c71564ae55559984c83edfb7..3ea8d1433d818e0f07b8300426a5a3cfdb2d0afa 100644 (file)
@@ -1,18 +1,10 @@
 <DefaultWindow xmlns="https://spacestation14.io"
                xmlns:humanoid="clr-namespace:Content.Client.Humanoid">
-    <ScrollContainer MinHeight="500" MinWidth="700">
-        <BoxContainer Orientation="Vertical" HorizontalExpand="True">
-            <humanoid:MarkingPicker Name="MarkingPickerWidget" />
-            <BoxContainer>
-                <CheckBox Name="MarkingForced" Text="{Loc humanoid-marking-modifier-force}" Pressed="True" />
-                <CheckBox Name="MarkingIgnoreSpecies" Text="{Loc humanoid-marking-modifier-ignore-species}" Pressed="True" />
-            </BoxContainer>
-            <Collapsible HorizontalExpand="True">
-                <CollapsibleHeading Title="{Loc humanoid-marking-modifier-base-layers}" />
-                <CollapsibleBody HorizontalExpand="True">
-                    <BoxContainer Name="BaseLayersContainer" Orientation="Vertical" HorizontalExpand="True" />
-                </CollapsibleBody>
-            </Collapsible>
+    <BoxContainer Orientation="Vertical" HorizontalExpand="True" MinHeight="500" MinWidth="700">
+        <BoxContainer>
+            <CheckBox Name="RespectLimits" Text="{Loc humanoid-marking-modifier-respect-limits}" Pressed="True" Access="Public" />
+            <CheckBox Name="RespectGroupSex" Text="{Loc humanoid-marking-modifier-respect-group-sex}" Pressed="True" Access="Public" />
         </BoxContainer>
-    </ScrollContainer>
+        <humanoid:MarkingPicker Name="MarkingPickerWidget" Access="Public" HorizontalExpand="True" VerticalExpand="True" />
+    </BoxContainer>
 </DefaultWindow>
index 4d9d6a90ba4501bab1f196ced7dc3b538c488cdc..6fd78a4250d668e8949c1e9127c5b6580172d860 100644 (file)
@@ -14,147 +14,8 @@ namespace Content.Client.Humanoid;
 [GenerateTypedNameReferences]
 public sealed partial class HumanoidMarkingModifierWindow : DefaultWindow
 {
-    public Action<MarkingSet>? OnMarkingAdded;
-    public Action<MarkingSet>? OnMarkingRemoved;
-    public Action<MarkingSet>? OnMarkingColorChange;
-    public Action<MarkingSet>? OnMarkingRankChange;
-    public Action<HumanoidVisualLayers, CustomBaseLayerInfo?>? OnLayerInfoModified;
-    private readonly IPrototypeManager _protoMan = default!;
-
-    private readonly Dictionary<HumanoidVisualLayers, HumanoidBaseLayerModifier> _modifiers = new();
-
     public HumanoidMarkingModifierWindow()
     {
         RobustXamlLoader.Load(this);
-        _protoMan = IoCManager.Resolve<IPrototypeManager>();
-
-        foreach (var layer in Enum.GetValues<HumanoidVisualLayers>())
-        {
-            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<HumanoidSpeciesSpriteLayer>(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<HumanoidVisualLayers, CustomBaseLayerInfo> 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 (file)
index 0000000..11695a3
--- /dev/null
@@ -0,0 +1,23 @@
+<BoxContainer xmlns="https://spacestation14.io"
+        xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+        Orientation="Vertical"
+        HorizontalExpand="True"
+        MouseFilter="Pass">
+
+    <BoxContainer Orientation="Horizontal" SeparationOverride="4">
+        <PanelContainer SetSize="64 64" HorizontalAlignment="Right" MouseFilter="Ignore">
+            <PanelContainer.PanelOverride>
+                <graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
+            </PanelContainer.PanelOverride>
+            <LayeredTextureRect TextureScale="2 2" Name="MarkingTexture" />
+        </PanelContainer>
+
+        <Button Name="SelectButton" ToggleMode="True" HorizontalExpand="True" />
+
+        <Button Name="ColorsButton" ToggleMode="True" Visible="False">
+            <TextureRect TexturePath="/Textures/Interface/palette.svg.png" HorizontalAlignment="Center" VerticalAlignment="Center" Stretch="Scale" SetSize="24 24" />
+        </Button>
+    </BoxContainer>
+
+    <BoxContainer Name="ColorsContainer" Visible="False" Orientation="Vertical" />
+</BoxContainer>
diff --git a/Content.Client/Humanoid/LayerMarkingItem.xaml.cs b/Content.Client/Humanoid/LayerMarkingItem.xaml.cs
new file mode 100644 (file)
index 0000000..0e16efc
--- /dev/null
@@ -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<OrganCategoryPrototype> _organ;
+    private readonly HumanoidVisualLayers _layer;
+    private bool _interactive;
+
+    private List<ColorSelectorSliders>? _colorSliders;
+
+    public event Action<GUIBoundKeyEventArgs, LayerMarkingItem>? Pressed;
+    public event Action<GUIBoundKeyEventArgs, LayerMarkingItem>? Unpressed;
+    public ProtoId<MarkingPrototype> MarkingId => _markingPrototype.ID;
+
+    public LayerMarkingItem(MarkingsViewModel model, ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer, MarkingPrototype prototype, bool interactive)
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        _sprite = _entity.System<SpriteSystem>();
+
+        _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<OrganCategoryPrototype> 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 (file)
index 0000000..c25a292
--- /dev/null
@@ -0,0 +1,3 @@
+<BoxContainer xmlns="https://spacestation14.io" Orientation="Vertical">
+    <BoxContainer Name="Items" Margin="4" Orientation="Vertical" />
+</BoxContainer>
diff --git a/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs b/Content.Client/Humanoid/LayerMarkingOrderer.xaml.cs
new file mode 100644 (file)
index 0000000..c3275ca
--- /dev/null
@@ -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<OrganCategoryPrototype> _organ;
+    private readonly HumanoidVisualLayers _layer;
+    private readonly MarkingsViewModel _markingsModel;
+    private readonly DragDropHelper<LayerMarkingDragged> _dragDropHelper;
+    private readonly List<LayerDragDropBeacon> _beacons = new();
+    private LayerDragDropBeacon? _dragTarget;
+
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+    public LayerMarkingOrderer(MarkingsViewModel markingsModel, ProtoId<OrganCategoryPrototype> 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<OrganCategoryPrototype> 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<MarkingPrototype>(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 (file)
index 0000000..c580458
--- /dev/null
@@ -0,0 +1,12 @@
+<BoxContainer xmlns="https://spacestation14.io" Orientation="Vertical">
+    <LineEdit Name="SearchBar" PlaceHolder="{Loc 'markings-search'}" HorizontalExpand="True" />
+    <ScrollContainer Name="SelectionItems" HorizontalExpand="True" VerticalExpand="True">
+        <GridContainer Name="Items" Columns="2" Margin="4" />
+    </ScrollContainer>
+    <ScrollContainer Name="OrderingItems" HorizontalExpand="True" VerticalExpand="True" Visible="False">
+    </ScrollContainer>
+    <BoxContainer HorizontalExpand="True" Margin="4">
+        <Label Name="MarkingsStatus" HorizontalExpand="True" />
+        <Button Name="ReorderButton" ToggleMode="True" Text="{Loc 'markings-reorder'}" />
+    </BoxContainer>
+</BoxContainer>
diff --git a/Content.Client/Humanoid/LayerMarkingPicker.xaml.cs b/Content.Client/Humanoid/LayerMarkingPicker.xaml.cs
new file mode 100644 (file)
index 0000000..fad6a98
--- /dev/null
@@ -0,0 +1,113 @@
+using System.Linq;
+using Content.Client.UserInterface.ControlExtensions;
+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.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Humanoid;
+
+[GenerateTypedNameReferences]
+public sealed partial class LayerMarkingPicker : BoxContainer
+{
+    private readonly IReadOnlyDictionary<string, MarkingPrototype> _allMarkings;
+    private readonly ProtoId<OrganCategoryPrototype> _organ;
+    private readonly HumanoidVisualLayers _layer;
+    private readonly MarkingsViewModel _markingsModel;
+    private List<ISearchableControl> _searchable = new();
+    private const int _columnWidth = 500;
+
+    public LayerMarkingPicker(MarkingsViewModel markingsModel, ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer, IReadOnlyDictionary<string, MarkingPrototype> allMarkings)
+    {
+        RobustXamlLoader.Load(this);
+
+        _markingsModel = markingsModel;
+        _allMarkings = allMarkings;
+        _organ = organ;
+        _layer = layer;
+
+        OrderingItems.AddChild(new LayerMarkingOrderer(markingsModel, organ, layer));
+
+        UpdateMarkings();
+
+        SearchBar.OnTextChanged += _ =>
+        {
+            foreach (var element in _searchable)
+            {
+                element.SetHiddenState(true, SearchBar.Text.Trim());
+            }
+        };
+
+        UpdateCount();
+
+        ReorderButton.OnPressed += ReorderButtonPressed;
+    }
+
+    protected override void EnteredTree()
+    {
+        base.EnteredTree();
+
+        _markingsModel.MarkingsReset += UpdateCount;
+        _markingsModel.MarkingsChanged += MarkingsChanged;
+    }
+
+    protected override void ExitedTree()
+    {
+        base.ExitedTree();
+
+        _markingsModel.MarkingsReset -= UpdateCount;
+        _markingsModel.MarkingsChanged -= MarkingsChanged;
+    }
+
+    private void MarkingsChanged(ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer)
+    {
+        if (_organ != organ ||  _layer != layer)
+            return;
+
+        UpdateCount();
+    }
+
+    private void UpdateMarkings()
+    {
+        foreach (var marking in _allMarkings.Values.OrderBy(marking => Loc.GetString($"marking-{marking.ID}")))
+        {
+            var item = new LayerMarkingItem(_markingsModel, _organ, _layer, marking, true);
+            Items.AddChild(item);
+        }
+        _searchable = Items.GetSearchableControls();
+    }
+
+    private void UpdateCount()
+    {
+        _markingsModel.GetMarkingCounts(_organ, _layer, out var isRequired, out var count, out var selected);
+        MarkingsStatus.Text = Loc.GetString("markings-limits", ("required", isRequired), ("count", count), ("selectable", count - selected));
+    }
+
+    private void ReorderButtonPressed(BaseButton.ButtonEventArgs args)
+    {
+        if (ReorderButton.Pressed)
+        {
+            SelectionItems.Visible = false;
+            SearchBar.Visible = false;
+            OrderingItems.Visible = true;
+        }
+        else
+        {
+            SelectionItems.Visible = true;
+            SearchBar.Visible = true;
+            OrderingItems.Visible = false;
+        }
+    }
+
+    protected override void Resized()
+    {
+        base.Resized();
+
+        Items.Columns = (int)(Width / _columnWidth);
+    }
+}
index 1928dd87162ec3d17d75791eb8f9c63dc593c8df..6da97ee168e1178c97a65ae8c203957b6b788d5e 100644 (file)
@@ -1,37 +1,3 @@
 <Control xmlns="https://spacestation14.io">
-    <!-- Primary container -->
-    <BoxContainer Orientation="Vertical" HorizontalExpand="True">
-        <!-- Marking lists -->
-        <BoxContainer Orientation="Horizontal" SeparationOverride="5" HorizontalExpand="True">
-            <!-- Unused markings -->
-            <BoxContainer Orientation="Vertical" HorizontalExpand="True">
-                <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
-                    <Label Text="{Loc 'markings-unused'}" HorizontalAlignment="Stretch" HorizontalExpand="True" />
-                    <Label Name="CMarkingPoints" Text="uwu" HorizontalAlignment="Right" />
-                </BoxContainer>
-
-                <OptionButton Name="CMarkingCategoryButton" StyleClasses="OpenLeft" />
-                <LineEdit Name="CMarkingSearch" PlaceHolder="{Loc 'markings-search'}" />
-
-                <ItemList Name="CMarkingsUnused" VerticalExpand="True" MinSize="300 250" />
-                <Button Name="CMarkingAdd" Text="{Loc 'markings-add'}" StyleClasses="OpenRight" />
-            </BoxContainer>
-
-            <!-- Used markings -->
-            <BoxContainer Orientation="Vertical" HorizontalExpand="True">
-                <Label Text="{Loc 'markings-used'}" />
-
-                <ItemList Name="CMarkingsUsed" VerticalExpand="True" MinSize="300 250" />
-
-                <BoxContainer Orientation="Horizontal">
-                    <Button Name="CMarkingRankUp" Text="{Loc 'markings-rank-up'}" StyleClasses="OpenBoth" HorizontalExpand="True" />
-                    <Button Name="CMarkingRankDown" Text="{Loc 'markings-rank-down'}" StyleClasses="OpenBoth" HorizontalExpand="True" />
-                </BoxContainer>
-                <Button Name="CMarkingRemove" Text="{Loc 'markings-remove'}" StyleClasses="OpenRight" />
-            </BoxContainer>
-        </BoxContainer>
-
-        <!-- Colors -->
-        <BoxContainer Name="CMarkingColors" Orientation="Vertical" Visible="False" />
-    </BoxContainer>
+    <TabContainer Name="OrganTabs" />
 </Control>
index 992a72b93049e511677d768fb9291c1a00c96781..8f73d42beac3ca08e4b223f94427a4fc62915a41 100644 (file)
 using System.Linq;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Markings;
-using Content.Shared.Humanoid.Prototypes;
 using Robust.Client.AutoGenerated;
-using Robust.Client.GameObjects;
 using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
-using Robust.Client.Utility;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
 
 namespace Content.Client.Humanoid;
 
 [GenerateTypedNameReferences]
 public sealed partial class MarkingPicker : Control
 {
-    [Dependency] private readonly MarkingManager _markingManager = default!;
-    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-    [Dependency] private readonly IEntityManager _entityManager = default!;
-
-    private readonly SpriteSystem _sprite;
-
-    public Action<MarkingSet>? OnMarkingAdded;
-    public Action<MarkingSet>? OnMarkingRemoved;
-    public Action<MarkingSet>? OnMarkingColorChange;
-    public Action<MarkingSet>? OnMarkingRankChange;
-
-    private List<Color> _currentMarkingColors = new();
-
-    private ItemList.Item? _selectedMarking;
-    private ItemList.Item? _selectedUnusedMarking;
-    private MarkingCategories _selectedMarkingCategory = MarkingCategories.Chest;
-
-    private MarkingSet _currentMarkings = new();
-
-    private List<MarkingCategories> _markingCategories = Enum.GetValues<MarkingCategories>().ToList();
-
-    private string _currentSpecies = SharedHumanoidAppearanceSystem.DefaultSpecies;
-    private Sex _currentSex = Sex.Unsexed;
-    public Color CurrentSkinColor = Color.White;
-    public Color CurrentEyeColor = Color.Black;
-    public Marking? HairMarking;
-    public Marking? FacialHairMarking;
-
-    private readonly HashSet<MarkingCategories> _ignoreCategories = new();
-
-    public string IgnoreCategories
-    {
-        get => string.Join(',',  _ignoreCategories);
-        set
-        {
-            _ignoreCategories.Clear();
-            var split = value.Split(',');
-            foreach (var category in split)
-            {
-                if (!Enum.TryParse(category, out MarkingCategories categoryParse))
-                {
-                    continue;
-                }
-
-                _ignoreCategories.Add(categoryParse);
-            }
-
-            SetupCategoryButtons();
-        }
-    }
-
-    public bool Forced { get; set; }
-
-    private bool _ignoreSpecies;
-
-    public bool IgnoreSpecies
-    {
-        get => _ignoreSpecies;
-        set
-        {
-            _ignoreSpecies = value;
-            Populate(CMarkingSearch.Text);
-        }
-    }
-
-    public void SetData(List<Marking> newMarkings, string species, Sex sex, Color skinColor, Color eyeColor)
-    {
-        var pointsProto = _prototypeManager
-            .Index<SpeciesPrototype>(species).MarkingPoints;
-        _currentMarkings = new(newMarkings, pointsProto, _markingManager);
-
-        if (!IgnoreSpecies)
-        {
-            _currentMarkings.EnsureSpecies(species, skinColor, _markingManager); // should be validated server-side but it can't hurt
-        }
-
-        _currentSpecies = species;
-        _currentSex = sex;
-        CurrentSkinColor = skinColor;
-        CurrentEyeColor = eyeColor;
-
-        Populate(CMarkingSearch.Text);
-        PopulateUsed();
-    }
-
-    public void SetData(MarkingSet set, string species, Sex sex, Color skinColor, Color eyeColor)
-    {
-        _currentMarkings = set;
-
-        if (!IgnoreSpecies)
-        {
-            _currentMarkings.EnsureSpecies(species, skinColor, _markingManager); // should be validated server-side but it can't hurt
-        }
-
-        _currentSpecies = species;
-        _currentSex = sex;
-        CurrentSkinColor = skinColor;
-        CurrentEyeColor = eyeColor;
-
-        Populate(CMarkingSearch.Text);
-        PopulateUsed();
-    }
-
-    public void SetSkinColor(Color color) => CurrentSkinColor = color;
-    public void SetEyeColor(Color color) => CurrentEyeColor = color;
+    private MarkingsViewModel? _markingsModel;
 
     public MarkingPicker()
     {
         RobustXamlLoader.Load(this);
         IoCManager.InjectDependencies(this);
 
-        _sprite = _entityManager.System<SpriteSystem>();
-
-        CMarkingCategoryButton.OnItemSelected +=  OnCategoryChange;
-        CMarkingsUnused.OnItemSelected += item =>
-            _selectedUnusedMarking = CMarkingsUnused[item.ItemIndex];
-
-        CMarkingAdd.OnPressed += _ =>
-            MarkingAdd();
-
-        CMarkingsUsed.OnItemSelected += OnUsedMarkingSelected;
-
-        CMarkingRemove.OnPressed += _ =>
-            MarkingRemove();
-
-        CMarkingRankUp.OnPressed += _ => SwapMarkingUp();
-        CMarkingRankDown.OnPressed += _ => SwapMarkingDown();
-
-        CMarkingSearch.OnTextChanged += args => Populate(args.Text);
-    }
-
-    private void SetupCategoryButtons()
-    {
-        CMarkingCategoryButton.Clear();
-
-        var validCategories = new List<MarkingCategories>();
-        for (var i = 0; i < _markingCategories.Count; i++)
-        {
-            var category = _markingCategories[i];
-            var markings = GetMarkings(category);
-            if (_ignoreCategories.Contains(category) ||
-                markings.Count == 0)
-            {
-                continue;
-            }
-
-            validCategories.Add(category);
-            CMarkingCategoryButton.AddItem(Loc.GetString($"markings-category-{category.ToString()}"), i);
-        }
-
-        if (validCategories.Contains(_selectedMarkingCategory))
-        {
-            CMarkingCategoryButton.SelectId(_markingCategories.IndexOf(_selectedMarkingCategory));
-        }
-        else if (validCategories.Count > 0)
-        {
-            _selectedMarkingCategory = validCategories[0];
-        }
-        else
-        {
-            _selectedMarkingCategory = MarkingCategories.Chest;
-        }
-    }
-
-    private string GetMarkingName(MarkingPrototype marking) => Loc.GetString($"marking-{marking.ID}");
-
-    private List<string> GetMarkingStateNames(MarkingPrototype marking)
-    {
-        List<string> result = new();
-        foreach (var markingState in marking.Sprites)
-        {
-            switch (markingState)
-            {
-                case SpriteSpecifier.Rsi rsi:
-                    result.Add(Loc.GetString($"marking-{marking.ID}-{rsi.RsiState}"));
-                    break;
-                case SpriteSpecifier.Texture texture:
-                    result.Add(Loc.GetString($"marking-{marking.ID}-{texture.TexturePath.Filename}"));
-                    break;
-            }
-        }
-
-        return result;
-    }
-
-    private IReadOnlyDictionary<string, MarkingPrototype> GetMarkings(MarkingCategories category)
-    {
-        return IgnoreSpecies
-            ? _markingManager.MarkingsByCategoryAndSex(category, _currentSex)
-            : _markingManager.MarkingsByCategoryAndSpeciesAndSex(category, _currentSpecies, _currentSex);
-    }
-
-    public void Populate(string filter)
-    {
-        SetupCategoryButtons();
-
-        CMarkingsUnused.Clear();
-        _selectedUnusedMarking = null;
-
-        var sortedMarkings = GetMarkings(_selectedMarkingCategory).Values.Where(m =>
-            m.ID.ToLower().Contains(filter.ToLower()) ||
-            GetMarkingName(m).ToLower().Contains(filter.ToLower())
-        ).OrderBy(p => Loc.GetString(GetMarkingName(p)));
-
-        foreach (var marking in sortedMarkings)
-        {
-            if (_currentMarkings.TryGetMarking(_selectedMarkingCategory, marking.ID, out _))
-            {
-                continue;
-            }
-
-            var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", _sprite.Frame0(marking.Sprites[0]));
-            item.Metadata = marking;
-        }
-
-        CMarkingPoints.Visible = _currentMarkings.PointsLeft(_selectedMarkingCategory) != -1;
-    }
-
-    // Populate the used marking list. Returns a list of markings that weren't
-    // valid to add to the marking list.
-    public void PopulateUsed()
-    {
-        CMarkingsUsed.Clear();
-        CMarkingColors.Visible = false;
-        _selectedMarking = null;
-
-        if (!IgnoreSpecies)
-        {
-            _currentMarkings.EnsureSpecies(_currentSpecies, null, _markingManager);
-        }
-
-        // walk backwards through the list for visual purposes
-        foreach (var marking in _currentMarkings.GetReverseEnumerator(_selectedMarkingCategory))
-        {
-            if (!_markingManager.TryGetMarking(marking, out var newMarking))
-            {
-                continue;
-            }
-
-            var text = Loc.GetString(marking.Forced ? "marking-used-forced" : "marking-used", ("marking-name", $"{GetMarkingName(newMarking)}"),
-                ("marking-category", Loc.GetString($"markings-category-{newMarking.MarkingCategory}")));
-
-            var _item = new ItemList.Item(CMarkingsUsed)
-            {
-                Text = text,
-                Icon = _sprite.Frame0(newMarking.Sprites[0]),
-                Selectable = true,
-                Metadata = newMarking,
-                IconModulate = marking.MarkingColors[0]
-            };
-
-            CMarkingsUsed.Add(_item);
-        }
-
-        // since all the points have been processed, update the points visually
-        UpdatePoints();
-    }
-
-    private void SwapMarkingUp()
-    {
-        if (_selectedMarking == null)
-        {
-            return;
-        }
-
-        var i = CMarkingsUsed.IndexOf(_selectedMarking);
-        if (ShiftMarkingRank(i, -1))
-        {
-            OnMarkingRankChange?.Invoke(_currentMarkings);
-        }
-    }
-
-    private void SwapMarkingDown()
-    {
-        if (_selectedMarking == null)
-        {
-            return;
-        }
-
-        var i = CMarkingsUsed.IndexOf(_selectedMarking);
-        if (ShiftMarkingRank(i, 1))
-        {
-            OnMarkingRankChange?.Invoke(_currentMarkings);
-        }
-    }
-
-    private bool ShiftMarkingRank(int src, int places)
-    {
-        if (src + places >= CMarkingsUsed.Count || src + places < 0)
-        {
-            return false;
-        }
-
-        var visualDest = src + places; // what it would visually look like
-        var visualTemp = CMarkingsUsed[visualDest];
-        CMarkingsUsed[visualDest] = CMarkingsUsed[src];
-        CMarkingsUsed[src] = visualTemp;
-
-        switch (places)
-        {
-            // i.e., we're going down in rank
-            case < 0:
-                _currentMarkings.ShiftRankDownFromEnd(_selectedMarkingCategory, src);
-                break;
-            // i.e., we're going up in rank
-            case > 0:
-                _currentMarkings.ShiftRankUpFromEnd(_selectedMarkingCategory, src);
-                break;
-            // do nothing?
-            // ReSharper disable once RedundantEmptySwitchSection
-            default:
-                break;
-        }
-
-        return true;
+        UpdateMarkings();
     }
 
-
-
-    // repopulate in case markings are restricted,
-    // and also filter out any markings that are now invalid
-    // attempt to preserve any existing markings as well:
-    // it would be frustrating to otherwise have all markings
-    // cleared, imo
-    public void SetSpecies(string species)
+    public void SetModel(MarkingsViewModel model)
     {
-        _currentSpecies = species;
-        var markingList = _currentMarkings.GetForwardEnumerator().ToList();
-
-        var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(species);
-
-        _currentMarkings = new(markingList, speciesPrototype.MarkingPoints, _markingManager, _prototypeManager);
-        _currentMarkings.EnsureSpecies(species, null, _markingManager);
-        _currentMarkings.EnsureSexes(_currentSex, _markingManager);
-
-        Populate(CMarkingSearch.Text);
-        PopulateUsed();
-    }
-
-    public void SetSex(Sex sex)
-    {
-        _currentSex = sex;
-        var markingList = _currentMarkings.GetForwardEnumerator().ToList();
-
-        var speciesPrototype = _prototypeManager.Index<SpeciesPrototype>(_currentSpecies);
-
-        _currentMarkings = new(markingList, speciesPrototype.MarkingPoints, _markingManager, _prototypeManager);
-        _currentMarkings.EnsureSpecies(_currentSpecies, null, _markingManager);
-        _currentMarkings.EnsureSexes(_currentSex, _markingManager);
-
-        Populate(CMarkingSearch.Text);
-        PopulateUsed();
-    }
-
-    private void UpdatePoints()
-    {
-        var count = _currentMarkings.PointsLeft(_selectedMarkingCategory);
-        if (count > -1)
-        {
-            CMarkingPoints.Text = Loc.GetString("marking-points-remaining", ("points", count));
-        }
-    }
+        _markingsModel = model;
 
-    private void OnCategoryChange(OptionButton.ItemSelectedEventArgs category)
-    {
-        CMarkingCategoryButton.SelectId(category.Id);
-        _selectedMarkingCategory = _markingCategories[category.Id];
-        Populate(CMarkingSearch.Text);
-        PopulateUsed();
-        UpdatePoints();
+        _markingsModel.OrganDataChanged += UpdateMarkings;
+        _markingsModel.EnforcementsChanged += UpdateMarkings;
     }
 
-    // TODO: This should be using ColorSelectorSliders once that's merged, so
-    private void OnUsedMarkingSelected(ItemList.ItemListSelectedEventArgs item)
+    protected override void EnteredTree()
     {
-        _selectedMarking = CMarkingsUsed[item.ItemIndex];
-        var prototype = (MarkingPrototype) _selectedMarking.Metadata!;
-
-        if (prototype.ForcedColoring)
-        {
-            CMarkingColors.Visible = false;
-
-            return;
-        }
-
-        var stateNames = GetMarkingStateNames(prototype);
-        _currentMarkingColors.Clear();
-        CMarkingColors.RemoveAllChildren();
-        List<ColorSelectorSliders> colorSliders = new();
-        for (int i = 0; i < prototype.Sprites.Count; i++)
-        {
-            var colorContainer = new BoxContainer
-            {
-                Orientation = LayoutOrientation.Vertical,
-            };
-
-            CMarkingColors.AddChild(colorContainer);
-
-            ColorSelectorSliders colorSelector = new ColorSelectorSliders();
-            colorSelector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; // defaults color selector to HSV
-            colorSliders.Add(colorSelector);
+        base.EnteredTree();
 
-            colorContainer.AddChild(new Label { Text = $"{stateNames[i]} color:" });
-            colorContainer.AddChild(colorSelector);
-
-            var listing = _currentMarkings.Markings[_selectedMarkingCategory];
-
-            var color = listing[listing.Count - 1 - item.ItemIndex].MarkingColors[i];
-            var currentColor = new Color(
-                color.RByte,
-                color.GByte,
-                color.BByte
-            );
-            colorSelector.Color = currentColor;
-            _currentMarkingColors.Add(currentColor);
-            var colorIndex = _currentMarkingColors.Count - 1;
-
-            Action<Color> colorChanged = _ =>
-            {
-                _currentMarkingColors[colorIndex] = colorSelector.Color;
-
-                ColorChanged(colorIndex);
-            };
-            colorSelector.OnColorChanged += colorChanged;
-        }
-
-        CMarkingColors.Visible = true;
+        _markingsModel?.OrganDataChanged += UpdateMarkings;
+        _markingsModel?.EnforcementsChanged += UpdateMarkings;
     }
 
-    private void ColorChanged(int colorIndex)
+    protected override void ExitedTree()
     {
-        if (_selectedMarking is null) return;
-        var markingPrototype = (MarkingPrototype) _selectedMarking.Metadata!;
-        int markingIndex = _currentMarkings.FindIndexOf(_selectedMarkingCategory, markingPrototype.ID);
+        base.ExitedTree();
 
-        if (markingIndex < 0) return;
-
-        _selectedMarking.IconModulate = _currentMarkingColors[colorIndex];
-
-        var marking = new Marking(_currentMarkings.Markings[_selectedMarkingCategory][markingIndex]);
-        marking.SetColor(colorIndex, _currentMarkingColors[colorIndex]);
-        _currentMarkings.Replace(_selectedMarkingCategory, markingIndex, marking);
-
-        OnMarkingColorChange?.Invoke(_currentMarkings);
+        _markingsModel?.OrganDataChanged -= UpdateMarkings;
+        _markingsModel?.EnforcementsChanged -= UpdateMarkings;
     }
 
-    private void MarkingAdd()
+    private void UpdateMarkings()
     {
-        if (_selectedUnusedMarking is null) return;
-
-        if (_currentMarkings.PointsLeft(_selectedMarkingCategory) == 0 && !Forced)
-        {
+        if (_markingsModel is null)
             return;
-        }
 
-        var marking = (MarkingPrototype) _selectedUnusedMarking.Metadata!;
-        var markingObject = marking.AsMarking();
+        OrganTabs.RemoveAllChildren();
 
-        // We need add hair markings in cloned set manually because _currentMarkings doesn't have it
-        var markingSet = new MarkingSet(_currentMarkings);
-        if (HairMarking != null)
-        {
-            markingSet.AddBack(MarkingCategories.Hair, HairMarking);
-        }
-        if (FacialHairMarking != null)
+        var i = 0;
+        foreach (var (organ, organData) in _markingsModel.OrganData)
         {
-            markingSet.AddBack(MarkingCategories.FacialHair, FacialHairMarking);
-        }
+            var control = new OrganMarkingPicker(_markingsModel, organ, organData.Layers, organData.Group);
+            if (control.Empty)
+                continue;
 
-        if (!_markingManager.MustMatchSkin(_currentSpecies, marking.BodyPart, out var _, _prototypeManager))
-        {
-            // Do default coloring
-            var colors = MarkingColoring.GetMarkingLayerColors(
-                marking,
-                CurrentSkinColor,
-                CurrentEyeColor,
-                markingSet
-            );
-            for (var i = 0; i < colors.Count; i++)
-            {
-                markingObject.SetColor(i, colors[i]);
-            }
+            OrganTabs.AddChild(control);
+            OrganTabs.SetTabTitle(i, Loc.GetString($"markings-organ-{organ.Id}"));
+            i++;
         }
-        else
-        {
-            // Color everything in skin color
-            for (var i = 0; i < marking.Sprites.Count; i++)
-            {
-                markingObject.SetColor(i, CurrentSkinColor);
-            }
-        }
-
-        markingObject.Forced = Forced;
 
-        _currentMarkings.AddBack(_selectedMarkingCategory, markingObject);
-
-        UpdatePoints();
-
-        CMarkingsUnused.Remove(_selectedUnusedMarking);
-        var item = new ItemList.Item(CMarkingsUsed)
-        {
-            Text = Loc.GetString("marking-used", ("marking-name", $"{GetMarkingName(marking)}"), ("marking-category", Loc.GetString($"markings-category-{marking.MarkingCategory}"))),
-            Icon = _sprite.Frame0(marking.Sprites[0]),
-            Selectable = true,
-            Metadata = marking,
-        };
-        CMarkingsUsed.Insert(0, item);
-
-        _selectedUnusedMarking = null;
-        OnMarkingAdded?.Invoke(_currentMarkings);
-    }
-
-    private void MarkingRemove()
-    {
-        if (_selectedMarking is null) return;
-
-        var marking = (MarkingPrototype) _selectedMarking.Metadata!;
-
-        _currentMarkings.Remove(_selectedMarkingCategory, marking.ID);
-
-        UpdatePoints();
-
-        CMarkingsUsed.Remove(_selectedMarking);
-
-        if (marking.MarkingCategory == _selectedMarkingCategory)
-        {
-            var item = CMarkingsUnused.AddItem($"{GetMarkingName(marking)}", _sprite.Frame0(marking.Sprites[0]));
-            item.Metadata = marking;
-        }
-        _selectedMarking = null;
-        CMarkingColors.Visible = false;
-        OnMarkingRemoved?.Invoke(_currentMarkings);
+        if (i > 0)
+            OrganTabs.CurrentTab = 0;
+        OrganTabs.TabsVisible = i > 1;
     }
 }
diff --git a/Content.Client/Humanoid/MarkingsViewModel.cs b/Content.Client/Humanoid/MarkingsViewModel.cs
new file mode 100644 (file)
index 0000000..8fe9208
--- /dev/null
@@ -0,0 +1,392 @@
+using System.Linq;
+using Content.Shared.Body;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Humanoid;
+
+public sealed class MarkingsViewModel
+{
+    [Dependency] private readonly MarkingManager _marking = default!;
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+    private bool _enforceLimits = true;
+
+    public bool EnforceLimits
+    {
+        get => _enforceLimits;
+        set
+        {
+            if (_enforceLimits == value)
+                return;
+
+            _enforceLimits = value;
+            EnforcementsChanged?.Invoke();
+        }
+    }
+
+    private bool _enforceGroupAndSexRestrictions = true;
+
+    public bool EnforceGroupAndSexRestrictions
+    {
+        get => _enforceGroupAndSexRestrictions;
+        set
+        {
+            if (_enforceGroupAndSexRestrictions == value)
+                return;
+
+            _enforceGroupAndSexRestrictions = value;
+            EnforcementsChanged?.Invoke();
+        }
+    }
+
+    private bool AnyEnforcementsLifted => !_enforceLimits || !_enforceGroupAndSexRestrictions;
+
+    public event Action? EnforcementsChanged;
+
+    private Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> _organProfileData = new();
+
+    public Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> OrganProfileData
+    {
+        get => _organProfileData;
+        set
+        {
+            _organProfileData = value.ShallowClone();
+            OrganProfileDataChanged?.Invoke();
+        }
+    }
+
+    public void SetOrganSexes(Sex sex)
+    {
+        foreach (var (organ, data) in _organProfileData)
+        {
+            _organProfileData[organ] = data with { Sex = sex };
+        }
+        OrganProfileDataChanged?.Invoke();
+    }
+
+    public void SetOrganSkinColor(Color skinColor)
+    {
+        foreach (var (organ, data) in _organProfileData)
+        {
+            _organProfileData[organ] = data with { SkinColor = skinColor };
+        }
+        OrganProfileDataChanged?.Invoke();
+    }
+
+    public void SetOrganEyeColor(Color eyeColor)
+    {
+        foreach (var (organ, data) in _organProfileData)
+        {
+            _organProfileData[organ] = data with { EyeColor = eyeColor };
+        }
+        OrganProfileDataChanged?.Invoke();
+    }
+
+    public event Action? OrganProfileDataChanged;
+
+    private Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> _markings = new();
+
+    public Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> Markings
+    {
+        get => _markings;
+        set
+        {
+            _markings = value.ToDictionary(
+                kvp => kvp.Key,
+                kvp => kvp.Value.ToDictionary(
+                    it => it.Key,
+                    it => it.Value.Select(marking => new Marking(marking)).ToList()));
+
+            MarkingsReset?.Invoke();
+        }
+    }
+
+    public event Action? MarkingsReset;
+
+    public event Action<ProtoId<OrganCategoryPrototype>, HumanoidVisualLayers>? MarkingsChanged;
+
+    private Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData> _organData = new();
+
+    public Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData>
+        OrganData
+    {
+        get => _organData;
+        set
+        {
+            if (_organData == value)
+                return;
+
+            _organData = value;
+            _previousColors.Clear();
+            OrganDataChanged?.Invoke();
+        }
+    }
+
+    public event Action? OrganDataChanged;
+
+    private Dictionary<ProtoId<MarkingPrototype>, List<Color>> _previousColors = new();
+
+    public MarkingsViewModel()
+    {
+        IoCManager.InjectDependencies(this);
+    }
+
+    public bool IsMarkingSelected(ProtoId<OrganCategoryPrototype> organ,
+        HumanoidVisualLayers layer,
+        ProtoId<MarkingPrototype> markingId)
+    {
+        return TryGetMarking(organ, layer, markingId) is not null;
+    }
+
+    public bool IsMarkingColorCustomizable(ProtoId<OrganCategoryPrototype> organ,
+        HumanoidVisualLayers layer,
+        ProtoId<MarkingPrototype> markingId)
+    {
+        if (!_prototype.TryIndex(markingId, out var markingProto))
+            return false;
+
+        if (markingProto.ForcedColoring)
+            return false;
+
+        if (!_organData.TryGetValue(organ, out var organData))
+            return false;
+
+        if (!_prototype.TryIndex(organData.Group, out var groupProto))
+            return false;
+
+        if (!groupProto.Appearances.TryGetValue(layer, out var appearance))
+            return true;
+
+        return !appearance.MatchSkin;
+    }
+
+    public Marking? TryGetMarking(ProtoId<OrganCategoryPrototype> organ,
+        HumanoidVisualLayers layer,
+        ProtoId<MarkingPrototype> markingId)
+    {
+        if (!_markings.TryGetValue(organ, out var markingSet))
+            return null;
+
+        if (!markingSet.TryGetValue(layer, out var markings))
+            return null;
+
+        return markings.FirstOrDefault(it => it.MarkingId == markingId);
+    }
+
+    public bool TrySelectMarking(ProtoId<OrganCategoryPrototype> organ,
+        HumanoidVisualLayers layer,
+        ProtoId<MarkingPrototype> markingId)
+    {
+        if (!_prototype.TryIndex(markingId, out var markingProto))
+            return false;
+
+        if (!_organData.TryGetValue(organ, out var organData) || !_organProfileData.TryGetValue(organ, out var profileData))
+            return false;
+
+        if (!organData.Layers.Contains(layer))
+            return false;
+
+        if (!_prototype.TryIndex(organData.Group, out var groupPrototype))
+            return false;
+
+        if (EnforceGroupAndSexRestrictions && !_marking.CanBeApplied(organData.Group, profileData.Sex, markingProto))
+            return false;
+
+        _markings[organ] = _markings.GetValueOrDefault(organ) ?? [];
+        var organMarkings = _markings[organ];
+        organMarkings[layer] = organMarkings.GetValueOrDefault(layer) ?? [];
+        var layerMarkings = organMarkings[layer];
+
+        var colors = _previousColors.GetValueOrDefault(markingId) ??
+                     MarkingColoring.GetMarkingLayerColors(markingProto, profileData.SkinColor, profileData.EyeColor, layerMarkings);
+        var newMarking = new Marking(markingId, colors);
+        newMarking.Forced = AnyEnforcementsLifted;
+
+        var limits = groupPrototype.Limits.GetValueOrDefault(layer);
+        if (limits is null || !EnforceLimits)
+        {
+            layerMarkings.Add(newMarking);
+            MarkingsChanged?.Invoke(organ, layer);
+            return true;
+        }
+
+        if (limits.Limit == 1 && layerMarkings.Count == 1)
+        {
+            layerMarkings.Clear();
+            layerMarkings.Add(newMarking);
+            MarkingsChanged?.Invoke(organ, layer);
+            return true;
+        }
+
+        if (layerMarkings.Count < limits.Limit)
+        {
+            layerMarkings.Add(newMarking);
+            MarkingsChanged?.Invoke(organ, layer);
+            return true;
+        }
+
+        return false;
+    }
+
+    public List<Marking>? SelectedMarkings(ProtoId<OrganCategoryPrototype> organ,
+        HumanoidVisualLayers layer)
+    {
+        if (!_markings.TryGetValue(organ, out var organMarkings))
+            return null;
+
+        if (!organMarkings.TryGetValue(layer, out var layerMarkings))
+            return null;
+
+        return layerMarkings;
+    }
+
+    public bool TryDeselectMarking(ProtoId<OrganCategoryPrototype> organ,
+        HumanoidVisualLayers layer,
+        ProtoId<MarkingPrototype> markingId)
+    {
+        if (!_organData.TryGetValue(organ, out var organData))
+            return false;
+
+        if (!organData.Layers.Contains(layer))
+            return false;
+
+        if (!_prototype.TryIndex(organData.Group, out var groupPrototype))
+            return false;
+
+        var limits = groupPrototype.Limits.GetValueOrDefault(layer);
+
+        _markings[organ] = _markings.GetValueOrDefault(organ) ?? [];
+        var organMarkings = _markings[organ];
+        organMarkings[layer] = organMarkings.GetValueOrDefault(layer) ?? [];
+        var layerMarkings = organMarkings[layer];
+
+        var count = layerMarkings.Count(marking => marking.MarkingId == markingId);
+        if (count == 0)
+            return false;
+
+        if (EnforceLimits && limits is not null && limits.Required && (layerMarkings.Count - count) <= 0)
+            return false;
+
+        if (layerMarkings.Find(marking => marking.MarkingId == markingId) is { } removingMarking)
+        {
+            _previousColors[removingMarking.MarkingId] = removingMarking.MarkingColors.ToList();
+        }
+        layerMarkings.RemoveAll(marking => marking.MarkingId == markingId);
+        MarkingsChanged?.Invoke(organ, layer);
+
+        return true;
+    }
+
+    public void TrySetMarkingColor(ProtoId<OrganCategoryPrototype> organ,
+        HumanoidVisualLayers layer,
+        ProtoId<MarkingPrototype> markingId,
+        int colorIndex,
+        Color color)
+    {
+        if (!_markings.TryGetValue(organ, out var markingSet))
+            return;
+
+        if (!markingSet.TryGetValue(layer, out var markings))
+            return;
+
+        if (markings.FirstOrDefault(it => it.MarkingId == markingId) is not { } marking)
+            return;
+
+        marking.SetColor(colorIndex, color);
+        MarkingsChanged?.Invoke(organ, layer);
+    }
+
+    public void ValidateMarkings()
+    {
+        foreach (var (organ, organData) in _organData)
+        {
+            if (!_organProfileData.TryGetValue(organ, out var organProfileData))
+            {
+                _markings.Remove(organ);
+                continue;
+            }
+
+            var actualMarkings = _markings.GetValueOrDefault(organ)?.ShallowClone() ?? [];
+
+            _marking.EnsureValidColors(actualMarkings);
+            _marking.EnsureValidGroupAndSex(actualMarkings, organData.Group, organProfileData.Sex);
+            _marking.EnsureValidLayers(actualMarkings, organData.Layers);
+            _marking.EnsureValidLimits(actualMarkings, organData.Group, organData.Layers, organProfileData.SkinColor, organProfileData.EyeColor);
+
+            _markings[organ] = actualMarkings;
+        }
+
+        MarkingsReset?.Invoke();
+    }
+
+    public void GetMarkingCounts(ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer, out bool isRequired, out int count, out int selected)
+    {
+        isRequired = false;
+        count = 0;
+        selected = 0;
+
+        if (!_organData.TryGetValue(organ, out var organData))
+            return;
+
+        if (!organData.Layers.Contains(layer))
+            return;
+
+        if (!_prototype.TryIndex(organData.Group, out var groupPrototype))
+            return;
+
+        if (!groupPrototype.Limits.TryGetValue(layer, out var limits))
+            return;
+
+        isRequired = limits.Required;
+        count = limits.Limit;
+
+        if (!_markings.TryGetValue(organ, out var organMarkings))
+            return;
+
+        if (!organMarkings.TryGetValue(layer, out var layerMarkings))
+            return;
+
+        selected = layerMarkings.Count;
+    }
+
+    public void ChangeMarkingOrder(ProtoId<OrganCategoryPrototype> organ,
+        HumanoidVisualLayers layer,
+        ProtoId<MarkingPrototype> markingId,
+        CandidatePosition position,
+        int positionIndex
+    )
+    {
+        if (!_markings.TryGetValue(organ, out var organMarkings))
+            return;
+
+        if (!organMarkings.TryGetValue(layer, out var layerMarkings))
+            return;
+
+        var currentIndex = layerMarkings.FindIndex(marking => marking.MarkingId == markingId);
+        var currentMarking = layerMarkings[currentIndex];
+
+        if (position == CandidatePosition.Before)
+        {
+            layerMarkings.RemoveAt(currentIndex);
+            var insertionIndex = currentIndex < positionIndex ? positionIndex - 1 : positionIndex;
+            layerMarkings.Insert(insertionIndex, currentMarking);
+        }
+        else if (position == CandidatePosition.After)
+        {
+            layerMarkings.RemoveAt(currentIndex);
+            var insertionIndex = currentIndex > positionIndex ? positionIndex + 1 : positionIndex;
+            layerMarkings.Insert(insertionIndex, currentMarking);
+        }
+
+        MarkingsChanged?.Invoke(organ, layer);
+    }
+}
+
+public enum CandidatePosition
+{
+    Before,
+    After,
+}
diff --git a/Content.Client/Humanoid/OrganMarkingPicker.xaml b/Content.Client/Humanoid/OrganMarkingPicker.xaml
new file mode 100644 (file)
index 0000000..c821ed5
--- /dev/null
@@ -0,0 +1,3 @@
+<Control xmlns="https://spacestation14.io">
+    <TabContainer Name="LayerTabs" />
+</Control>
diff --git a/Content.Client/Humanoid/OrganMarkingPicker.xaml.cs b/Content.Client/Humanoid/OrganMarkingPicker.xaml.cs
new file mode 100644 (file)
index 0000000..63ef739
--- /dev/null
@@ -0,0 +1,86 @@
+using System.Linq;
+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;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Humanoid;
+
+[GenerateTypedNameReferences]
+public sealed partial class OrganMarkingPicker : Control
+{
+    [Dependency] private readonly MarkingManager _marking = default!;
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+    [Dependency] private readonly IEntityManager _entity = default!;
+
+    private readonly SpriteSystem _sprite;
+
+    private readonly MarkingsViewModel _markingsModel;
+    private readonly HashSet<HumanoidVisualLayers> _layers;
+    private readonly ProtoId<MarkingsGroupPrototype> _group;
+    private readonly ProtoId<OrganCategoryPrototype> _organ;
+
+    public OrganMarkingPicker(MarkingsViewModel markingsModel, ProtoId<OrganCategoryPrototype> organ, HashSet<HumanoidVisualLayers> layers, ProtoId<MarkingsGroupPrototype> group)
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        _markingsModel = markingsModel;
+        _layers = layers;
+        _group = group;
+        _organ = organ;
+
+        _sprite = _entity.System<SpriteSystem>();
+
+        UpdateMarkings();
+    }
+
+    protected override void EnteredTree()
+    {
+        base.EnteredTree();
+
+        _markingsModel.OrganProfileDataChanged += UpdateMarkings;
+        _markingsModel.EnforcementsChanged += UpdateMarkings;
+    }
+
+    protected override void ExitedTree()
+    {
+        base.ExitedTree();
+
+        _markingsModel.OrganProfileDataChanged -= UpdateMarkings;
+        _markingsModel.EnforcementsChanged -= UpdateMarkings;
+    }
+
+    public bool Empty => LayerTabs.ChildCount == 0;
+
+    private void UpdateMarkings()
+    {
+        if (!_markingsModel.OrganProfileData.TryGetValue(_organ, out var organProfileData))
+            return;
+
+        LayerTabs.RemoveAllChildren();
+        var i = 0;
+        foreach (var layer in _layers)
+        {
+            var allMarkings =
+                _markingsModel.EnforceGroupAndSexRestrictions ? _marking.MarkingsByLayerAndGroupAndSex(layer, _group, organProfileData.Sex) : _marking.MarkingsByLayer(layer);
+
+            if (allMarkings.Count == 0)
+                continue;
+
+            var control = new LayerMarkingPicker(_markingsModel, _organ, layer, allMarkings);
+            LayerTabs.AddChild(control);
+            if (Loc.TryGetString($"markings-layer-{layer}-{_group.Id}", out var layerTitle))
+                LayerTabs.SetTabTitle(i, layerTitle);
+            else
+                LayerTabs.SetTabTitle(i, Loc.GetString($"markings-layer-{layer}"));
+            i++;
+        }
+
+        LayerTabs.TabsVisible = i > 1;
+    }
+}
diff --git a/Content.Client/Humanoid/SingleMarkingPicker.xaml b/Content.Client/Humanoid/SingleMarkingPicker.xaml
deleted file mode 100644 (file)
index c816c52..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-<BoxContainer xmlns="https://spacestation14.io"
-              Orientation="Vertical"
-              HorizontalExpand="True"
-              VerticalExpand="True">
-    <!-- "Slot" selection -->
-    <Label Name="CategoryName" />
-    <BoxContainer Name="SlotSelectorContainer" HorizontalExpand="True">
-        <OptionButton Name="SlotSelector" HorizontalExpand="True" StyleClasses="OpenBoth" />
-        <Button Name="AddButton" Text="{Loc 'marking-slot-add'}" StyleClasses="OpenBoth" />
-        <Button Name="RemoveButton" Text="{Loc 'marking-slot-remove'}" StyleClasses="OpenLeft" />
-    </BoxContainer>
-    <LineEdit Name="Search" PlaceHolder="{Loc 'markings-search'}" HorizontalExpand="True" />
-
-    <!-- Item list -->
-    <BoxContainer Name="MarkingSelectorContainer" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
-        <ScrollContainer MinHeight="500" VerticalExpand="True" HorizontalExpand="True">
-            <ItemList Name="MarkingList" VerticalExpand="True" />
-        </ScrollContainer>
-
-        <!-- Color sliders -->
-        <ScrollContainer MinHeight="200" HorizontalExpand="True">
-            <BoxContainer Name="ColorSelectorContainer" HorizontalExpand="True" />
-        </ScrollContainer>
-    </BoxContainer>
-</BoxContainer>
diff --git a/Content.Client/Humanoid/SingleMarkingPicker.xaml.cs b/Content.Client/Humanoid/SingleMarkingPicker.xaml.cs
deleted file mode 100644 (file)
index 7ff10d7..0000000
+++ /dev/null
@@ -1,304 +0,0 @@
-using System.Linq;
-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.Utility;
-
-namespace Content.Client.Humanoid;
-
-[GenerateTypedNameReferences]
-public sealed partial class SingleMarkingPicker : BoxContainer
-{
-    [Dependency] private readonly MarkingManager _markingManager = default!;
-    [Dependency] private readonly IEntityManager _entityManager = default!;
-
-    private readonly SpriteSystem _sprite;
-
-    /// <summary>
-    ///     What happens if a marking is selected.
-    ///     It will send the 'slot' (marking index)
-    ///     and the selected marking's ID.
-    /// </summary>
-    public Action<(int slot, string id)>? OnMarkingSelect;
-    /// <summary>
-    ///     What happens if a slot is removed.
-    ///     This will send the 'slot' (marking index).
-    /// </summary>
-    public Action<int>? OnSlotRemove;
-
-    /// <summary>
-    ///     What happens when a slot is added.
-    /// </summary>
-    public Action? OnSlotAdd;
-
-    /// <summary>
-    ///     What happens if a marking's color is changed.
-    ///     Sends a 'slot' number, and the marking in question.
-    /// </summary>
-    public Action<(int slot, Marking marking)>? OnColorChanged;
-
-    // current selected slot
-    private int _slot = -1;
-    private int Slot
-    {
-        get
-        {
-            if (_markings == null || _markings.Count == 0)
-            {
-                _slot = -1;
-            }
-            else if (_slot == -1)
-            {
-                _slot = 0;
-            }
-
-            return _slot;
-        }
-        set
-        {
-            if (_markings == null || _markings.Count == 0)
-            {
-                _slot = -1;
-                return;
-            }
-
-            _slot = value;
-            _ignoreItemSelected = true;
-
-            foreach (var item in MarkingList)
-            {
-                item.Selected = (string) item.Metadata! == _markings[_slot].MarkingId;
-            }
-
-            _ignoreItemSelected = false;
-            PopulateColors();
-        }
-    }
-
-    // amount of slots to show
-    private int _totalPoints;
-
-    private bool _ignoreItemSelected;
-
-    private MarkingCategories _category;
-    public MarkingCategories Category
-    {
-        get => _category;
-        set
-        {
-            _category = value;
-            CategoryName.Text = Loc.GetString($"markings-category-{_category}");
-
-            if (!string.IsNullOrEmpty(_species))
-            {
-                PopulateList(Search.Text);
-            }
-        }
-    }
-    private IReadOnlyDictionary<string, MarkingPrototype>? _markingPrototypeCache;
-
-    private string? _species;
-    private List<Marking>? _markings;
-
-    private int PointsLeft
-    {
-        get
-        {
-            if (_markings == null)
-            {
-                return 0;
-            }
-
-            if (_totalPoints < 0)
-            {
-                return -1;
-            }
-
-            return _totalPoints - _markings.Count;
-        }
-    }
-
-    private int PointsUsed => _markings?.Count ?? 0;
-
-    public SingleMarkingPicker()
-    {
-        RobustXamlLoader.Load(this);
-        IoCManager.InjectDependencies(this);
-
-        _sprite = _entityManager.System<SpriteSystem>();
-        MarkingList.OnItemSelected += SelectMarking;
-        AddButton.OnPressed += _ =>
-        {
-            OnSlotAdd!();
-        };
-
-        SlotSelector.OnItemSelected += args =>
-        {
-            Slot = args.Button.SelectedId;
-        };
-
-        RemoveButton.OnPressed += _ =>
-        {
-            OnSlotRemove!(_slot);
-        };
-
-        Search.OnTextChanged += args =>
-        {
-            PopulateList(args.Text);
-        };
-    }
-
-    public void UpdateData(List<Marking> markings, string species, int totalPoints)
-    {
-        _markings = markings;
-        _species = species;
-        _totalPoints = totalPoints;
-
-        _markingPrototypeCache = _markingManager.MarkingsByCategoryAndSpecies(Category, _species);
-
-        Visible = _markingPrototypeCache.Count != 0;
-        if (_markingPrototypeCache.Count == 0)
-        {
-            return;
-        }
-
-        PopulateList(Search.Text);
-        PopulateColors();
-        PopulateSlotSelector();
-    }
-
-    public void PopulateList(string filter)
-    {
-        if (string.IsNullOrEmpty(_species))
-        {
-            throw new ArgumentException("Tried to populate marking list without a set species!");
-        }
-
-        _markingPrototypeCache ??= _markingManager.MarkingsByCategoryAndSpecies(Category, _species);
-
-        MarkingSelectorContainer.Visible = _markings != null && _markings.Count != 0;
-        if (_markings == null || _markings.Count == 0)
-        {
-            return;
-        }
-
-        MarkingList.Clear();
-
-        var sortedMarkings = _markingPrototypeCache.Where(m =>
-            m.Key.ToLower().Contains(filter.ToLower()) ||
-            GetMarkingName(m.Value).ToLower().Contains(filter.ToLower())
-        ).OrderBy(p => Loc.GetString($"marking-{p.Key}"));
-
-        foreach (var (id, marking) in sortedMarkings)
-        {
-            var item = MarkingList.AddItem(Loc.GetString($"marking-{id}"), _sprite.Frame0(marking.Sprites[0]));
-            item.Metadata = marking.ID;
-
-            if (_markings[Slot].MarkingId == id)
-            {
-                _ignoreItemSelected = true;
-                item.Selected = true;
-                _ignoreItemSelected = false;
-            }
-        }
-    }
-
-    private void PopulateColors()
-    {
-        if (_markings == null
-            || _markings.Count == 0
-            || !_markingManager.TryGetMarking(_markings[Slot], out var proto))
-        {
-            return;
-        }
-
-        var marking = _markings[Slot];
-
-        ColorSelectorContainer.RemoveAllChildren();
-
-        if (marking.MarkingColors.Count != proto.Sprites.Count)
-        {
-            marking = new Marking(marking.MarkingId, proto.Sprites.Count);
-        }
-
-        for (var i = 0; i < marking.MarkingColors.Count; i++)
-        {
-            var selector = new ColorSelectorSliders
-            {
-                HorizontalExpand = true
-            };
-            selector.Color = marking.MarkingColors[i];
-            selector.SelectorType = ColorSelectorSliders.ColorSelectorType.Hsv; // defaults color selector to HSV
-
-            var colorIndex = i;
-            selector.OnColorChanged += color =>
-            {
-                marking.SetColor(colorIndex, color);
-                OnColorChanged!((_slot, marking));
-            };
-
-            ColorSelectorContainer.AddChild(selector);
-        }
-    }
-
-    private void SelectMarking(ItemList.ItemListSelectedEventArgs args)
-    {
-        if (_ignoreItemSelected)
-        {
-            return;
-        }
-
-        var id = (string) MarkingList[args.ItemIndex].Metadata!;
-        if (!_markingManager.Markings.TryGetValue(id, out var proto))
-        {
-            throw new ArgumentException("Attempted to select non-existent marking.");
-        }
-
-        var oldMarking = _markings![Slot];
-        _markings[Slot] = proto.AsMarking();
-
-        for (var i = 0; i < _markings[Slot].MarkingColors.Count && i < oldMarking.MarkingColors.Count; i++)
-        {
-            _markings[Slot].SetColor(i, oldMarking.MarkingColors[i]);
-        }
-
-        PopulateColors();
-
-        OnMarkingSelect!((_slot, id));
-    }
-
-    // Slot logic
-
-    private void PopulateSlotSelector()
-    {
-        SlotSelector.Visible = Slot >= 0;
-        Search.Visible = Slot >= 0;
-        AddButton.HorizontalExpand = Slot < 0;
-        RemoveButton.HorizontalExpand = Slot < 0;
-        AddButton.Disabled = PointsLeft == 0 && _totalPoints > -1 ;
-        RemoveButton.Disabled = PointsUsed == 0;
-        SlotSelector.Clear();
-
-        if (Slot < 0)
-        {
-            return;
-        }
-
-        for (var i = 0; i < PointsUsed; i++)
-        {
-            SlotSelector.AddItem(Loc.GetString("marking-slot", ("number", $"{i + 1}")), i);
-
-            if (i == _slot)
-            {
-                SlotSelector.SelectId(i);
-            }
-        }
-    }
-
-    private string GetMarkingName(MarkingPrototype marking)
-    {
-        return Loc.GetString($"marking-{marking.ID}");
-    }
-}
index e453dfd74bff366a6553cab17add4189e7e11f31..aace5c8cf9be7b5bceb769d272b3b786594ff193 100644 (file)
@@ -70,7 +70,6 @@ public sealed class DragDropHelper<T>
         _onBeginDrag = onBeginDrag;
         _onEndDrag = onEndDrag;
         _onContinueDrag = onContinueDrag;
-        _cfg.OnValueChanged(CCVars.DragDropDeadZone, SetDeadZone, true);
     }
 
     /// <summary>
@@ -90,6 +89,7 @@ public sealed class DragDropHelper<T>
         Dragged = target;
         _state = DragState.MouseDown;
         _mouseDownScreenPos = _inputManager.MouseScreenPosition;
+        _deadzone = _cfg.GetCVar(CCVars.DragDropDeadZone);
     }
 
     /// <summary>
@@ -97,9 +97,9 @@ public sealed class DragDropHelper<T>
     /// </summary>
     public void EndDrag()
     {
-        Dragged = default;
         _state = DragState.NotDragging;
         _onEndDrag.Invoke();
+        Dragged = default;
     }
 
     private void StartDragging()
@@ -143,11 +143,6 @@ public sealed class DragDropHelper<T>
             }
         }
     }
-
-    private void SetDeadZone(float value)
-    {
-        _deadzone = value;
-    }
 }
 
 /// <summary>
index e36a2cd174bd0170344c2f45f44d627e76566477..b76652f050894685a51a36945d4dd2aad912c20e 100644 (file)
@@ -1,6 +1,6 @@
 using System.Linq;
+using Content.Client.Body;
 using Content.Client.Guidebook;
-using Content.Client.Humanoid;
 using Content.Client.Inventory;
 using Content.Client.Lobby.UI;
 using Content.Client.Players.PlayTimeTracking;
@@ -8,7 +8,6 @@ using Content.Client.Station;
 using Content.Shared.CCVar;
 using Content.Shared.Clothing;
 using Content.Shared.GameTicking;
-using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Preferences;
@@ -38,7 +37,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
     [Dependency] private readonly IStateManager _stateManager = default!;
     [Dependency] private readonly JobRequirementsManager _requirements = default!;
     [Dependency] private readonly MarkingManager _markings = default!;
-    [UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
+    [UISystemDependency] private readonly VisualBodySystem _visualBody = default!;
     [UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
     [UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
     [UISystemDependency] private readonly GuidebookSystem _guide = default!;
@@ -470,16 +469,15 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
         }
         else if (humanoid is not null)
         {
-            var dummy = _prototypeManager.Index<SpeciesPrototype>(humanoid.Species).DollPrototype;
+            var dummy = _prototypeManager.Index(humanoid.Species).DollPrototype;
             dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
+            _visualBody.ApplyProfileTo(dummyEnt, humanoid);
         }
         else
         {
-            dummyEnt = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
+            dummyEnt = EntityManager.SpawnEntity(_prototypeManager.Index(HumanoidCharacterProfile.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
         }
 
-        _humanoid.LoadProfile(dummyEnt, humanoid);
-
         if (humanoid != null && jobClothes)
         {
             DebugTools.Assert(job != null);
index 7efd1c594fb5caeb3e1709a861aeb475027a9574..c96090e8d52b1f6246a322cd49b61b4df5e081fa 100644 (file)
@@ -45,7 +45,7 @@ public sealed partial class CharacterPickerButton : ContainerButton
 
         if (profile is not HumanoidCharacterProfile humanoid)
         {
-            _previewDummy = entityManager.SpawnEntity(prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
+            _previewDummy = entityManager.SpawnEntity(prototypeManager.Index<SpeciesPrototype>(HumanoidCharacterProfile.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
         }
         else
         {
index 703b64bce3e185b65d9aaace5857e7bcbc53bf1f..6c04a8070ecc83938eee43fd937d6031e6d76c91 100644 (file)
                                 <Slider HorizontalExpand="True" Name="Skin" MinValue="0" MaxValue="100" Value="20" />
                                 <BoxContainer Name="RgbSkinColorContainer" Visible="False" Orientation="Vertical" HorizontalExpand="True"></BoxContainer>
                             </BoxContainer>
-                            <!-- Hair -->
-                            <BoxContainer Margin="10" Orientation="Horizontal">
-                                <humanoid:SingleMarkingPicker Name="HairStylePicker" Category="Hair" />
-                                <humanoid:SingleMarkingPicker Name="FacialHairPicker" Category="FacialHair" />
-                            </BoxContainer>
                             <!-- Eyes -->
                             <BoxContainer Margin="10" Orientation="Vertical">
                                 <Label Text="{Loc 'humanoid-profile-editor-eyes-label'}" />
                 </BoxContainer>
                 <BoxContainer Name="MarkingsTab" Orientation="Vertical" Margin="10">
                     <!-- Markings -->
-                    <ScrollContainer VerticalExpand="True">
-                        <humanoid:MarkingPicker Name="Markings" IgnoreCategories="Hair,FacialHair" />
-                    </ScrollContainer>
+                    <humanoid:MarkingPicker Name="Markings" HorizontalExpand="True" VerticalExpand="True" />
                 </BoxContainer>
             </TabContainer>
         </BoxContainer>
index e9b1f41a4164ed11a31b8c7825be77f62d4c73b0..a7c67d78407a0745b22726ad84b163d9a826d8a1 100644 (file)
@@ -9,6 +9,7 @@ using Content.Client.Players.PlayTimeTracking;
 using Content.Client.Stylesheets;
 using Content.Client.Sprite;
 using Content.Client.UserInterface.Systems.Guidebook;
+using Content.Shared.Body;
 using Content.Shared.CCVar;
 using Content.Shared.Clothing;
 using Content.Shared.GameTicking;
@@ -109,6 +110,8 @@ namespace Content.Client.Lobby.UI
 
         private ISawmill _sawmill;
 
+        private MarkingsViewModel _markingsModel = new();
+
         public HumanoidProfileEditor(
             IClientPreferencesManager preferencesManager,
             IConfigurationManager configurationManager,
@@ -138,6 +141,8 @@ namespace Content.Client.Lobby.UI
             _maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
             _allowFlavorText = _cfgManager.GetCVar(CCVars.FlavorText);
 
+            Markings.SetModel(_markingsModel);
+
             ImportButton.OnPressed += args =>
             {
                 ImportProfile();
@@ -227,7 +232,6 @@ namespace Content.Client.Lobby.UI
             {
                 SpeciesButton.SelectId(args.Id);
                 SetSpecies(_species[args.Id].ID);
-                UpdateHairPickers();
                 OnSkinColorOnValueChanged();
             };
 
@@ -247,112 +251,6 @@ namespace Content.Client.Lobby.UI
 
             #endregion
 
-            #region Hair
-
-            HairStylePicker.OnMarkingSelect += newStyle =>
-            {
-                if (Profile is null)
-                    return;
-                Profile = Profile.WithCharacterAppearance(
-                    Profile.Appearance.WithHairStyleName(newStyle.id));
-                ReloadPreview();
-            };
-
-            HairStylePicker.OnColorChanged += newColor =>
-            {
-                if (Profile is null)
-                    return;
-                Profile = Profile.WithCharacterAppearance(
-                    Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
-                UpdateCMarkingsHair();
-                ReloadPreview();
-            };
-
-            FacialHairPicker.OnMarkingSelect += newStyle =>
-            {
-                if (Profile is null)
-                    return;
-                Profile = Profile.WithCharacterAppearance(
-                    Profile.Appearance.WithFacialHairStyleName(newStyle.id));
-                ReloadPreview();
-            };
-
-            FacialHairPicker.OnColorChanged += newColor =>
-            {
-                if (Profile is null)
-                    return;
-                Profile = Profile.WithCharacterAppearance(
-                    Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
-                UpdateCMarkingsFacialHair();
-                ReloadPreview();
-            };
-
-            HairStylePicker.OnSlotRemove += _ =>
-            {
-                if (Profile is null)
-                    return;
-                Profile = Profile.WithCharacterAppearance(
-                    Profile.Appearance.WithHairStyleName(HairStyles.DefaultHairStyle)
-                );
-                UpdateHairPickers();
-                UpdateCMarkingsHair();
-                ReloadPreview();
-            };
-
-            FacialHairPicker.OnSlotRemove += _ =>
-            {
-                if (Profile is null)
-                    return;
-                Profile = Profile.WithCharacterAppearance(
-                    Profile.Appearance.WithFacialHairStyleName(HairStyles.DefaultFacialHairStyle)
-                );
-                UpdateHairPickers();
-                UpdateCMarkingsFacialHair();
-                ReloadPreview();
-            };
-
-            HairStylePicker.OnSlotAdd += delegate()
-            {
-                if (Profile is null)
-                    return;
-
-                var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.Hair, Profile.Species).Keys
-                    .FirstOrDefault();
-
-                if (string.IsNullOrEmpty(hair))
-                    return;
-
-                Profile = Profile.WithCharacterAppearance(
-                    Profile.Appearance.WithHairStyleName(hair)
-                );
-
-                UpdateHairPickers();
-                UpdateCMarkingsHair();
-                ReloadPreview();
-            };
-
-            FacialHairPicker.OnSlotAdd += delegate()
-            {
-                if (Profile is null)
-                    return;
-
-                var hair = _markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.FacialHair, Profile.Species).Keys
-                    .FirstOrDefault();
-
-                if (string.IsNullOrEmpty(hair))
-                    return;
-
-                Profile = Profile.WithCharacterAppearance(
-                    Profile.Appearance.WithFacialHairStyleName(hair)
-                );
-
-                UpdateHairPickers();
-                UpdateCMarkingsFacialHair();
-                ReloadPreview();
-            };
-
-            #endregion Hair
-
             #region SpawnPriority
 
             foreach (var value in Enum.GetValues<SpawnPriorityPreference>())
@@ -376,7 +274,7 @@ namespace Content.Client.Lobby.UI
                     return;
                 Profile = Profile.WithCharacterAppearance(
                     Profile.Appearance.WithEyeColor(newColor));
-                Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
+                _markingsModel.SetOrganEyeColor(Profile.Appearance.EyeColor);
                 ReloadProfilePreview();
             };
 
@@ -418,10 +316,8 @@ namespace Content.Client.Lobby.UI
 
             TabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-markings-tab"));
 
-            Markings.OnMarkingAdded += OnMarkingChange;
-            Markings.OnMarkingRemoved += OnMarkingChange;
-            Markings.OnMarkingColorChange += OnMarkingChange;
-            Markings.OnMarkingRankChange += OnMarkingChange;
+            _markingsModel.MarkingsChanged += (_, _) => OnMarkingChange();
+            _markingsModel.MarkingsReset += OnMarkingChange;
 
             #endregion Markings
 
@@ -626,7 +522,7 @@ namespace Content.Client.Lobby.UI
             {
                 if (!speciesIds.Contains(Profile.Species))
                 {
-                    SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies);
+                    SetSpecies(HumanoidCharacterProfile.DefaultSpecies);
                 }
             }
         }
@@ -768,9 +664,6 @@ namespace Content.Client.Lobby.UI
             UpdateEyePickers();
             UpdateSaveButton();
             UpdateMarkings();
-            UpdateHairPickers();
-            UpdateCMarkingsHair();
-            UpdateCMarkingsFacialHair();
 
             RefreshAntags();
             RefreshJobs();
@@ -795,7 +688,7 @@ namespace Content.Client.Lobby.UI
             if (Profile == null || !_entManager.EntityExists(PreviewDummy))
                 return;
 
-            _entManager.System<HumanoidAppearanceSystem>().LoadProfile(PreviewDummy, Profile);
+            _entManager.System<SharedVisualBodySystem>().ApplyProfileTo(PreviewDummy, Profile);
 
             // Check and set the dirty flag to enable the save/reset buttons as appropriate.
             SetDirty();
@@ -808,7 +701,7 @@ namespace Content.Client.Lobby.UI
             // I.e., do what jobs/antags do.
 
             var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
-            var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
+            var species = Profile?.Species ?? HumanoidCharacterProfile.DefaultSpecies;
             var page = DefaultSpeciesGuidebook;
             if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
                 page = new ProtoId<GuideEntryPrototype>(species.Id); // Gross. See above todo comment.
@@ -1077,13 +970,14 @@ namespace Content.Client.Lobby.UI
             SetDirty();
         }
 
-        private void OnMarkingChange(MarkingSet markings)
+        private void OnMarkingChange()
         {
             if (Profile is null)
                 return;
 
-            Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
+            Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(_markingsModel.Markings));
             ReloadProfilePreview();
+            SetDirty();
         }
 
         private void OnSkinColorOnValueChanged()
@@ -1105,7 +999,7 @@ namespace Content.Client.Lobby.UI
 
                     var color = strategy.FromUnary(Skin.Value);
 
-                    Markings.CurrentSkinColor = color;
+                    _markingsModel.SetOrganSkinColor(color);
                     Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
 
                     break;
@@ -1120,7 +1014,7 @@ namespace Content.Client.Lobby.UI
 
                     var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color);
 
-                    Markings.CurrentSkinColor = color;
+                    _markingsModel.SetOrganSkinColor(color);
                     Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
 
                     break;
@@ -1177,7 +1071,7 @@ namespace Content.Client.Lobby.UI
             }
 
             UpdateGenderControls();
-            Markings.SetSex(newSex);
+            _markingsModel.SetOrganSexes(newSex);
             ReloadPreview();
         }
 
@@ -1191,7 +1085,8 @@ namespace Content.Client.Lobby.UI
         {
             Profile = Profile?.WithSpecies(newSpecies);
             OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
-            Markings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
+            _markingsModel.OrganData = _markingManager.GetMarkingData(newSpecies);
+            _markingsModel.ValidateMarkings();
             // In case there's job restrictions for the species
             RefreshJobs();
             // In case there's species restrictions for loadouts
@@ -1358,9 +1253,9 @@ namespace Content.Client.Lobby.UI
                 return;
             }
 
-            Markings.SetData(Profile.Appearance.Markings, Profile.Species,
-                Profile.Sex, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor
-            );
+            _markingsModel.OrganData = _markingManager.GetMarkingData(Profile.Species);
+            _markingsModel.OrganProfileData = _markingManager.GetProfileData(Profile.Species, Profile.Sex, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor);
+            _markingsModel.Markings = Profile.Appearance.Markings;
         }
 
         private void UpdateGenderControls()
@@ -1383,99 +1278,6 @@ namespace Content.Client.Lobby.UI
             SpawnPriorityButton.SelectId((int) Profile.SpawnPriority);
         }
 
-        private void UpdateHairPickers()
-        {
-            if (Profile == null)
-            {
-                return;
-            }
-            var hairMarking = Profile.Appearance.HairStyleId == HairStyles.DefaultHairStyle
-                ? new List<Marking>()
-                : new() { new(Profile.Appearance.HairStyleId, new List<Color>() { Profile.Appearance.HairColor }) };
-
-            var facialHairMarking = Profile.Appearance.FacialHairStyleId == HairStyles.DefaultFacialHairStyle
-                ? new List<Marking>()
-                : new() { new(Profile.Appearance.FacialHairStyleId, new List<Color>() { Profile.Appearance.FacialHairColor }) };
-
-            HairStylePicker.UpdateData(
-                hairMarking,
-                Profile.Species,
-                1);
-            FacialHairPicker.UpdateData(
-                facialHairMarking,
-                Profile.Species,
-                1);
-        }
-
-        private void UpdateCMarkingsHair()
-        {
-            if (Profile == null)
-            {
-                return;
-            }
-
-            // hair color
-            Color? hairColor = null;
-            if ( Profile.Appearance.HairStyleId != HairStyles.DefaultHairStyle &&
-                _markingManager.Markings.TryGetValue(Profile.Appearance.HairStyleId, out var hairProto)
-            )
-            {
-                if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, hairProto, _prototypeManager))
-                {
-                    if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager))
-                    {
-                        hairColor = Profile.Appearance.SkinColor;
-                    }
-                    else
-                    {
-                        hairColor = Profile.Appearance.HairColor;
-                    }
-                }
-            }
-            if (hairColor != null)
-            {
-                Markings.HairMarking = new (Profile.Appearance.HairStyleId, new List<Color>() { hairColor.Value });
-            }
-            else
-            {
-                Markings.HairMarking = null;
-            }
-        }
-
-        private void UpdateCMarkingsFacialHair()
-        {
-            if (Profile == null)
-            {
-                return;
-            }
-
-            // facial hair color
-            Color? facialHairColor = null;
-            if ( Profile.Appearance.FacialHairStyleId != HairStyles.DefaultFacialHairStyle &&
-                _markingManager.Markings.TryGetValue(Profile.Appearance.FacialHairStyleId, out var facialHairProto))
-            {
-                if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, facialHairProto, _prototypeManager))
-                {
-                    if (_markingManager.MustMatchSkin(Profile.Species, HumanoidVisualLayers.Hair, out var _, _prototypeManager))
-                    {
-                        facialHairColor = Profile.Appearance.SkinColor;
-                    }
-                    else
-                    {
-                        facialHairColor = Profile.Appearance.FacialHairColor;
-                    }
-                }
-            }
-            if (facialHairColor != null)
-            {
-                Markings.FacialHairMarking = new (Profile.Appearance.FacialHairStyleId, new List<Color>() { facialHairColor.Value });
-            }
-            else
-            {
-                Markings.FacialHairMarking = null;
-            }
-        }
-
         private void UpdateEyePickers()
         {
             if (Profile == null)
@@ -1483,7 +1285,7 @@ namespace Content.Client.Lobby.UI
                 return;
             }
 
-            Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
+            _markingsModel.SetOrganEyeColor(Profile.Appearance.EyeColor);
             EyeColorPicker.SetData(Profile.Appearance.EyeColor);
         }
 
@@ -1542,7 +1344,7 @@ namespace Content.Client.Lobby.UI
 
             try
             {
-                var profile = _entManager.System<HumanoidAppearanceSystem>().FromStream(file, _playerManager.LocalSession!);
+                var profile = HumanoidCharacterProfile.FromStream(file, _playerManager.LocalSession!);
                 var oldProfile = Profile;
                 SetProfile(profile, CharacterSlot);
 
@@ -1574,7 +1376,7 @@ namespace Content.Client.Lobby.UI
 
             try
             {
-                var dataNode = _entManager.System<HumanoidAppearanceSystem>().ToDataNode(Profile);
+                var dataNode = Profile.ToDataNode();
                 await using var writer = new StreamWriter(file.Value.fileStream);
                 dataNode.Write(writer);
             }
index 0a87948ff62fd411fe86bdd9acf1eea950ac3b87..ec247da1acba208b175d468247357538d88a2d32 100644 (file)
@@ -1,6 +1,5 @@
-using Content.Shared.Humanoid.Markings;
+using Content.Client.Humanoid;
 using Content.Shared.MagicMirror;
-using Robust.Client.GameObjects;
 using Robust.Client.UserInterface;
 
 namespace Content.Client.MagicMirror;
@@ -10,6 +9,8 @@ public sealed class MagicMirrorBoundUserInterface : BoundUserInterface
     [ViewVariables]
     private MagicMirrorWindow? _window;
 
+    private readonly MarkingsViewModel _markingsModel = new();
+
     public MagicMirrorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
     {
     }
@@ -19,49 +20,24 @@ public sealed class MagicMirrorBoundUserInterface : BoundUserInterface
         base.Open();
 
         _window = this.CreateWindow<MagicMirrorWindow>();
+        _window.MarkingsPicker.SetModel(_markingsModel);
 
-        _window.OnHairSelected += tuple => SelectHair(MagicMirrorCategory.Hair, tuple.id, tuple.slot);
-        _window.OnHairColorChanged += args => ChangeColor(MagicMirrorCategory.Hair, args.marking, args.slot);
-        _window.OnHairSlotAdded += delegate () { AddSlot(MagicMirrorCategory.Hair); };
-        _window.OnHairSlotRemoved += args => RemoveSlot(MagicMirrorCategory.Hair, args);
-
-        _window.OnFacialHairSelected += tuple => SelectHair(MagicMirrorCategory.FacialHair, tuple.id, tuple.slot);
-        _window.OnFacialHairColorChanged +=
-            args => ChangeColor(MagicMirrorCategory.FacialHair, args.marking, args.slot);
-        _window.OnFacialHairSlotAdded += delegate () { AddSlot(MagicMirrorCategory.FacialHair); };
-        _window.OnFacialHairSlotRemoved += args => RemoveSlot(MagicMirrorCategory.FacialHair, args);
-    }
-
-    private void SelectHair(MagicMirrorCategory category, string marking, int slot)
-    {
-        SendMessage(new MagicMirrorSelectMessage(category, marking, slot));
-    }
-
-    private void ChangeColor(MagicMirrorCategory category, Marking marking, int slot)
-    {
-        SendMessage(new MagicMirrorChangeColorMessage(category, new(marking.MarkingColors), slot));
-    }
-
-    private void RemoveSlot(MagicMirrorCategory category, int slot)
-    {
-        SendMessage(new MagicMirrorRemoveSlotMessage(category, slot));
-    }
-
-    private void AddSlot(MagicMirrorCategory category)
-    {
-        SendMessage(new MagicMirrorAddSlotMessage(category));
+        _markingsModel.MarkingsChanged += (_, _) =>
+        {
+            SendMessage(new MagicMirrorSelectMessage(_markingsModel.Markings));
+        };
     }
 
     protected override void UpdateState(BoundUserInterfaceState state)
     {
         base.UpdateState(state);
 
-        if (state is not MagicMirrorUiState data || _window == null)
-        {
+        if (state is not MagicMirrorUiState data)
             return;
-        }
 
-        _window.UpdateState(data);
+        _markingsModel.OrganData = data.OrganMarkingData;
+        _markingsModel.OrganProfileData = data.OrganProfileData;
+        _markingsModel.Markings = data.AppliedMarkings;
     }
 }
 
diff --git a/Content.Client/MagicMirror/MagicMirrorSystem.cs b/Content.Client/MagicMirror/MagicMirrorSystem.cs
deleted file mode 100644 (file)
index 9b0b1de..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-using Content.Shared.MagicMirror;
-
-namespace Content.Client.MagicMirror;
-
-public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
-{
-
-}
index b4c11d9c9b8b837ddbe8ccebbb1a1243a02c40b0..a09b6745085a0d14b5289fda502b5c9309195d17 100644 (file)
@@ -1,9 +1,6 @@
 <DefaultWindow xmlns="https://spacestation14.io"
                xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
                Title="{Loc 'magic-mirror-window-title'}"
-               MinSize="600 400">
-    <BoxContainer>
-        <humanoid:SingleMarkingPicker Name="HairPicker" Category="Hair" />
-        <humanoid:SingleMarkingPicker Name="FacialHairPicker" Category="FacialHair" />
-    </BoxContainer>
+               MinSize="700 500">
+   <humanoid:MarkingPicker Name="MarkingsPicker" Access="Public" HorizontalExpand="True" VerticalExpand="True" />
 </DefaultWindow>
index e7f5ec343fd02f54c85f131eb303d890fc51a385..7b9d947860a97f20f8c36b32c8c178d366c6b330 100644 (file)
@@ -1,7 +1,4 @@
-using Content.Shared.Humanoid.Markings;
-using Content.Shared.MagicMirror;
 using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.CustomControls;
 using Robust.Client.UserInterface.XAML;
 
@@ -10,40 +7,8 @@ namespace Content.Client.MagicMirror;
 [GenerateTypedNameReferences]
 public sealed partial class MagicMirrorWindow : DefaultWindow
 {
-    // MMMMMMM
-    public Action<(int slot, string id)>? OnHairSelected;
-    public Action<(int slot, Marking marking)>? OnHairColorChanged;
-    public Action<int>? OnHairSlotRemoved;
-    public Action? OnHairSlotAdded;
-
-    public Action<(int slot, string id)>? OnFacialHairSelected;
-    public Action<(int slot, Marking marking)>? OnFacialHairColorChanged;
-    public Action<int>? OnFacialHairSlotRemoved;
-    public Action? OnFacialHairSlotAdded;
-
     public MagicMirrorWindow()
     {
         RobustXamlLoader.Load(this);
-
-        HairPicker.OnMarkingSelect += args => OnHairSelected!(args);
-        HairPicker.OnColorChanged += args => OnHairColorChanged!(args);
-        HairPicker.OnSlotRemove += args => OnHairSlotRemoved!(args);
-        HairPicker.OnSlotAdd += delegate { OnHairSlotAdded!(); };
-
-        FacialHairPicker.OnMarkingSelect += args => OnFacialHairSelected!(args);
-        FacialHairPicker.OnColorChanged += args => OnFacialHairColorChanged!(args);
-        FacialHairPicker.OnSlotRemove += args => OnFacialHairSlotRemoved!(args);
-        FacialHairPicker.OnSlotAdd += delegate { OnFacialHairSlotAdded!(); };
-    }
-
-    public void UpdateState(MagicMirrorUiState state)
-    {
-        HairPicker.UpdateData(state.Hair, state.Species, state.HairSlotTotal);
-        FacialHairPicker.UpdateData(state.FacialHair, state.Species, state.FacialHairSlotTotal);
-
-        if (!HairPicker.Visible && !FacialHairPicker.Visible)
-        {
-            AddChild(new Label { Text = Loc.GetString("magic-mirror-component-activate-user-has-no-hair") });
-        }
     }
 }
index d4d2114054c55624e1e13fadf0274a61d5a43769..0fb4a0400e6b4ee5a80409b23f0d2d5c29111c81 100644 (file)
@@ -24,11 +24,18 @@ public sealed class PanelSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet,
         var boxPositive = new StyleBoxFlat { BackgroundColor = sheet.PositivePalette.Background };
         var boxNegative = new StyleBoxFlat { BackgroundColor = sheet.NegativePalette.Background };
         var boxHighlight = new StyleBoxFlat { BackgroundColor = sheet.HighlightPalette.Background };
+        var boxDropTarget = new StyleBoxFlat
+        {
+            BackgroundColor = sheet.ButtonPalette.BackgroundDark.WithAlpha(0.5f),
+            BorderColor = sheet.ButtonPalette.Base,
+            BorderThickness = new(2)
+        };
 
         return
         [
             E<PanelContainer>().Class(StyleClass.PanelLight).Panel(boxLight),
             E<PanelContainer>().Class(StyleClass.PanelDark).Panel(boxDark),
+            E<PanelContainer>().Class(StyleClass.PanelDropTarget).Panel(boxDropTarget),
 
             E<PanelContainer>().Class(StyleClass.Positive).Panel(boxPositive),
             E<PanelContainer>().Class(StyleClass.Negative).Panel(boxNegative),
index e569824b76359b91eb8bac82fb9c0e39a3e4b844..ce4e93bdc9881b19c39489f40e0e8a3a14cac5c3 100644 (file)
@@ -51,6 +51,7 @@ public static class StyleClass
 
     public const string PanelDark = "PanelDark";
     public const string PanelLight = "PanelLight";
+    public const string PanelDropTarget = "PanelDropTarget";
 
     public const string ButtonOpenRight = "OpenRight";
     public const string ButtonOpenLeft = "OpenLeft";
index 822f116e474c76d5586ee3ccb8bc63fd78c66bfb..23253c1d4bf8ed0308d9dc38a5a85691a1cd3d61 100644 (file)
@@ -1,6 +1,6 @@
 using System.Linq;
+using Content.Shared.Body;
 using Content.Shared.Ghost;
-using Content.Shared.Humanoid;
 using Content.Shared.StatusIcon;
 using Content.Shared.StatusIcon.Components;
 using Content.Shared.Zombies;
@@ -40,7 +40,7 @@ public sealed class ZombieSystem : SharedZombieSystem
 
     private void OnStartup(EntityUid uid, ZombieComponent component, ComponentStartup args)
     {
-        if (HasComp<HumanoidAppearanceComponent>(uid))
+        if (HasComp<VisualBodyComponent>(uid))
             return;
 
         if (!TryComp<SpriteComponent>(uid, out var sprite))
index d5791861cf655a31acd75af8a004d3b7223ec7d0..a940df82d24608c3b8fecb968865ff67f539516e 100644 (file)
@@ -110,10 +110,6 @@ public sealed class CharacterCreationTest
         if (a.MemberwiseEquals(b))
             return;
 
-        Assert.That(a.HairStyleId, Is.EqualTo(b.HairStyleId));
-        Assert.That(a.HairColor, Is.EqualTo(b.HairColor));
-        Assert.That(a.FacialHairStyleId, Is.EqualTo(b.FacialHairStyleId));
-        Assert.That(a.FacialHairColor, Is.EqualTo(b.FacialHairColor));
         Assert.That(a.EyeColor, Is.EqualTo(b.EyeColor));
         Assert.That(a.SkinColor, Is.EqualTo(b.SkinColor));
         Assert.That(a.Markings, Is.EquivalentTo(b.Markings));
diff --git a/Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs b/Content.IntegrationTests/Tests/Markings/MarkingManagerTests.cs
new file mode 100644 (file)
index 0000000..c81ff6a
--- /dev/null
@@ -0,0 +1,238 @@
+using System.Collections.Generic;
+using Content.Shared.Body;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+
+namespace Content.IntegrationTests.Tests.Markings;
+
+[TestFixture]
+[TestOf(typeof(MarkingManager))]
+public sealed class MarkingManagerTests
+{
+    [TestPrototypes]
+    private const string Prototypes = @"
+- type: markingsGroup
+  id: Testing
+
+- type: markingsGroup
+  id: TestingOther
+
+- type: markingsGroup
+  id: TestingOptionalEyes
+  limits:
+    enum.HumanoidVisualLayers.Eyes:
+      limit: 1
+      required: false
+
+- type: markingsGroup
+  id: TestingRequiredEyes
+  limits:
+    enum.HumanoidVisualLayers.Eyes:
+      limit: 1
+      required: true
+      default: [ EyesMarking ]
+
+- type: marking
+  id: SingleColorMarking
+  bodyPart: Eyes
+  sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
+  coloring:
+    default:
+      type:
+        !type:EyeColoring
+
+- type: marking
+  id: MenOnlyMarking
+  bodyPart: Eyes
+  sexRestriction: Male
+  sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
+
+- type: marking
+  id: TestingOnlyMarking
+  bodyPart: Eyes
+  groupWhitelist: [ Testing ]
+  sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
+
+- type: marking
+  id: TestingMenOnlyMarking
+  bodyPart: Eyes
+  sexRestriction: Male
+  groupWhitelist: [ Testing ]
+  sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
+
+- type: marking
+  id: EyesMarking
+  bodyPart: Eyes
+  sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
+
+- type: marking
+  id: ChestMarking
+  bodyPart: Chest
+  sprites: [{ sprite: Mobs/Customization/human_hair.rsi, state: afro }]
+";
+
+    [Test]
+    public async Task HairConvesion()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        await server.WaitIdleAsync();
+
+        await server.WaitAssertion(() =>
+        {
+            var markingManager = server.ResolveDependency<MarkingManager>();
+
+            var markings = new List<Marking>() { new("HumanHairLongBedhead2", new List<Color>() { Color.Red }) };
+
+            var converted = markingManager.ConvertMarkings(markings, "Human");
+
+            Assert.That(converted, Does.ContainKey(new ProtoId<OrganCategoryPrototype>("Head")));
+            Assert.That(converted["Head"], Does.ContainKey(HumanoidVisualLayers.Hair));
+            var hairMarkings = converted["Head"][HumanoidVisualLayers.Hair];
+            Assert.That(hairMarkings, Has.Count.EqualTo(1));
+            Assert.That(hairMarkings[0].MarkingId, Is.EqualTo("HumanHairLongBedhead2"));
+            Assert.That(hairMarkings[0].MarkingColors[0], Is.EqualTo(Color.Red));
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    [Test]
+    public async Task LimitsFilling()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        await server.WaitIdleAsync();
+
+        await server.WaitAssertion(() =>
+        {
+            var markingManager = server.ResolveDependency<MarkingManager>();
+            var dict = new Dictionary<HumanoidVisualLayers, List<Marking>>();
+
+            markingManager.EnsureValidLimits(dict, "TestingRequiredEyes", new() { HumanoidVisualLayers.Eyes }, null, null);
+            Assert.That(dict, Does.ContainKey(HumanoidVisualLayers.Eyes));
+            Assert.That(dict[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1));
+            Assert.That(dict[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("EyesMarking"));
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    [Test]
+    public async Task LimitsTruncations()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        await server.WaitIdleAsync();
+
+        await server.WaitAssertion(() =>
+        {
+            var markingManager = server.ResolveDependency<MarkingManager>();
+            var dict = new Dictionary<HumanoidVisualLayers, List<Marking>>()
+            {
+                [HumanoidVisualLayers.Eyes] = new()
+                {
+                    new("EyesMarking", 0),
+                    new("MenOnlyMarking", 0),
+                },
+            };
+
+            markingManager.EnsureValidLimits(dict, "TestingOptionalEyes", new() { HumanoidVisualLayers.Eyes }, null, null);
+            Assert.That(dict[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1));
+            Assert.That(dict[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("MenOnlyMarking"));
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    [Test]
+    public async Task EnsureValidGroupAndSex()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        await server.WaitIdleAsync();
+
+        await server.WaitAssertion(() =>
+        {
+            var markingManager = server.ResolveDependency<MarkingManager>();
+            var dictFactory = static () => new Dictionary<HumanoidVisualLayers, List<Marking>>()
+            {
+                [HumanoidVisualLayers.Eyes] = new()
+                {
+                    new("MenOnlyMarking", 0),
+                    new("TestingOnlyMarking", 0),
+                    new("TestingMenOnlyMarking", 0),
+                }
+            };
+
+            var menMarkings = dictFactory();
+            markingManager.EnsureValidGroupAndSex(menMarkings, "TestingOther", Sex.Male);
+
+            Assert.That(menMarkings[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1));
+            Assert.That(menMarkings[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("MenOnlyMarking"));
+
+            var testingMarkings = dictFactory();
+            markingManager.EnsureValidGroupAndSex(testingMarkings, "Testing", Sex.Female);
+
+            Assert.That(testingMarkings[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(1));
+            Assert.That(testingMarkings[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("TestingOnlyMarking"));
+
+            var testingMenMarkings = dictFactory();
+            markingManager.EnsureValidGroupAndSex(testingMenMarkings, "Testing", Sex.Male);
+
+            Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes], Has.Count.EqualTo(3));
+            Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][0].MarkingId, Is.EqualTo("MenOnlyMarking"));
+            Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][1].MarkingId, Is.EqualTo("TestingOnlyMarking"));
+            Assert.That(testingMenMarkings[HumanoidVisualLayers.Eyes][2].MarkingId, Is.EqualTo("TestingMenOnlyMarking"));
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    [Test]
+    public async Task EnsureValidColors()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        await server.WaitIdleAsync();
+
+        await server.WaitAssertion(() =>
+        {
+            var markingManager = server.ResolveDependency<MarkingManager>();
+
+            var dict = new Dictionary<HumanoidVisualLayers, List<Marking>>()
+            {
+                [HumanoidVisualLayers.Eyes] = new()
+                {
+                    new("SingleColorMarking", 0),
+                    new("SingleColorMarking", new List<Color>() { Color.Red }),
+                    new("SingleColorMarking", 2),
+                    new("SingleColorMarking", new List<Color>() { Color.Green }),
+                }
+            };
+
+            markingManager.EnsureValidColors(dict);
+
+            var eyeMarkings = dict[HumanoidVisualLayers.Eyes];
+
+            // ensure all colors are the correct length
+            Assert.That(eyeMarkings[0].MarkingColors, Has.Count.EqualTo(1));
+            Assert.That(eyeMarkings[1].MarkingColors, Has.Count.EqualTo(1));
+            Assert.That(eyeMarkings[2].MarkingColors, Has.Count.EqualTo(1));
+            Assert.That(eyeMarkings[3].MarkingColors, Has.Count.EqualTo(1));
+
+            // and make sure we didn't shuffle our colors around
+            Assert.That(eyeMarkings[1].MarkingColors[0], Is.EqualTo(Color.Red));
+            Assert.That(eyeMarkings[3].MarkingColors[0], Is.EqualTo(Color.Green));
+        });
+
+        await pair.CleanReturnAsync();
+    }
+}
index afaeafc6655e31cc6672af0ae04943e038dcf6c8..9e90919ef413ad17c5e3df022ca59fcf1827bb9a 100644 (file)
@@ -8,11 +8,13 @@ using Content.Shared.Preferences.Loadouts;
 using Content.Shared.Preferences.Loadouts.Effects;
 using Microsoft.Data.Sqlite;
 using Microsoft.EntityFrameworkCore;
+using Robust.Shared.Asynchronous;
 using Robust.Shared.Configuration;
 using Robust.Shared.Enums;
 using Robust.Shared.Log;
 using Robust.Shared.Maths;
 using Robust.Shared.Network;
+using Robust.Shared.Serialization.Manager;
 using Robust.UnitTesting;
 
 namespace Content.IntegrationTests.Tests.Preferences
@@ -46,10 +48,6 @@ namespace Content.IntegrationTests.Tests.Preferences
                 Species = "Human",
                 Age = 21,
                 Appearance = new(
-                    "Afro",
-                    Color.Aqua,
-                    "Shaved",
-                    Color.Aquamarine,
                     Color.Azure,
                     Color.Beige,
                     new ())
@@ -59,12 +57,14 @@ namespace Content.IntegrationTests.Tests.Preferences
         private static ServerDbSqlite GetDb(RobustIntegrationTest.ServerIntegrationInstance server)
         {
             var cfg = server.ResolveDependency<IConfigurationManager>();
+            var serialization = server.ResolveDependency<ISerializationManager>();
+            var task = server.ResolveDependency<ITaskManager>();
             var opsLog = server.ResolveDependency<ILogManager>().GetSawmill("db.ops");
             var builder = new DbContextOptionsBuilder<SqliteServerDbContext>();
             var conn = new SqliteConnection("Data Source=:memory:");
             conn.Open();
             builder.UseSqlite(conn);
-            return new ServerDbSqlite(() => builder.Options, true, cfg, true, opsLog);
+            return new ServerDbSqlite(() => builder.Options, true, cfg, true, opsLog, task, serialization);
         }
 
         [Test]
diff --git a/Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.Designer.cs b/Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.Designer.cs
new file mode 100644 (file)
index 0000000..a8d031f
--- /dev/null
@@ -0,0 +1,2125 @@
+// <auto-generated />
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text.Json;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+    [DbContext(typeof(PostgresServerDbContext))]
+    [Migration("20260118084629_OrganMarkings")]
+    partial class OrganMarkings
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "10.0.0")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<int?>("AdminRankId")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<bool>("Deadminned")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deadminned");
+
+                    b.Property<bool>("Suspended")
+                        .HasColumnType("boolean")
+                        .HasColumnName("suspended");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("text")
+                        .HasColumnName("title");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_admin");
+
+                    b.HasIndex("AdminRankId")
+                        .HasDatabaseName("IX_admin_admin_rank_id");
+
+                    b.ToTable("admin", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_flag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("AdminId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("admin_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flag");
+
+                    b.Property<bool>("Negative")
+                        .HasColumnType("boolean")
+                        .HasColumnName("negative");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_flag");
+
+                    b.HasIndex("AdminId")
+                        .HasDatabaseName("IX_admin_flag_admin_id");
+
+                    b.HasIndex("Flag", "AdminId")
+                        .IsUnique();
+
+                    b.ToTable("admin_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Id")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_log_id");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("date");
+
+                    b.Property<short>("Impact")
+                        .HasColumnType("smallint")
+                        .HasColumnName("impact");
+
+                    b.Property<JsonDocument>("Json")
+                        .IsRequired()
+                        .HasColumnType("jsonb")
+                        .HasColumnName("json");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("message");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("integer")
+                        .HasColumnName("type");
+
+                    b.HasKey("RoundId", "Id")
+                        .HasName("PK_admin_log");
+
+                    b.HasIndex("Date");
+
+                    b.HasIndex("Message")
+                        .HasAnnotation("Npgsql:TsVectorConfig", "english");
+
+                    NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
+
+                    b.HasIndex("Type")
+                        .HasDatabaseName("IX_admin_log_type");
+
+                    b.ToTable("admin_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("LogId")
+                        .HasColumnType("integer")
+                        .HasColumnName("log_id");
+
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.HasKey("RoundId", "LogId", "PlayerUserId")
+                        .HasName("PK_admin_log_player");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+                    b.ToTable("admin_log_player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_messages_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<bool>("Dismissed")
+                        .HasColumnType("boolean")
+                        .HasColumnName("dismissed");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Seen")
+                        .HasColumnType("boolean")
+                        .HasColumnName("seen");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_messages");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_messages_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_messages_round_id");
+
+                    b.ToTable("admin_messages", null, t =>
+                        {
+                            t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_notes_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Secret")
+                        .HasColumnType("boolean")
+                        .HasColumnName("secret");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_notes");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_notes_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_notes_round_id");
+
+                    b.ToTable("admin_notes", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank");
+
+                    b.ToTable("admin_rank", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_flag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("AdminRankId")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flag");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank_flag");
+
+                    b.HasIndex("AdminRankId");
+
+                    b.HasIndex("Flag", "AdminRankId")
+                        .IsUnique();
+
+                    b.ToTable("admin_rank_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_watchlists_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_watchlists");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_watchlists_round_id");
+
+                    b.ToTable("admin_watchlists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("antag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AntagName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("antag_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_antag");
+
+                    b.HasIndex("ProfileId", "AntagName")
+                        .IsUnique();
+
+                    b.ToTable("antag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("assigned_user_id_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_assigned_user_id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.HasIndex("UserName")
+                        .IsUnique();
+
+                    b.ToTable("assigned_user_id", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_template_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("boolean")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("integer")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<TimeSpan>("Length")
+                        .HasColumnType("interval")
+                        .HasColumnName("length");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("title");
+
+                    b.HasKey("Id")
+                        .HasName("PK_ban_template");
+
+                    b.ToTable("ban_template", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_blacklist");
+
+                    b.ToTable("blacklist", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("connection_log_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<IPAddress>("Address")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<byte?>("Denied")
+                        .HasColumnType("smallint")
+                        .HasColumnName("denied");
+
+                    b.Property<int>("ServerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasDefaultValue(0)
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("time");
+
+                    b.Property<float>("Trust")
+                        .HasColumnType("real")
+                        .HasColumnName("trust");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_connection_log");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_connection_log_server_id");
+
+                    b.HasIndex("Time");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("connection_log", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("ipintel_cache_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<IPAddress>("Address")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<float>("Score")
+                        .HasColumnType("real")
+                        .HasColumnName("score");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("time");
+
+                    b.HasKey("Id")
+                        .HasName("PK_ipintel_cache");
+
+                    b.ToTable("ipintel_cache", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("job_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("JobName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("job_name");
+
+                    b.Property<int>("Priority")
+                        .HasColumnType("integer")
+                        .HasColumnName("priority");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_job");
+
+                    b.HasIndex("ProfileId");
+
+                    b.HasIndex("ProfileId", "JobName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+                        .IsUnique()
+                        .HasFilter("priority = 3");
+
+                    b.ToTable("job", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("play_time_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("PlayerId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_id");
+
+                    b.Property<TimeSpan>("TimeSpent")
+                        .HasColumnType("interval")
+                        .HasColumnName("time_spent");
+
+                    b.Property<string>("Tracker")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("tracker");
+
+                    b.HasKey("Id")
+                        .HasName("PK_play_time");
+
+                    b.HasIndex("PlayerId", "Tracker")
+                        .IsUnique();
+
+                    b.ToTable("play_time", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("player_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("FirstSeenTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("first_seen_time");
+
+                    b.Property<DateTime?>("LastReadRules")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_read_rules");
+
+                    b.Property<IPAddress>("LastSeenAddress")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("last_seen_address");
+
+                    b.Property<DateTime>("LastSeenTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_seen_time");
+
+                    b.Property<string>("LastSeenUserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("last_seen_user_name");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_player");
+
+                    b.HasAlternateKey("UserId")
+                        .HasName("ak_player_user_id");
+
+                    b.HasIndex("LastSeenUserName");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("player", null, t =>
+                        {
+                            t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("preference_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AdminOOCColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("admin_ooc_color");
+
+                    b.PrimitiveCollection<List<string>>("ConstructionFavorites")
+                        .IsRequired()
+                        .HasColumnType("text[]")
+                        .HasColumnName("construction_favorites");
+
+                    b.Property<int>("SelectedCharacterSlot")
+                        .HasColumnType("integer")
+                        .HasColumnName("selected_character_slot");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_preference");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("preference", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("Age")
+                        .HasColumnType("integer")
+                        .HasColumnName("age");
+
+                    b.Property<string>("CharacterName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("char_name");
+
+                    b.Property<string>("EyeColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("eye_color");
+
+                    b.Property<string>("FacialHairColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("facial_hair_color");
+
+                    b.Property<string>("FacialHairName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("facial_hair_name");
+
+                    b.Property<string>("FlavorText")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flavor_text");
+
+                    b.Property<string>("Gender")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("gender");
+
+                    b.Property<string>("HairColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("hair_color");
+
+                    b.Property<string>("HairName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("hair_name");
+
+                    b.Property<JsonDocument>("Markings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("markings");
+
+                    b.Property<JsonDocument>("OrganMarkings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("organ_markings");
+
+                    b.Property<int>("PreferenceId")
+                        .HasColumnType("integer")
+                        .HasColumnName("preference_id");
+
+                    b.Property<int>("PreferenceUnavailable")
+                        .HasColumnType("integer")
+                        .HasColumnName("pref_unavailable");
+
+                    b.Property<string>("Sex")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("sex");
+
+                    b.Property<string>("SkinColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("skin_color");
+
+                    b.Property<int>("Slot")
+                        .HasColumnType("integer")
+                        .HasColumnName("slot");
+
+                    b.Property<int>("SpawnPriority")
+                        .HasColumnType("integer")
+                        .HasColumnName("spawn_priority");
+
+                    b.Property<string>("Species")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("species");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile");
+
+                    b.HasIndex("PreferenceId")
+                        .HasDatabaseName("IX_profile_preference_id");
+
+                    b.HasIndex("Slot", "PreferenceId")
+                        .IsUnique();
+
+                    b.ToTable("profile", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("LoadoutName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("ProfileLoadoutGroupId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout");
+
+                    b.HasIndex("ProfileLoadoutGroupId");
+
+                    b.ToTable("profile_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("group_name");
+
+                    b.Property<int>("ProfileRoleLoadoutId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout_group");
+
+                    b.HasIndex("ProfileRoleLoadoutId");
+
+                    b.ToTable("profile_loadout_group", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("EntityName")
+                        .HasMaxLength(256)
+                        .HasColumnType("character varying(256)")
+                        .HasColumnName("entity_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("RoleName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_role_loadout");
+
+                    b.HasIndex("ProfileId");
+
+                    b.ToTable("profile_role_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+                {
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<string>("RoleId")
+                        .HasColumnType("text")
+                        .HasColumnName("role_id");
+
+                    b.HasKey("PlayerUserId", "RoleId")
+                        .HasName("PK_role_whitelists");
+
+                    b.ToTable("role_whitelists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ServerId")
+                        .HasColumnType("integer")
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime?>("StartDate")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("start_date");
+
+                    b.HasKey("Id")
+                        .HasName("PK_round");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_round_server_id");
+
+                    b.HasIndex("StartDate");
+
+                    b.ToTable("round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server");
+
+                    b.ToTable("server", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_ban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<NpgsqlInet?>("Address")
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("boolean")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("integer")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_ban_round_id");
+
+                    b.ToTable("server_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<int>("Flags")
+                        .HasColumnType("integer")
+                        .HasColumnName("flags");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_server_ban_exemption");
+
+                    b.ToTable("server_ban_exemption", null, t =>
+                        {
+                            t.HasCheckConstraint("FlagsNotZero", "flags != 0");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_ban_hit_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("ConnectionId")
+                        .HasColumnType("integer")
+                        .HasColumnName("connection_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban_hit");
+
+                    b.HasIndex("BanId")
+                        .HasDatabaseName("IX_server_ban_hit_ban_id");
+
+                    b.HasIndex("ConnectionId")
+                        .HasDatabaseName("IX_server_ban_hit_connection_id");
+
+                    b.ToTable("server_ban_hit", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_role_ban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<NpgsqlInet?>("Address")
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_id");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_role_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_role_ban_round_id");
+
+                    b.ToTable("server_role_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("role_unban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_role_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("unban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("trait_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("TraitName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("trait_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_trait");
+
+                    b.HasIndex("ProfileId", "TraitName")
+                        .IsUnique();
+
+                    b.ToTable("trait", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("uploaded_resource_log_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<byte[]>("Data")
+                        .IsRequired()
+                        .HasColumnType("bytea")
+                        .HasColumnName("data");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("date");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("path");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_uploaded_resource_log");
+
+                    b.ToTable("uploaded_resource_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_whitelist");
+
+                    b.ToTable("whitelist", (string)null);
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.Property<int>("PlayersId")
+                        .HasColumnType("integer")
+                        .HasColumnName("players_id");
+
+                    b.Property<int>("RoundsId")
+                        .HasColumnType("integer")
+                        .HasColumnName("rounds_id");
+
+                    b.HasKey("PlayersId", "RoundsId")
+                        .HasName("PK_player_round");
+
+                    b.HasIndex("RoundsId")
+                        .HasDatabaseName("IX_player_round_rounds_id");
+
+                    b.ToTable("player_round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+                        .WithMany("Admins")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+                    b.Navigation("AdminRank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Admin", "Admin")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+                    b.Navigation("Admin");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("RoundId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_round_round_id");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.AdminLog", "Log")
+                        .WithMany("Players")
+                        .HasForeignKey("RoundId", "LogId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id");
+
+                    b.Navigation("Log");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminMessagesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminMessagesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminMessagesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminMessagesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_messages_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_messages_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminNotesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminNotesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminNotesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminNotesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_notes_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_notes_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "Rank")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+                    b.Navigation("Rank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminWatchlistsCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminWatchlistsDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminWatchlistsLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminWatchlistsReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_watchlists_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_watchlists_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Antags")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_antag_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("ConnectionLogs")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired()
+                        .HasConstraintName("FK_connection_log_server_server_id");
+
+                    b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
+                        {
+                            b1.Property<int>("ConnectionLogId")
+                                .HasColumnType("integer")
+                                .HasColumnName("connection_log_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("bytea")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("Type")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("integer")
+                                .HasDefaultValue(0)
+                                .HasColumnName("hwid_type");
+
+                            b1.HasKey("ConnectionLogId");
+
+                            b1.ToTable("connection_log");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConnectionLogId")
+                                .HasConstraintName("FK_connection_log_connection_log_connection_log_id");
+                        });
+
+                    b.Navigation("HWId");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Jobs")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_job_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
+                        {
+                            b1.Property<int>("PlayerId")
+                                .HasColumnType("integer")
+                                .HasColumnName("player_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("bytea")
+                                .HasColumnName("last_seen_hwid");
+
+                            b1.Property<int>("Type")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("integer")
+                                .HasDefaultValue(0)
+                                .HasColumnName("last_seen_hwid_type");
+
+                            b1.HasKey("PlayerId");
+
+                            b1.ToTable("player");
+
+                            b1.WithOwner()
+                                .HasForeignKey("PlayerId")
+                                .HasConstraintName("FK_player_player_player_id");
+                        });
+
+                    b.Navigation("LastSeenHWId");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.HasOne("Content.Server.Database.Preference", "Preference")
+                        .WithMany("Profiles")
+                        .HasForeignKey("PreferenceId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_preference_preference_id");
+
+                    b.Navigation("Preference");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileLoadoutGroupId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~");
+
+                    b.Navigation("ProfileLoadoutGroup");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+                        .WithMany("Groups")
+                        .HasForeignKey("ProfileRoleLoadoutId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~");
+
+                    b.Navigation("ProfileRoleLoadout");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("JobWhitelists")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_role_whitelists_player_player_user_id");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("Rounds")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_round_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_ban_round_round_id");
+
+                    b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
+                        {
+                            b1.Property<int>("ServerBanId")
+                                .HasColumnType("integer")
+                                .HasColumnName("server_ban_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("bytea")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("Type")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("integer")
+                                .HasDefaultValue(0)
+                                .HasColumnName("hwid_type");
+
+                            b1.HasKey("ServerBanId");
+
+                            b1.ToTable("server_ban");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ServerBanId")
+                                .HasConstraintName("FK_server_ban_server_ban_server_ban_id");
+                        });
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("HWId");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithMany("BanHits")
+                        .HasForeignKey("BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
+
+                    b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
+                        .WithMany("BanHits")
+                        .HasForeignKey("ConnectionId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_connection_log_connection_id");
+
+                    b.Navigation("Ban");
+
+                    b.Navigation("Connection");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerRoleBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerRoleBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_role_ban_round_round_id");
+
+                    b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
+                        {
+                            b1.Property<int>("ServerRoleBanId")
+                                .HasColumnType("integer")
+                                .HasColumnName("server_role_ban_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("bytea")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("Type")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("integer")
+                                .HasDefaultValue(0)
+                                .HasColumnName("hwid_type");
+
+                            b1.HasKey("ServerRoleBanId");
+
+                            b1.ToTable("server_role_ban");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ServerRoleBanId")
+                                .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
+                        });
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("HWId");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Traits")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_trait_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", null)
+                        .WithMany()
+                        .HasForeignKey("PlayersId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_player_players_id");
+
+                    b.HasOne("Content.Server.Database.Round", null)
+                        .WithMany()
+                        .HasForeignKey("RoundsId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_round_rounds_id");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Navigation("Players");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Navigation("Admins");
+
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Navigation("BanHits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Navigation("AdminLogs");
+
+                    b.Navigation("AdminMessagesCreated");
+
+                    b.Navigation("AdminMessagesDeleted");
+
+                    b.Navigation("AdminMessagesLastEdited");
+
+                    b.Navigation("AdminMessagesReceived");
+
+                    b.Navigation("AdminNotesCreated");
+
+                    b.Navigation("AdminNotesDeleted");
+
+                    b.Navigation("AdminNotesLastEdited");
+
+                    b.Navigation("AdminNotesReceived");
+
+                    b.Navigation("AdminServerBansCreated");
+
+                    b.Navigation("AdminServerBansLastEdited");
+
+                    b.Navigation("AdminServerRoleBansCreated");
+
+                    b.Navigation("AdminServerRoleBansLastEdited");
+
+                    b.Navigation("AdminWatchlistsCreated");
+
+                    b.Navigation("AdminWatchlistsDeleted");
+
+                    b.Navigation("AdminWatchlistsLastEdited");
+
+                    b.Navigation("AdminWatchlistsReceived");
+
+                    b.Navigation("JobWhitelists");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Navigation("Profiles");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Navigation("Antags");
+
+                    b.Navigation("Jobs");
+
+                    b.Navigation("Loadouts");
+
+                    b.Navigation("Traits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Navigation("Loadouts");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Navigation("Groups");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Navigation("AdminLogs");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Navigation("ConnectionLogs");
+
+                    b.Navigation("Rounds");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Navigation("BanHits");
+
+                    b.Navigation("Unban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Navigation("Unban");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.cs b/Content.Server.Database/Migrations/Postgres/20260118084629_OrganMarkings.cs
new file mode 100644 (file)
index 0000000..ec2b926
--- /dev/null
@@ -0,0 +1,29 @@
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+    /// <inheritdoc />
+    public partial class OrganMarkings : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<JsonDocument>(
+                name: "organ_markings",
+                table: "profile",
+                type: "jsonb",
+                nullable: true);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "organ_markings",
+                table: "profile");
+        }
+    }
+}
index 51ba56049e783fa7d95cb81217c9b27a9dbdbd5e..9ab525942c7a712a58ca9baafa5133e453b48d7a 100644 (file)
@@ -1,5 +1,6 @@
 // <auto-generated />
 using System;
+using System.Collections.Generic;
 using System.Net;
 using System.Text.Json;
 using Content.Server.Database;
@@ -20,7 +21,7 @@ namespace Content.Server.Database.Migrations.Postgres
         {
 #pragma warning disable 612, 618
             modelBuilder
-                .HasAnnotation("ProductVersion", "9.0.1")
+                .HasAnnotation("ProductVersion", "10.0.0")
                 .HasAnnotation("Relational:MaxIdentifierLength", 63);
 
             NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -795,7 +796,7 @@ namespace Content.Server.Database.Migrations.Postgres
                         .HasColumnType("text")
                         .HasColumnName("admin_ooc_color");
 
-                    b.PrimitiveCollection<string[]>("ConstructionFavorites")
+                    b.PrimitiveCollection<List<string>>("ConstructionFavorites")
                         .IsRequired()
                         .HasColumnType("text[]")
                         .HasColumnName("construction_favorites");
@@ -874,6 +875,10 @@ namespace Content.Server.Database.Migrations.Postgres
                         .HasColumnType("jsonb")
                         .HasColumnName("markings");
 
+                    b.Property<JsonDocument>("OrganMarkings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("organ_markings");
+
                     b.Property<int>("PreferenceId")
                         .HasColumnType("integer")
                         .HasColumnName("preference_id");
diff --git a/Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.Designer.cs
new file mode 100644 (file)
index 0000000..7536236
--- /dev/null
@@ -0,0 +1,2048 @@
+// <auto-generated />
+using System;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Sqlite
+{
+    [DbContext(typeof(SqliteServerDbContext))]
+    [Migration("20260118084622_OrganMarkings")]
+    partial class OrganMarkings
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<int?>("AdminRankId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<bool>("Deadminned")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deadminned");
+
+                    b.Property<bool>("Suspended")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("suspended");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("title");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_admin");
+
+                    b.HasIndex("AdminRankId")
+                        .HasDatabaseName("IX_admin_admin_rank_id");
+
+                    b.ToTable("admin", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_flag_id");
+
+                    b.Property<Guid>("AdminId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("admin_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flag");
+
+                    b.Property<bool>("Negative")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("negative");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_flag");
+
+                    b.HasIndex("AdminId")
+                        .HasDatabaseName("IX_admin_flag_admin_id");
+
+                    b.HasIndex("Flag", "AdminId")
+                        .IsUnique();
+
+                    b.ToTable("admin_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_log_id");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("date");
+
+                    b.Property<sbyte>("Impact")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("impact");
+
+                    b.Property<string>("Json")
+                        .IsRequired()
+                        .HasColumnType("jsonb")
+                        .HasColumnName("json");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("type");
+
+                    b.HasKey("RoundId", "Id")
+                        .HasName("PK_admin_log");
+
+                    b.HasIndex("Date");
+
+                    b.HasIndex("Type")
+                        .HasDatabaseName("IX_admin_log_type");
+
+                    b.ToTable("admin_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("LogId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("log_id");
+
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.HasKey("RoundId", "LogId", "PlayerUserId")
+                        .HasName("PK_admin_log_player");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+                    b.ToTable("admin_log_player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_messages_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<bool>("Dismissed")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("dismissed");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Seen")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("seen");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_messages");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_messages_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_messages_round_id");
+
+                    b.ToTable("admin_messages", null, t =>
+                        {
+                            t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_notes_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Secret")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("secret");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_notes");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_notes_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_notes_round_id");
+
+                    b.ToTable("admin_notes", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank");
+
+                    b.ToTable("admin_rank", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_flag_id");
+
+                    b.Property<int>("AdminRankId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flag");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank_flag");
+
+                    b.HasIndex("AdminRankId");
+
+                    b.HasIndex("Flag", "AdminRankId")
+                        .IsUnique();
+
+                    b.ToTable("admin_rank_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_watchlists_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_watchlists");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_watchlists_round_id");
+
+                    b.ToTable("admin_watchlists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("antag_id");
+
+                    b.Property<string>("AntagName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("antag_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_antag");
+
+                    b.HasIndex("ProfileId", "AntagName")
+                        .IsUnique();
+
+                    b.ToTable("antag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("assigned_user_id_id");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_assigned_user_id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.HasIndex("UserName")
+                        .IsUnique();
+
+                    b.ToTable("assigned_user_id", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_template_id");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<TimeSpan>("Length")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("length");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("title");
+
+                    b.HasKey("Id")
+                        .HasName("PK_ban_template");
+
+                    b.ToTable("ban_template", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_blacklist");
+
+                    b.ToTable("blacklist", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("connection_log_id");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<byte?>("Denied")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("denied");
+
+                    b.Property<int>("ServerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasDefaultValue(0)
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time");
+
+                    b.Property<float>("Trust")
+                        .HasColumnType("REAL")
+                        .HasColumnName("trust");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_connection_log");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_connection_log_server_id");
+
+                    b.HasIndex("Time");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("connection_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ipintel_cache_id");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<float>("Score")
+                        .HasColumnType("REAL")
+                        .HasColumnName("score");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time");
+
+                    b.HasKey("Id")
+                        .HasName("PK_ipintel_cache");
+
+                    b.HasIndex("Address")
+                        .IsUnique();
+
+                    b.ToTable("ipintel_cache", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("job_id");
+
+                    b.Property<string>("JobName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("job_name");
+
+                    b.Property<int>("Priority")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("priority");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_job");
+
+                    b.HasIndex("ProfileId");
+
+                    b.HasIndex("ProfileId", "JobName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+                        .IsUnique()
+                        .HasFilter("priority = 3");
+
+                    b.ToTable("job", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("play_time_id");
+
+                    b.Property<Guid>("PlayerId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_id");
+
+                    b.Property<TimeSpan>("TimeSpent")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time_spent");
+
+                    b.Property<string>("Tracker")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("tracker");
+
+                    b.HasKey("Id")
+                        .HasName("PK_play_time");
+
+                    b.HasIndex("PlayerId", "Tracker")
+                        .IsUnique();
+
+                    b.ToTable("play_time", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("player_id");
+
+                    b.Property<DateTime>("FirstSeenTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("first_seen_time");
+
+                    b.Property<DateTime?>("LastReadRules")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_read_rules");
+
+                    b.Property<string>("LastSeenAddress")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_address");
+
+                    b.Property<DateTime>("LastSeenTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_time");
+
+                    b.Property<string>("LastSeenUserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_user_name");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_player");
+
+                    b.HasAlternateKey("UserId")
+                        .HasName("ak_player_user_id");
+
+                    b.HasIndex("LastSeenUserName");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("preference_id");
+
+                    b.Property<string>("AdminOOCColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("admin_ooc_color");
+
+                    b.PrimitiveCollection<string>("ConstructionFavorites")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("construction_favorites");
+
+                    b.Property<int>("SelectedCharacterSlot")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("selected_character_slot");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_preference");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("preference", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<int>("Age")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("age");
+
+                    b.Property<string>("CharacterName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("char_name");
+
+                    b.Property<string>("EyeColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("eye_color");
+
+                    b.Property<string>("FacialHairColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("facial_hair_color");
+
+                    b.Property<string>("FacialHairName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("facial_hair_name");
+
+                    b.Property<string>("FlavorText")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flavor_text");
+
+                    b.Property<string>("Gender")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("gender");
+
+                    b.Property<string>("HairColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("hair_color");
+
+                    b.Property<string>("HairName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("hair_name");
+
+                    b.Property<byte[]>("Markings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("markings");
+
+                    b.Property<byte[]>("OrganMarkings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("organ_markings");
+
+                    b.Property<int>("PreferenceId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("preference_id");
+
+                    b.Property<int>("PreferenceUnavailable")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("pref_unavailable");
+
+                    b.Property<string>("Sex")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("sex");
+
+                    b.Property<string>("SkinColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("skin_color");
+
+                    b.Property<int>("Slot")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("slot");
+
+                    b.Property<int>("SpawnPriority")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("spawn_priority");
+
+                    b.Property<string>("Species")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("species");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile");
+
+                    b.HasIndex("PreferenceId")
+                        .HasDatabaseName("IX_profile_preference_id");
+
+                    b.HasIndex("Slot", "PreferenceId")
+                        .IsUnique();
+
+                    b.ToTable("profile", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_id");
+
+                    b.Property<string>("LoadoutName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("ProfileLoadoutGroupId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout");
+
+                    b.HasIndex("ProfileLoadoutGroupId");
+
+                    b.ToTable("profile_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("group_name");
+
+                    b.Property<int>("ProfileRoleLoadoutId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout_group");
+
+                    b.HasIndex("ProfileRoleLoadoutId");
+
+                    b.ToTable("profile_loadout_group", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.Property<string>("EntityName")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("entity_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("RoleName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_role_loadout");
+
+                    b.HasIndex("ProfileId");
+
+                    b.ToTable("profile_role_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+                {
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<string>("RoleId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_id");
+
+                    b.HasKey("PlayerUserId", "RoleId")
+                        .HasName("PK_role_whitelists");
+
+                    b.ToTable("role_whitelists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("ServerId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime?>("StartDate")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("start_date");
+
+                    b.HasKey("Id")
+                        .HasName("PK_round");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_round_server_id");
+
+                    b.HasIndex("StartDate");
+
+                    b.ToTable("round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_id");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server");
+
+                    b.ToTable("server", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_ban_id");
+
+                    b.Property<string>("Address")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_ban_round_id");
+
+                    b.ToTable("server_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<int>("Flags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("flags");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_server_ban_exemption");
+
+                    b.ToTable("server_ban_exemption", null, t =>
+                        {
+                            t.HasCheckConstraint("FlagsNotZero", "flags != 0");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_ban_hit_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("ConnectionId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("connection_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban_hit");
+
+                    b.HasIndex("BanId")
+                        .HasDatabaseName("IX_server_ban_hit_ban_id");
+
+                    b.HasIndex("ConnectionId")
+                        .HasDatabaseName("IX_server_ban_hit_connection_id");
+
+                    b.ToTable("server_ban_hit", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_role_ban_id");
+
+                    b.Property<string>("Address")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_id");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_role_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_role_ban_round_id");
+
+                    b.ToTable("server_role_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("role_unban_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_role_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("unban_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("trait_id");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("TraitName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("trait_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_trait");
+
+                    b.HasIndex("ProfileId", "TraitName")
+                        .IsUnique();
+
+                    b.ToTable("trait", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("uploaded_resource_log_id");
+
+                    b.Property<byte[]>("Data")
+                        .IsRequired()
+                        .HasColumnType("BLOB")
+                        .HasColumnName("data");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("date");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("path");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_uploaded_resource_log");
+
+                    b.ToTable("uploaded_resource_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_whitelist");
+
+                    b.ToTable("whitelist", (string)null);
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.Property<int>("PlayersId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("players_id");
+
+                    b.Property<int>("RoundsId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("rounds_id");
+
+                    b.HasKey("PlayersId", "RoundsId")
+                        .HasName("PK_player_round");
+
+                    b.HasIndex("RoundsId")
+                        .HasDatabaseName("IX_player_round_rounds_id");
+
+                    b.ToTable("player_round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+                        .WithMany("Admins")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+                    b.Navigation("AdminRank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Admin", "Admin")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+                    b.Navigation("Admin");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("RoundId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_round_round_id");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.AdminLog", "Log")
+                        .WithMany("Players")
+                        .HasForeignKey("RoundId", "LogId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id");
+
+                    b.Navigation("Log");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminMessagesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminMessagesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminMessagesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminMessagesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_messages_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_messages_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminNotesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminNotesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminNotesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminNotesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_notes_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_notes_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "Rank")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+                    b.Navigation("Rank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminWatchlistsCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminWatchlistsDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminWatchlistsLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminWatchlistsReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_watchlists_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_watchlists_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Antags")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_antag_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("ConnectionLogs")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired()
+                        .HasConstraintName("FK_connection_log_server_server_id");
+
+                    b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
+                        {
+                            b1.Property<int>("ConnectionLogId")
+                                .HasColumnType("INTEGER")
+                                .HasColumnName("connection_log_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("BLOB")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("Type")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("INTEGER")
+                                .HasDefaultValue(0)
+                                .HasColumnName("hwid_type");
+
+                            b1.HasKey("ConnectionLogId");
+
+                            b1.ToTable("connection_log");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConnectionLogId")
+                                .HasConstraintName("FK_connection_log_connection_log_connection_log_id");
+                        });
+
+                    b.Navigation("HWId");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Jobs")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_job_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
+                        {
+                            b1.Property<int>("PlayerId")
+                                .HasColumnType("INTEGER")
+                                .HasColumnName("player_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("BLOB")
+                                .HasColumnName("last_seen_hwid");
+
+                            b1.Property<int>("Type")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("INTEGER")
+                                .HasDefaultValue(0)
+                                .HasColumnName("last_seen_hwid_type");
+
+                            b1.HasKey("PlayerId");
+
+                            b1.ToTable("player");
+
+                            b1.WithOwner()
+                                .HasForeignKey("PlayerId")
+                                .HasConstraintName("FK_player_player_player_id");
+                        });
+
+                    b.Navigation("LastSeenHWId");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.HasOne("Content.Server.Database.Preference", "Preference")
+                        .WithMany("Profiles")
+                        .HasForeignKey("PreferenceId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_preference_preference_id");
+
+                    b.Navigation("Preference");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileLoadoutGroupId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id");
+
+                    b.Navigation("ProfileLoadoutGroup");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+                        .WithMany("Groups")
+                        .HasForeignKey("ProfileRoleLoadoutId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id");
+
+                    b.Navigation("ProfileRoleLoadout");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("JobWhitelists")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_role_whitelists_player_player_user_id");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("Rounds")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_round_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_ban_round_round_id");
+
+                    b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
+                        {
+                            b1.Property<int>("ServerBanId")
+                                .HasColumnType("INTEGER")
+                                .HasColumnName("server_ban_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("BLOB")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("Type")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("INTEGER")
+                                .HasDefaultValue(0)
+                                .HasColumnName("hwid_type");
+
+                            b1.HasKey("ServerBanId");
+
+                            b1.ToTable("server_ban");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ServerBanId")
+                                .HasConstraintName("FK_server_ban_server_ban_server_ban_id");
+                        });
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("HWId");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithMany("BanHits")
+                        .HasForeignKey("BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
+
+                    b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
+                        .WithMany("BanHits")
+                        .HasForeignKey("ConnectionId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_connection_log_connection_id");
+
+                    b.Navigation("Ban");
+
+                    b.Navigation("Connection");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerRoleBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerRoleBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_role_ban_round_round_id");
+
+                    b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
+                        {
+                            b1.Property<int>("ServerRoleBanId")
+                                .HasColumnType("INTEGER")
+                                .HasColumnName("server_role_ban_id");
+
+                            b1.Property<byte[]>("Hwid")
+                                .IsRequired()
+                                .HasColumnType("BLOB")
+                                .HasColumnName("hwid");
+
+                            b1.Property<int>("Type")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("INTEGER")
+                                .HasDefaultValue(0)
+                                .HasColumnName("hwid_type");
+
+                            b1.HasKey("ServerRoleBanId");
+
+                            b1.ToTable("server_role_ban");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ServerRoleBanId")
+                                .HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
+                        });
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("HWId");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Traits")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_trait_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", null)
+                        .WithMany()
+                        .HasForeignKey("PlayersId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_player_players_id");
+
+                    b.HasOne("Content.Server.Database.Round", null)
+                        .WithMany()
+                        .HasForeignKey("RoundsId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_round_rounds_id");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Navigation("Players");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Navigation("Admins");
+
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Navigation("BanHits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Navigation("AdminLogs");
+
+                    b.Navigation("AdminMessagesCreated");
+
+                    b.Navigation("AdminMessagesDeleted");
+
+                    b.Navigation("AdminMessagesLastEdited");
+
+                    b.Navigation("AdminMessagesReceived");
+
+                    b.Navigation("AdminNotesCreated");
+
+                    b.Navigation("AdminNotesDeleted");
+
+                    b.Navigation("AdminNotesLastEdited");
+
+                    b.Navigation("AdminNotesReceived");
+
+                    b.Navigation("AdminServerBansCreated");
+
+                    b.Navigation("AdminServerBansLastEdited");
+
+                    b.Navigation("AdminServerRoleBansCreated");
+
+                    b.Navigation("AdminServerRoleBansLastEdited");
+
+                    b.Navigation("AdminWatchlistsCreated");
+
+                    b.Navigation("AdminWatchlistsDeleted");
+
+                    b.Navigation("AdminWatchlistsLastEdited");
+
+                    b.Navigation("AdminWatchlistsReceived");
+
+                    b.Navigation("JobWhitelists");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Navigation("Profiles");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Navigation("Antags");
+
+                    b.Navigation("Jobs");
+
+                    b.Navigation("Loadouts");
+
+                    b.Navigation("Traits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Navigation("Loadouts");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Navigation("Groups");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Navigation("AdminLogs");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Navigation("ConnectionLogs");
+
+                    b.Navigation("Rounds");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Navigation("BanHits");
+
+                    b.Navigation("Unban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Navigation("Unban");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.cs b/Content.Server.Database/Migrations/Sqlite/20260118084622_OrganMarkings.cs
new file mode 100644 (file)
index 0000000..8b1f79f
--- /dev/null
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Sqlite
+{
+    /// <inheritdoc />
+    public partial class OrganMarkings : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<byte[]>(
+                name: "organ_markings",
+                table: "profile",
+                type: "jsonb",
+                nullable: true);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "organ_markings",
+                table: "profile");
+        }
+    }
+}
index 584c96efbc1ddba532c550b212b97db64a35f6c4..2d2df5e595d9849cda2388d3ac590fa44b71a875 100644 (file)
@@ -15,7 +15,7 @@ namespace Content.Server.Database.Migrations.Sqlite
         protected override void BuildModel(ModelBuilder modelBuilder)
         {
 #pragma warning disable 612, 618
-            modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
+            modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
 
             modelBuilder.Entity("Content.Server.Database.Admin", b =>
                 {
@@ -826,6 +826,10 @@ namespace Content.Server.Database.Migrations.Sqlite
                         .HasColumnType("jsonb")
                         .HasColumnName("markings");
 
+                    b.Property<byte[]>("OrganMarkings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("organ_markings");
+
                     b.Property<int>("PreferenceId")
                         .HasColumnType("INTEGER")
                         .HasColumnName("preference_id");
index 8757b19680181928876641883cb1ea078b544adb..ac5b003a737d0a21aea9fe55f4f9c9801396bf12 100644 (file)
@@ -406,6 +406,7 @@ namespace Content.Server.Database
         public string Sex { get; set; } = null!;
         public string Gender { get; set; } = null!;
         public string Species { get; set; } = null!;
+        [Column(TypeName = "jsonb")] public JsonDocument? OrganMarkings { get; set; } = null!;
         [Column(TypeName = "jsonb")] public JsonDocument? Markings { get; set; } = null!;
         public string HairName { get; set; } = null!;
         public string HairColor { get; set; } = null!;
index 5a993bdbfa9878faddef53033227f0b56d128b7d..33bc2bcd729757a85e45895c70dfb4b815cf571c 100644 (file)
@@ -85,6 +85,10 @@ namespace Content.Server.Database
                 .Property(log => log.Markings)
                 .HasConversion(jsonByteArrayConverter);
 
+            modelBuilder.Entity<Profile>()
+                .Property(log => log.OrganMarkings)
+                .HasConversion(jsonByteArrayConverter);
+
             // EF core can make this automatically unique on sqlite but not psql.
             modelBuilder.Entity<IPIntelCache>()
                 .HasIndex(p => p.Address)
index 786a4294bf560395091b59f3ed3c46129d1b4cce..905b2492cd984eef458129a8b3c545dcf7d47d61 100644 (file)
@@ -223,7 +223,7 @@ public sealed partial class AdminVerbSystem
         };
         args.Verbs.Add(ninja);
 
-        if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
+        if (HasComp<HumanoidProfileComponent>(args.Target)) // only humanoids can be cloned
             args.Verbs.Add(paradox);
     }
 }
index 367885a04a5ab03b58db1b0e7184cf07bb540682..5c94942c9ae2d266dc788dc1d5bc900cc55f975c 100644 (file)
@@ -585,7 +585,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
         if (_arrivals.IsOnArrivals((entity.Value, null)))
             return false;
 
-        if (!def.AllowNonHumans && !HasComp<HumanoidAppearanceComponent>(entity))
+        if (!def.AllowNonHumans && !HasComp<HumanoidProfileComponent>(entity))
             return false;
 
         if (def.Whitelist != null)
diff --git a/Content.Server/Body/VisualBodySystem.cs b/Content.Server/Body/VisualBodySystem.cs
new file mode 100644 (file)
index 0000000..4cb1f91
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Body;
+
+namespace Content.Server.Body;
+
+public sealed partial class VisualBodySystem : SharedVisualBodySystem;
index 4975a0b097c1c8a19d377ed282519673b8645c33..ed0e3c85d1f1bc4ff383112cf9b5c8c9647061f4 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Server.Humanoid;
 using Content.Shared.Administration.Logs;
+using Content.Shared.Body;
 using Content.Shared.Cloning;
 using Content.Shared.Cloning.Events;
 using Content.Shared.Database;
@@ -27,7 +28,6 @@ namespace Content.Server.Cloning;
 /// </summary>
 public sealed partial class CloningSystem : SharedCloningSystem
 {
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
     [Dependency] private readonly InventorySystem _inventory = default!;
     [Dependency] private readonly MetaDataSystem _metaData = default!;
     [Dependency] private readonly IPrototypeManager _prototype = default!;
@@ -36,6 +36,7 @@ public sealed partial class CloningSystem : SharedCloningSystem
     [Dependency] private readonly SharedContainerSystem _container = default!;
     [Dependency] private readonly SharedStorageSystem _storage = default!;
     [Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
     [Dependency] private readonly NameModifierSystem _nameMod = default!;
     [Dependency] private readonly Shared.StatusEffectNew.StatusEffectsSystem _statusEffects = default!; //TODO: This system has to support both the old and new status effect systems, until the old is able to be fully removed.
 
@@ -48,7 +49,7 @@ public sealed partial class CloningSystem : SharedCloningSystem
         if (!_prototype.Resolve(settingsId, out var settings))
             return false; // invalid settings
 
-        if (!TryComp<HumanoidAppearanceComponent>(original, out var humanoid))
+        if (!TryComp<HumanoidProfileComponent>(original, out var humanoid))
             return false; // whatever body was to be cloned, was not a humanoid
 
         if (!_prototype.Resolve(humanoid.Species, out var speciesPrototype))
@@ -60,7 +61,7 @@ public sealed partial class CloningSystem : SharedCloningSystem
             return false; // cannot clone, for example due to the unrevivable trait
 
         clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
-        _humanoidSystem.CloneAppearance(original, clone.Value);
+        _visualBody.CopyAppearanceFrom(original, clone.Value);
 
         CloneComponents(original, clone.Value, settings);
 
index c02a4f1a3bfa162e3d8d9dedcf5c30e65f258f9b..19d6e3d3f6e32e72b6a4318c79c671fa1e81b77b 100644 (file)
@@ -90,8 +90,8 @@ public sealed class OutfitSystem : EntitySystem
                 break;
 
             // Don't require a player, so this works on Urists
-            profile ??= EntityManager.TryGetComponent<HumanoidAppearanceComponent>(target, out var comp)
-                ? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species)
+            profile ??= EntityManager.TryGetComponent<HumanoidProfileComponent>(target, out var comp)
+                ? HumanoidCharacterProfile.DefaultWithSpecies(comp.Species, comp.Sex)
                 : new HumanoidCharacterProfile();
             // Try to get the user's existing loadout for the role
             profile.Loadouts.TryGetValue(jobProtoId, out var roleLoadout);
diff --git a/Content.Server/Database/DataNodeJsonExtensions.cs b/Content.Server/Database/DataNodeJsonExtensions.cs
new file mode 100644 (file)
index 0000000..975c8c5
--- /dev/null
@@ -0,0 +1,61 @@
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Robust.Shared.Serialization.Markdown;
+using Robust.Shared.Serialization.Markdown.Mapping;
+using Robust.Shared.Serialization.Markdown.Sequence;
+using Robust.Shared.Serialization.Markdown.Value;
+using YamlDotNet.RepresentationModel;
+
+namespace Content.Server.Database;
+
+public static class DataNodeJsonExtensions
+{
+    private static JsonNode ToJsonNode(this MappingDataNode node)
+    {
+        return new JsonObject(node.Children.Select(kvp => new KeyValuePair<string, JsonNode?>(kvp.Key, kvp.Value.ToJsonNode())));
+    }
+
+    private static JsonNode ToJsonNode(this SequenceDataNode node)
+    {
+        return new JsonArray(node.Select(ToJsonNode).ToArray());
+    }
+
+       public static JsonNode? ToJsonNode(this DataNode node)
+       {
+        return node switch
+        {
+            ValueDataNode valueDataNode => JsonValue.Create(valueDataNode.IsNull ? null : valueDataNode.Value),
+            MappingDataNode mappingDataNode => mappingDataNode.ToJsonNode(),
+            SequenceDataNode sequenceNode => sequenceNode.ToJsonNode(),
+            _ => throw new ArgumentOutOfRangeException(nameof(node))
+        };
+       }
+
+    public static DataNode ToDataNode(this JsonElement element)
+    {
+        return element.ValueKind switch
+        {
+            JsonValueKind.Object => new MappingDataNode(element.EnumerateObject().ToDictionary(kvp => kvp.Name, kvp => kvp.Value.ToDataNode())),
+            JsonValueKind.Array => new SequenceDataNode(element.EnumerateArray().Select(item => item.ToDataNode()).ToList()),
+            JsonValueKind.Number => new ValueDataNode(element.GetRawText()),
+            JsonValueKind.String => new ValueDataNode(element.GetString()),
+            JsonValueKind.True => new ValueDataNode("true"),
+            JsonValueKind.False => new ValueDataNode("false"),
+            JsonValueKind.Null => new ValueDataNode("null"),
+            _ => throw new ArgumentOutOfRangeException(nameof(element)),
+        };
+    }
+
+    public static DataNode ToDataNode(this JsonNode? node)
+    {
+        return node switch
+        {
+            null => ValueDataNode.Null(),
+            JsonValue value => new ValueDataNode(value.GetValue<string>()),
+            JsonArray array => new SequenceDataNode(array.Select(item => item.ToDataNode()).ToList()),
+            JsonObject obj => new MappingDataNode(obj.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToDataNode())),
+            _ => throw new ArgumentOutOfRangeException(nameof(node))
+        };
+    }
+}
index b1e55978949ae3654083073070d0ce60de89c895..00ad726d503706916a137b5d28d48b0662fb1450 100644 (file)
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
 using Content.Server.Administration.Logs;
 using Content.Server.Administration.Managers;
 using Content.Shared.Administration.Logs;
+using Content.Shared.Body;
 using Content.Shared.Construction.Prototypes;
 using Content.Shared.Database;
 using Content.Shared.Humanoid;
@@ -18,9 +19,11 @@ using Content.Shared.Preferences.Loadouts;
 using Content.Shared.Roles;
 using Content.Shared.Traits;
 using Microsoft.EntityFrameworkCore;
+using Robust.Shared.Asynchronous;
 using Robust.Shared.Enums;
 using Robust.Shared.Network;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
 using Robust.Shared.Utility;
 
 namespace Content.Server.Database
@@ -29,10 +32,14 @@ namespace Content.Server.Database
     {
         private readonly ISawmill _opsLog;
         public event Action<DatabaseNotification>? OnNotificationReceived;
+        private readonly ITaskManager _task;
+        private readonly ISerializationManager _serialization;
 
         /// <param name="opsLog">Sawmill to trace log database operations to.</param>
-        public ServerDbBase(ISawmill opsLog)
+        public ServerDbBase(ISawmill opsLog, ITaskManager taskManager, ISerializationManager serialization)
         {
+            _task = taskManager;
+            _serialization = serialization;
             _opsLog = opsLog;
         }
 
@@ -62,7 +69,7 @@ namespace Content.Server.Database
             var profiles = new Dictionary<int, ICharacterProfile>(maxSlot);
             foreach (var profile in prefs.Profiles)
             {
-                profiles[profile.Slot] = ConvertProfiles(profile);
+                profiles[profile.Slot] = await ConvertProfiles(profile);
             }
 
             var constructionFavorites = new List<ProtoId<ConstructionPrototype>>(prefs.ConstructionFavorites.Count);
@@ -202,8 +209,21 @@ namespace Content.Server.Database
             prefs.SelectedCharacterSlot = newSlot;
         }
 
-        private static HumanoidCharacterProfile ConvertProfiles(Profile profile)
+        private static TValue? TryDeserialize<TValue>(JsonDocument document) where TValue : class
         {
+            try
+            {
+                return document.Deserialize<TValue>();
+            }
+            catch (JsonException exception)
+            {
+                return null;
+            }
+        }
+
+        private async Task<HumanoidCharacterProfile> ConvertProfiles(Profile profile)
+        {
+
             var jobs = profile.Jobs.ToDictionary(j => new ProtoId<JobPrototype>(j.JobName), j => (JobPriority) j.Priority);
             var antags = profile.Antags.Select(a => new ProtoId<AntagPrototype>(a.AntagName));
             var traits = profile.Traits.Select(t => new ProtoId<TraitPrototype>(t.TraitName));
@@ -218,20 +238,53 @@ namespace Content.Server.Database
             if (Enum.TryParse<Gender>(profile.Gender, true, out var genderVal))
                 gender = genderVal;
 
-            // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
-            var markingsRaw = profile.Markings?.Deserialize<List<string>>();
 
-            List<Marking> markings = new();
-            if (markingsRaw != null)
+            var markings =
+                new Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>();
+
+            if (profile.OrganMarkings?.RootElement is { } element)
             {
+                var data = element.ToDataNode();
+                markings = _serialization
+                    .Read<Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>>(
+                        data,
+                        notNullableOverride: true);
+            }
+            else if (profile.Markings is { } profileMarkings && TryDeserialize<List<string>>(profileMarkings) is { } markingsRaw)
+            {
+                List<Marking> markingsList = new();
+
                 foreach (var marking in markingsRaw)
                 {
                     var parsed = Marking.ParseFromDbString(marking);
 
                     if (parsed is null) continue;
 
-                    markings.Add(parsed);
+                    markingsList.Add(parsed);
                 }
+
+                if (Marking.ParseFromDbString($"{profile.HairName}@{profile.HairColor}") is { } facialMarking)
+                    markingsList.Add(facialMarking);
+
+                if (Marking.ParseFromDbString($"{profile.HairName}@{profile.HairColor}") is { } hairMarking)
+                    markingsList.Add(hairMarking);
+
+                var completion = new TaskCompletionSource();
+                _task.RunOnMainThread(() =>
+                {
+                    var markingManager = IoCManager.Resolve<MarkingManager>();
+
+                    try
+                    {
+                        markings = markingManager.ConvertMarkings(markingsList, profile.Species);
+                        completion.SetResult();
+                    }
+                    catch (Exception ex)
+                    {
+                        completion.TrySetException(ex);
+                    }
+                });
+                await completion.Task;
             }
 
             var loadouts = new Dictionary<string, RoleLoadout>();
@@ -267,10 +320,6 @@ namespace Content.Server.Database
                 gender,
                 new HumanoidCharacterAppearance
                 (
-                    profile.HairName,
-                    Color.FromHex(profile.HairColor),
-                    profile.FacialHairName,
-                    Color.FromHex(profile.FacialHairColor),
                     Color.FromHex(profile.EyeColor),
                     Color.FromHex(profile.SkinColor),
                     markings
@@ -284,16 +333,11 @@ namespace Content.Server.Database
             );
         }
 
-        private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot, Profile? profile = null)
+        private Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot, Profile? profile = null)
         {
             profile ??= new Profile();
             var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance;
-            List<string> markingStrings = new();
-            foreach (var marking in appearance.Markings)
-            {
-                markingStrings.Add(marking.ToString());
-            }
-            var markings = JsonSerializer.SerializeToDocument(markingStrings);
+            var dataNode = _serialization.WriteValue(appearance.Markings, alwaysWrite: true, notNullableOverride: true);
 
             profile.CharacterName = humanoid.Name;
             profile.FlavorText = humanoid.FlavorText;
@@ -301,14 +345,28 @@ namespace Content.Server.Database
             profile.Age = humanoid.Age;
             profile.Sex = humanoid.Sex.ToString();
             profile.Gender = humanoid.Gender.ToString();
-            profile.HairName = appearance.HairStyleId;
-            profile.HairColor = appearance.HairColor.ToHex();
-            profile.FacialHairName = appearance.FacialHairStyleId;
-            profile.FacialHairColor = appearance.FacialHairColor.ToHex();
             profile.EyeColor = appearance.EyeColor.ToHex();
             profile.SkinColor = appearance.SkinColor.ToHex();
             profile.SpawnPriority = (int) humanoid.SpawnPriority;
-            profile.Markings = markings;
+            profile.OrganMarkings = JsonSerializer.SerializeToDocument(dataNode.ToJsonNode());
+
+            // support for downgrades - at some point this should be removed
+            var legacyMarkings = appearance.Markings
+                .SelectMany(organ => organ.Value.Values)
+                .SelectMany(i => i)
+                .Select(marking => marking.ToString())
+                .ToList();
+            var flattenedMarkings = appearance.Markings.SelectMany(it => it.Value)
+                .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+            var hairMarking = flattenedMarkings.FirstOrNull(kvp => kvp.Key == HumanoidVisualLayers.Hair)?.Value.FirstOrDefault();
+            var facialHairMarking = flattenedMarkings.FirstOrNull(kvp => kvp.Key == HumanoidVisualLayers.FacialHair)?.Value.FirstOrDefault();
+            profile.Markings =
+                JsonSerializer.SerializeToDocument(legacyMarkings.Select(marking => marking.ToString()).ToList());
+            profile.HairName = hairMarking?.MarkingId ?? HairStyles.DefaultHairStyle;
+            profile.FacialHairName = facialHairMarking?.MarkingId ?? HairStyles.DefaultFacialHairStyle;
+            profile.HairColor = (hairMarking?.MarkingColors[0] ?? Color.Black).ToHex();
+            profile.FacialHairColor = (facialHairMarking?.MarkingColors[0] ?? Color.Black).ToHex();
+
             profile.Slot = slot;
             profile.PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable;
 
index e36c484bdec6a5ade4784821ca8be780b6a125f1..5110227b967693bc5dd260dff5666a57ad3d3593 100644 (file)
@@ -16,10 +16,12 @@ using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
 using Npgsql;
 using Prometheus;
+using Robust.Shared.Asynchronous;
 using Robust.Shared.Configuration;
 using Robust.Shared.ContentPack;
 using Robust.Shared.Network;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
 using LogLevel = Robust.Shared.Log.LogLevel;
 using MSLogLevel = Microsoft.Extensions.Logging.LogLevel;
 
@@ -31,6 +33,8 @@ namespace Content.Server.Database
 
         void Shutdown();
 
+        Task<bool> HasPendingModelChanges();
+
         #region Preferences
         Task<PlayerPreferences> InitPrefsAsync(
             NetUserId userId,
@@ -407,6 +411,8 @@ namespace Content.Server.Database
         [Dependency] private readonly IConfigurationManager _cfg = default!;
         [Dependency] private readonly IResourceManager _res = default!;
         [Dependency] private readonly ILogManager _logMgr = default!;
+        [Dependency] private readonly ITaskManager _task = default!;
+        [Dependency] private readonly ISerializationManager _serialization = default!;
 
         private ServerDbBase _db = default!;
         private LoggingProvider _msLogProvider = default!;
@@ -438,11 +444,11 @@ namespace Content.Server.Database
             {
                 case "sqlite":
                     SetupSqlite(out var contextFunc, out var inMemory);
-                    _db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog);
+                    _db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog, _task, _serialization);
                     break;
                 case "postgres":
                     var (pgOptions, conString) = CreatePostgresOptions();
-                    _db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog);
+                    _db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog, _task, _serialization);
                     break;
                 default:
                     throw new InvalidDataException($"Unknown database engine {engine}.");
@@ -1082,6 +1088,11 @@ namespace Content.Server.Database
             }
         }
 
+        public Task<bool> HasPendingModelChanges()
+        {
+            return RunDbCommand(() => _db.HasPendingModelChanges());
+        }
+
         // Wrapper functions to run DB commands from the thread pool.
         // This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
         // For SQLite, this will also enable read parallelization (within limits).
index c0346708377e170afcf68889bea6b79e5e21d606..31584a8d74aff2aeaf4a86691667f93f7b4056eb 100644 (file)
@@ -11,8 +11,10 @@ using Content.Server.IP;
 using Content.Shared.CCVar;
 using Content.Shared.Database;
 using Microsoft.EntityFrameworkCore;
+using Robust.Shared.Asynchronous;
 using Robust.Shared.Configuration;
 using Robust.Shared.Network;
+using Robust.Shared.Serialization.Manager;
 using Robust.Shared.Utility;
 
 namespace Content.Server.Database
@@ -30,8 +32,10 @@ namespace Content.Server.Database
             string connectionString,
             IConfigurationManager cfg,
             ISawmill opsLog,
-            ISawmill notifyLog)
-            : base(opsLog)
+            ISawmill notifyLog,
+            ITaskManager taskManager,
+            ISerializationManager serialization)
+            : base(opsLog, taskManager, serialization)
         {
             var concurrency = cfg.GetCVar(CCVars.DatabasePgConcurrency);
 
index c3109ec6e6692b6189994be429c562ecd752328c..3e69ece7f17b0d228b13894ee3b83e7cc4e43632 100644 (file)
@@ -11,8 +11,10 @@ using Content.Server.Preferences.Managers;
 using Content.Shared.CCVar;
 using Content.Shared.Database;
 using Microsoft.EntityFrameworkCore;
+using Robust.Shared.Asynchronous;
 using Robust.Shared.Configuration;
 using Robust.Shared.Network;
+using Robust.Shared.Serialization.Manager;
 using Robust.Shared.Utility;
 
 namespace Content.Server.Database
@@ -36,8 +38,10 @@ namespace Content.Server.Database
             bool inMemory,
             IConfigurationManager cfg,
             bool synchronous,
-            ISawmill opsLog)
-            : base(opsLog)
+            ISawmill opsLog,
+            ITaskManager taskManager,
+            ISerializationManager serialization)
+            : base(opsLog, taskManager, serialization)
         {
             _options = options;
 
index 485ecdd9543b78c8f8f63b2982d351ad3322d2d6..27e01c00292395e9ab66dccd4f1d8ae6158bcd13 100644 (file)
@@ -78,7 +78,7 @@ namespace Content.Server.Destructible
                     }));
 
                     // If it doesn't have a humanoid component, it's probably not particularly notable?
-                    if (logImpact > LogImpact.Medium && !HasComp<HumanoidAppearanceComponent>(uid))
+                    if (logImpact > LogImpact.Medium && !HasComp<HumanoidProfileComponent>(uid))
                         logImpact = LogImpact.Medium;
 
                     if (args.Origin != null)
index 007f0d35979ca182203de3f59dcee272d50d1456..5f99123db3902ffc036a4f2d064114f4f7a12f38 100644 (file)
@@ -201,7 +201,7 @@ namespace Content.Server.GameTicking
                     }
 
                     speciesId = roundStart.Count == 0
-                        ? SharedHumanoidAppearanceSystem.DefaultSpecies
+                        ? HumanoidCharacterProfile.DefaultSpecies
                         : _robustRandom.Pick(roundStart);
                 }
                 else
@@ -211,6 +211,7 @@ namespace Content.Server.GameTicking
                 }
 
                 character = HumanoidCharacterProfile.RandomWithSpecies(speciesId);
+                character.Appearance = HumanoidCharacterAppearance.EnsureValid(character.Appearance, character.Species, character.Sex);
             }
 
             // We raise this event to allow other systems to handle spawning this player themselves. (e.g. late-join wizard, etc)
index 84f87a487bdeab5c55b65a22ee77a5528a922a8f..22916f0c18e82b2f70e7aa3aa3c6a277e1a08d84 100644 (file)
@@ -2,6 +2,7 @@ using Content.Server.Antag;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Humanoid;
 using Content.Server.Preferences.Managers;
+using Content.Shared.Body;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Preferences;
@@ -11,9 +12,10 @@ namespace Content.Server.GameTicking.Rules;
 
 public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfileRuleComponent>
 {
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
+    [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
     [Dependency] private readonly IPrototypeManager _proto = default!;
     [Dependency] private readonly IServerPreferencesManager _prefs = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
 
     public override void Initialize()
     {
@@ -34,7 +36,7 @@ public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfile
 
         if (profile?.Species is not { } speciesId || !_proto.Resolve(speciesId, out var species))
         {
-            species = _proto.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies);
+            species = _proto.Index<SpeciesPrototype>(HumanoidCharacterProfile.DefaultSpecies);
         }
 
         if (ent.Comp.SpeciesOverride != null
@@ -44,6 +46,10 @@ public sealed class AntagLoadProfileRuleSystem : GameRuleSystem<AntagLoadProfile
         }
 
         args.Entity = Spawn(species.Prototype);
-        _humanoid.LoadProfile(args.Entity.Value, profile?.WithSpecies(species.ID));
+        if (profile?.WithSpecies(species.ID) is { } humanoidProfile)
+        {
+            _visualBody.ApplyProfileTo(args.Entity.Value, humanoidProfile);
+            _humanoidProfile.ApplyProfileTo(args.Entity.Value, humanoidProfile);
+        }
     }
 }
index d2cd27022fb1dfe679941c81c6d91c860b1a7dbc..cf74513c874f2e46f12de411d3728642b2808678 100644 (file)
@@ -143,7 +143,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
 
         if (HasComp<RevolutionaryComponent>(ev.Target) ||
             HasComp<MindShieldComponent>(ev.Target) ||
-            !HasComp<HumanoidAppearanceComponent>(ev.Target) &&
+            !HasComp<HumanoidProfileComponent>(ev.Target) &&
             !alwaysConvertible ||
             !_mobState.IsAlive(ev.Target) ||
             HasComp<ZombieComponent>(ev.Target))
index 75bdd5387b3db8faa502d42485de01200d1676c0..00ad6e8934dd555c7999138e89534077ed1e49b6 100644 (file)
@@ -38,7 +38,7 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
 
     private string MakeBriefing(EntityUid ent)
     {
-        var isHuman = HasComp<HumanoidAppearanceComponent>(ent);
+        var isHuman = HasComp<HumanoidProfileComponent>(ent);
         var briefing = isHuman
             ? Loc.GetString("thief-role-greeting-human")
             : Loc.GetString("thief-role-greeting-animal");
index 11f8d756a042596a0fb3fe16be3f7078b2226ac8..334f751a2aa7fbcfcf06f52d719c7fd735e44287 100644 (file)
@@ -166,7 +166,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
     {
         var players = GetHealthyHumans(includeOffStation);
         var zombieCount = 0;
-        var query = EntityQueryEnumerator<HumanoidAppearanceComponent, ZombieComponent, MobStateComponent>();
+        var query = EntityQueryEnumerator<HumanoidProfileComponent, ZombieComponent, MobStateComponent>();
         while (query.MoveNext(out _, out _, out _, out var mob))
         {
             if (!includeDead && mob.CurrentState == MobState.Dead)
@@ -196,7 +196,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
             }
         }
 
-        var players = AllEntityQuery<HumanoidAppearanceComponent, ActorComponent, MobStateComponent, TransformComponent>();
+        var players = AllEntityQuery<HumanoidProfileComponent, ActorComponent, MobStateComponent, TransformComponent>();
         var zombers = GetEntityQuery<ZombieComponent>();
         while (players.MoveNext(out var uid, out _, out _, out var mob, out var xform))
         {
index 5efea024f4e3e15b029ef77c3fc33bfcc132c300..a7bb4c51fd2c0cf5fbabc78fc2145ffa315726d1 100644 (file)
@@ -1,14 +1,8 @@
-using Content.Shared.Humanoid.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
-
-namespace Content.Server.CharacterAppearance.Components;
+namespace Content.Server.Humanoid.Components;
 
 [RegisterComponent]
 public sealed partial class RandomHumanoidAppearanceComponent : Component
 {
-    [DataField("randomizeName")] public bool RandomizeName = true;
-    /// <summary>
-    /// After randomizing, sets the hair style to this, if possible
-    /// </summary>
-    [DataField] public string? Hair = null;
+    [DataField]
+    public bool RandomizeName = true;
 }
diff --git a/Content.Server/Humanoid/HideableHumanoidLayersSystem.cs b/Content.Server/Humanoid/HideableHumanoidLayersSystem.cs
new file mode 100644 (file)
index 0000000..2619ca2
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Humanoid;
+
+namespace Content.Server.Humanoid;
+
+public sealed class HideableHumanoidLayersSystem : SharedHideableHumanoidLayersSystem;
diff --git a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.Modifier.cs b/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.Modifier.cs
deleted file mode 100644 (file)
index 7744d16..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-using Content.Server.Administration.Managers;
-using Content.Shared.Administration;
-using Content.Shared.Humanoid;
-using Content.Shared.Verbs;
-using Robust.Server.GameObjects;
-using Robust.Shared.Player;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Humanoid;
-
-public sealed partial class HumanoidAppearanceSystem
-{
-    [Dependency] private readonly IAdminManager _adminManager = default!;
-    [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
-
-    private void OnVerbsRequest(EntityUid uid, HumanoidAppearanceComponent component, GetVerbsEvent<Verb> args)
-    {
-        if (!TryComp<ActorComponent>(args.User, out var actor))
-        {
-            return;
-        }
-
-        if (!_adminManager.HasAdminFlag(actor.PlayerSession, AdminFlags.Fun))
-        {
-            return;
-        }
-
-        args.Verbs.Add(new Verb
-        {
-            Text = "Modify markings",
-            Category = VerbCategory.Tricks,
-            Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Customization/reptilian_parts.rsi"), "tail_smooth"),
-            Act = () =>
-            {
-                _uiSystem.OpenUi(uid, HumanoidMarkingModifierKey.Key, actor.PlayerSession);
-                _uiSystem.SetUiState(
-                    uid,
-                    HumanoidMarkingModifierKey.Key,
-                    new HumanoidMarkingModifierState(component.MarkingSet, component.Species,
-                        component.Sex,
-                        component.SkinColor,
-                        component.CustomBaseLayers
-                    ));
-            }
-        });
-    }
-
-    private void OnBaseLayersSet(EntityUid uid, HumanoidAppearanceComponent component,
-        HumanoidMarkingModifierBaseLayersSetMessage message)
-    {
-        if (!_adminManager.HasAdminFlag(message.Actor, AdminFlags.Fun))
-        {
-            return;
-        }
-
-        if (message.Info == null)
-        {
-            component.CustomBaseLayers.Remove(message.Layer);
-        }
-        else
-        {
-            component.CustomBaseLayers[message.Layer] = message.Info.Value;
-        }
-
-        Dirty(uid, component);
-
-        if (message.ResendState)
-        {
-            _uiSystem.SetUiState(
-                uid,
-                HumanoidMarkingModifierKey.Key,
-                new HumanoidMarkingModifierState(component.MarkingSet, component.Species,
-                        component.Sex,
-                        component.SkinColor,
-                        component.CustomBaseLayers
-                    ));
-        }
-    }
-
-    private void OnMarkingsSet(EntityUid uid, HumanoidAppearanceComponent component,
-        HumanoidMarkingModifierMarkingSetMessage message)
-    {
-        if (!_adminManager.HasAdminFlag(message.Actor, AdminFlags.Fun))
-        {
-            return;
-        }
-
-        component.MarkingSet = message.MarkingSet;
-        Dirty(uid, component);
-
-        if (message.ResendState)
-        {
-            _uiSystem.SetUiState(
-                uid,
-                HumanoidMarkingModifierKey.Key,
-                new HumanoidMarkingModifierState(component.MarkingSet, component.Species,
-                        component.Sex,
-                        component.SkinColor,
-                        component.CustomBaseLayers
-                    ));
-        }
-
-    }
-}
diff --git a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs b/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs
deleted file mode 100644 (file)
index 9e719bd..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Markings;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Preferences;
-using Content.Shared.Verbs;
-using Robust.Shared.GameObjects.Components.Localization;
-
-namespace Content.Server.Humanoid;
-
-public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
-{
-    [Dependency] private readonly MarkingManager _markingManager = default!;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<HumanoidAppearanceComponent, HumanoidMarkingModifierMarkingSetMessage>(OnMarkingsSet);
-        SubscribeLocalEvent<HumanoidAppearanceComponent, HumanoidMarkingModifierBaseLayersSetMessage>(OnBaseLayersSet);
-        SubscribeLocalEvent<HumanoidAppearanceComponent, GetVerbsEvent<Verb>>(OnVerbsRequest);
-    }
-
-    /// <summary>
-    ///     Removes a marking from a humanoid by ID.
-    /// </summary>
-    /// <param name="uid">Humanoid mob's UID</param>
-    /// <param name="marking">The marking to try and remove.</param>
-    /// <param name="sync">Whether to immediately sync this to the humanoid</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void RemoveMarking(EntityUid uid, string marking, bool sync = true, HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (!Resolve(uid, ref humanoid)
-            || !_markingManager.Markings.TryGetValue(marking, out var prototype))
-        {
-            return;
-        }
-
-        humanoid.MarkingSet.Remove(prototype.MarkingCategory, marking);
-
-        if (sync)
-            Dirty(uid, humanoid);
-    }
-
-    /// <summary>
-    ///     Removes a marking from a humanoid by category and index.
-    /// </summary>
-    /// <param name="uid">Humanoid mob's UID</param>
-    /// <param name="category">Category of the marking</param>
-    /// <param name="index">Index of the marking</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void RemoveMarking(EntityUid uid, MarkingCategories category, int index, HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (index < 0
-            || !Resolve(uid, ref humanoid)
-            || !humanoid.MarkingSet.TryGetCategory(category, out var markings)
-            || index >= markings.Count)
-        {
-            return;
-        }
-
-        humanoid.MarkingSet.Remove(category, index);
-        Dirty(uid, humanoid);
-    }
-
-    /// <summary>
-    ///     Sets the marking ID of the humanoid in a category at an index in the category's list.
-    /// </summary>
-    /// <param name="uid">Humanoid mob's UID</param>
-    /// <param name="category">Category of the marking</param>
-    /// <param name="index">Index of the marking</param>
-    /// <param name="markingId">The marking ID to use</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void SetMarkingId(EntityUid uid, MarkingCategories category, int index, string markingId, HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (index < 0
-            || !_markingManager.MarkingsByCategory(category).TryGetValue(markingId, out var markingPrototype)
-            || !Resolve(uid, ref humanoid)
-            || !humanoid.MarkingSet.TryGetCategory(category, out var markings)
-            || index >= markings.Count)
-        {
-            return;
-        }
-
-        var marking = markingPrototype.AsMarking();
-        for (var i = 0; i < marking.MarkingColors.Count && i < markings[index].MarkingColors.Count; i++)
-        {
-            marking.SetColor(i, markings[index].MarkingColors[i]);
-        }
-
-        humanoid.MarkingSet.Replace(category, index, marking);
-        Dirty(uid, humanoid);
-    }
-
-    /// <summary>
-    ///     Sets the marking colors of the humanoid in a category at an index in the category's list.
-    /// </summary>
-    /// <param name="uid">Humanoid mob's UID</param>
-    /// <param name="category">Category of the marking</param>
-    /// <param name="index">Index of the marking</param>
-    /// <param name="colors">The marking colors to use</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void SetMarkingColor(EntityUid uid, MarkingCategories category, int index, List<Color> colors,
-        HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (index < 0
-            || !Resolve(uid, ref humanoid)
-            || !humanoid.MarkingSet.TryGetCategory(category, out var markings)
-            || index >= markings.Count)
-        {
-            return;
-        }
-
-        for (var i = 0; i < markings[index].MarkingColors.Count && i < colors.Count; i++)
-        {
-            markings[index].SetColor(i, colors[i]);
-        }
-
-        Dirty(uid, humanoid);
-    }
-}
index 2822fb69e1441ec18bee844a49eaedbe0fa40bb4..537ae12276560cd6a382574da52ed9d2ad67bf93 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Server.CharacterAppearance.Components;
+using Content.Server.Humanoid.Components;
+using Content.Shared.Body;
 using Content.Shared.Humanoid;
 using Content.Shared.Preferences;
 
@@ -6,8 +7,9 @@ namespace Content.Server.Humanoid.Systems;
 
 public sealed class RandomHumanoidAppearanceSystem : EntitySystem
 {
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
+    [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
     [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
 
     public override void Initialize()
     {
@@ -19,17 +21,13 @@ public sealed class RandomHumanoidAppearanceSystem : EntitySystem
     private void OnMapInit(EntityUid uid, RandomHumanoidAppearanceComponent component, MapInitEvent args)
     {
         // If we have an initial profile/base layer set, do not randomize this humanoid.
-        if (!TryComp(uid, out HumanoidAppearanceComponent? humanoid) || !string.IsNullOrEmpty(humanoid.Initial))
-        {
+        if (!TryComp<HumanoidProfileComponent>(uid, out var humanoid))
             return;
-        }
 
         var profile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
-        //If we have a specified hair style, change it to this
-        if(component.Hair != null)
-            profile = profile.WithCharacterAppearance(profile.Appearance.WithHairStyleName(component.Hair));
 
-        _humanoid.LoadProfile(uid, profile, humanoid);
+        _visualBody.ApplyProfileTo(uid, profile);
+        _humanoidProfile.ApplyProfileTo(uid, profile);
 
         if (component.RandomizeName)
             _metaData.SetEntityName(uid, profile.Name);
index 126416daa906f7a434c4272e6b34bee69ac6a1d8..4ae69d2fd7c249853fc89ddf1992df11827193a0 100644 (file)
@@ -1,6 +1,8 @@
 using Content.Server.Humanoid.Components;
 using Content.Server.RandomMetadata;
+using Content.Shared.Body;
 using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Humanoid;
 using Content.Shared.Preferences;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
@@ -13,11 +15,11 @@ namespace Content.Server.Humanoid.Systems;
 /// </summary>
 public sealed class RandomHumanoidSystem : EntitySystem
 {
+    [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly ISerializationManager _serialization = default!;
     [Dependency] private readonly MetaDataSystem _metaData = default!;
-
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
 
     /// <inheritdoc/>
     public override void Initialize()
@@ -44,8 +46,6 @@ public sealed class RandomHumanoidSystem : EntitySystem
 
         _metaData.SetEntityName(humanoid, prototype.RandomizeName ? profile.Name : name);
 
-        _humanoid.LoadProfile(humanoid, profile);
-
         if (prototype.Components != null)
         {
             foreach (var entry in prototype.Components.Values)
@@ -58,6 +58,9 @@ public sealed class RandomHumanoidSystem : EntitySystem
 
         EntityManager.InitializeAndStartEntity(humanoid);
 
+        _visualBody.ApplyProfileTo(humanoid, profile);
+        _humanoidProfile.ApplyProfileTo(humanoid, profile);
+
         return humanoid;
     }
 }
diff --git a/Content.Server/MagicMirror/MagicMirrorSystem.cs b/Content.Server/MagicMirror/MagicMirrorSystem.cs
deleted file mode 100644 (file)
index dbc258c..0000000
+++ /dev/null
@@ -1,414 +0,0 @@
-using System.Linq;
-using Content.Server.DoAfter;
-using Content.Server.Humanoid;
-using Content.Shared.DoAfter;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Markings;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Interaction;
-using Content.Shared.Inventory;
-using Content.Shared.MagicMirror;
-using Content.Shared.Popups;
-using Content.Shared.Tag;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Prototypes;
-
-namespace Content.Server.MagicMirror;
-
-/// <summary>
-/// Allows humanoids to change their appearance mid-round.
-/// </summary>
-public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
-{
-    [Dependency] private readonly SharedAudioSystem _audio = default!;
-    [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
-    [Dependency] private readonly MarkingManager _markings = default!;
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
-    [Dependency] private readonly SharedPopupSystem _popup = default!;
-    [Dependency] private readonly InventorySystem _inventory = default!;
-    [Dependency] private readonly TagSystem _tagSystem = default!;
-
-    private static readonly ProtoId<TagPrototype> HidesHairTag = "HidesHair";
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        Subs.BuiEvents<MagicMirrorComponent>(MagicMirrorUiKey.Key,
-            subs =>
-        {
-            subs.Event<BoundUIClosedEvent>(OnUiClosed);
-            subs.Event<MagicMirrorSelectMessage>(OnMagicMirrorSelect);
-            subs.Event<MagicMirrorChangeColorMessage>(OnTryMagicMirrorChangeColor);
-            subs.Event<MagicMirrorAddSlotMessage>(OnTryMagicMirrorAddSlot);
-            subs.Event<MagicMirrorRemoveSlotMessage>(OnTryMagicMirrorRemoveSlot);
-        });
-
-
-        SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorSelectDoAfterEvent>(OnSelectSlotDoAfter);
-        SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorChangeColorDoAfterEvent>(OnChangeColorDoAfter);
-        SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorRemoveSlotDoAfterEvent>(OnRemoveSlotDoAfter);
-        SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorAddSlotDoAfterEvent>(OnAddSlotDoAfter);
-    }
-
-    private void OnMagicMirrorSelect(EntityUid uid, MagicMirrorComponent component, MagicMirrorSelectMessage message)
-    {
-        if (component.Target is not { } target)
-            return;
-
-        // Check if the target getting their hair altered has any clothes that hides their hair
-        if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value))
-        {
-            _popup.PopupEntity(
-                component.Target == message.Actor
-                    ? Loc.GetString("magic-mirror-blocked-by-hat-self")
-                    : Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
-                message.Actor,
-                message.Actor,
-                PopupType.Medium);
-            return;
-        }
-
-        _doAfterSystem.Cancel(component.DoAfter);
-        component.DoAfter = null;
-
-        var doafterTime = component.SelectSlotTime;
-        if (component.Target == message.Actor)
-            doafterTime /= 3;
-
-        var doAfter = new MagicMirrorSelectDoAfterEvent()
-        {
-            Category = message.Category,
-            Slot = message.Slot,
-            Marking = message.Marking,
-        };
-
-        _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: target, used: uid)
-        {
-            DistanceThreshold = SharedInteractionSystem.InteractionRange,
-            BreakOnDamage = true,
-            BreakOnMove = true,
-            NeedHand = true,
-        },
-            out var doAfterId);
-
-        if (component.Target == message.Actor)
-        {
-            _popup.PopupEntity(Loc.GetString("magic-mirror-change-slot-self"), component.Target.Value, component.Target.Value, PopupType.Medium);
-        }
-        else
-        {
-            _popup.PopupEntity(Loc.GetString("magic-mirror-change-slot-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium);
-        }
-
-        component.DoAfter = doAfterId;
-        _audio.PlayPvs(component.ChangeHairSound, uid);
-    }
-
-    private void OnSelectSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorSelectDoAfterEvent args)
-    {
-        component.DoAfter = null;
-
-        if (args.Handled || args.Target == null || args.Cancelled)
-            return;
-
-        if (component.Target != args.Target)
-            return;
-
-        MarkingCategories category;
-
-        switch (args.Category)
-        {
-            case MagicMirrorCategory.Hair:
-                category = MarkingCategories.Hair;
-                break;
-            case MagicMirrorCategory.FacialHair:
-                category = MarkingCategories.FacialHair;
-                break;
-            default:
-                return;
-        }
-
-        _humanoid.SetMarkingId(component.Target.Value, category, args.Slot, args.Marking);
-
-        UpdateInterface(uid, component.Target.Value, component);
-    }
-
-    private void OnTryMagicMirrorChangeColor(EntityUid uid, MagicMirrorComponent component, MagicMirrorChangeColorMessage message)
-    {
-        if (component.Target is not { } target)
-            return;
-
-        // Check if the target getting their hair altered has any clothes that hides their hair
-        if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value))
-        {
-            _popup.PopupEntity(
-                component.Target == message.Actor
-                    ? Loc.GetString("magic-mirror-blocked-by-hat-self")
-                    : Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
-                message.Actor,
-                message.Actor,
-                PopupType.Medium);
-            return;
-        }
-
-        _doAfterSystem.Cancel(component.DoAfter);
-        component.DoAfter = null;
-
-        var doafterTime = component.ChangeSlotTime;
-        if (component.Target == message.Actor)
-            doafterTime /= 3;
-
-        var doAfter = new MagicMirrorChangeColorDoAfterEvent()
-        {
-            Category = message.Category,
-            Slot = message.Slot,
-            Colors = message.Colors,
-        };
-
-        _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: target, used: uid)
-        {
-            BreakOnDamage = true,
-            BreakOnMove = true,
-            NeedHand = true
-        },
-            out var doAfterId);
-
-        if (component.Target == message.Actor)
-        {
-            _popup.PopupEntity(Loc.GetString("magic-mirror-change-color-self"), component.Target.Value, component.Target.Value, PopupType.Medium);
-        }
-        else
-        {
-            _popup.PopupEntity(Loc.GetString("magic-mirror-change-color-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium);
-        }
-
-        component.DoAfter = doAfterId;
-    }
-    private void OnChangeColorDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorChangeColorDoAfterEvent args)
-    {
-        component.DoAfter = null;
-
-        if (args.Handled || args.Target == null || args.Cancelled)
-            return;
-
-        if (component.Target != args.Target)
-            return;
-
-        MarkingCategories category;
-        switch (args.Category)
-        {
-            case MagicMirrorCategory.Hair:
-                category = MarkingCategories.Hair;
-                break;
-            case MagicMirrorCategory.FacialHair:
-                category = MarkingCategories.FacialHair;
-                break;
-            default:
-                return;
-        }
-
-        _humanoid.SetMarkingColor(component.Target.Value, category, args.Slot, args.Colors);
-
-        // using this makes the UI feel like total ass
-        // que
-        // UpdateInterface(uid, component.Target, message.Session);
-    }
-
-    private void OnTryMagicMirrorRemoveSlot(EntityUid uid, MagicMirrorComponent component, MagicMirrorRemoveSlotMessage message)
-    {
-        if (component.Target is not { } target)
-            return;
-
-        // Check if the target getting their hair altered has any clothes that hides their hair
-        if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value))
-        {
-            _popup.PopupEntity(
-                component.Target == message.Actor
-                    ? Loc.GetString("magic-mirror-blocked-by-hat-self")
-                    : Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
-                message.Actor,
-                message.Actor,
-                PopupType.Medium);
-            return;
-        }
-
-        _doAfterSystem.Cancel(component.DoAfter);
-        component.DoAfter = null;
-
-        var doafterTime = component.RemoveSlotTime;
-        if (component.Target == message.Actor)
-            doafterTime /= 3;
-
-        var doAfter = new MagicMirrorRemoveSlotDoAfterEvent()
-        {
-            Category = message.Category,
-            Slot = message.Slot,
-        };
-
-        _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: target, used: uid)
-        {
-            DistanceThreshold = SharedInteractionSystem.InteractionRange,
-            BreakOnDamage = true,
-            NeedHand = true
-        },
-            out var doAfterId);
-
-        if (component.Target == message.Actor)
-        {
-            _popup.PopupEntity(Loc.GetString("magic-mirror-remove-slot-self"), component.Target.Value, component.Target.Value, PopupType.Medium);
-        }
-        else
-        {
-            _popup.PopupEntity(Loc.GetString("magic-mirror-remove-slot-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium);
-        }
-
-        component.DoAfter = doAfterId;
-        _audio.PlayPvs(component.ChangeHairSound, uid);
-    }
-
-    private void OnRemoveSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorRemoveSlotDoAfterEvent args)
-    {
-        component.DoAfter = null;
-
-        if (args.Handled || args.Target == null || args.Cancelled)
-            return;
-
-        if (component.Target != args.Target)
-            return;
-
-        MarkingCategories category;
-
-        switch (args.Category)
-        {
-            case MagicMirrorCategory.Hair:
-                category = MarkingCategories.Hair;
-                break;
-            case MagicMirrorCategory.FacialHair:
-                category = MarkingCategories.FacialHair;
-                break;
-            default:
-                return;
-        }
-
-        _humanoid.RemoveMarking(component.Target.Value, category, args.Slot);
-
-        UpdateInterface(uid, component.Target.Value, component);
-    }
-
-    private void OnTryMagicMirrorAddSlot(EntityUid uid, MagicMirrorComponent component, MagicMirrorAddSlotMessage message)
-    {
-        if (component.Target == null)
-            return;
-
-        // Check if the target getting their hair altered has any clothes that hides their hair
-        if (CheckHeadSlotOrClothes(message.Actor, component.Target.Value))
-        {
-            _popup.PopupEntity(
-                component.Target == message.Actor
-                    ? Loc.GetString("magic-mirror-blocked-by-hat-self")
-                    : Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(message.Actor, EntityManager))),
-                message.Actor,
-                message.Actor,
-                PopupType.Medium);
-            return;
-        }
-
-        _doAfterSystem.Cancel(component.DoAfter);
-        component.DoAfter = null;
-
-        var doafterTime = component.AddSlotTime;
-        if (component.Target == message.Actor)
-            doafterTime /= 3;
-
-        var doAfter = new MagicMirrorAddSlotDoAfterEvent()
-        {
-            Category = message.Category,
-        };
-
-        _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, message.Actor, doafterTime, doAfter, uid, target: component.Target.Value, used: uid)
-        {
-            BreakOnDamage = true,
-            BreakOnMove = true,
-            NeedHand = true,
-        },
-            out var doAfterId);
-
-        if (component.Target == message.Actor)
-        {
-            _popup.PopupEntity(Loc.GetString("magic-mirror-add-slot-self"), component.Target.Value, component.Target.Value, PopupType.Medium);
-        }
-        else
-        {
-            _popup.PopupEntity(Loc.GetString("magic-mirror-add-slot-target", ("user", Identity.Entity(message.Actor, EntityManager))), component.Target.Value, component.Target.Value, PopupType.Medium);
-        }
-
-        component.DoAfter = doAfterId;
-        _audio.PlayPvs(component.ChangeHairSound, uid);
-    }
-    private void OnAddSlotDoAfter(EntityUid uid, MagicMirrorComponent component, MagicMirrorAddSlotDoAfterEvent args)
-    {
-        component.DoAfter = null;
-
-        if (args.Handled || args.Target == null || args.Cancelled || !TryComp(component.Target, out HumanoidAppearanceComponent? humanoid))
-            return;
-
-        MarkingCategories category;
-
-        switch (args.Category)
-        {
-            case MagicMirrorCategory.Hair:
-                category = MarkingCategories.Hair;
-                break;
-            case MagicMirrorCategory.FacialHair:
-                category = MarkingCategories.FacialHair;
-                break;
-            default:
-                return;
-        }
-
-        var marking = _markings.MarkingsByCategoryAndSpecies(category, humanoid.Species).Keys.FirstOrDefault();
-
-        if (string.IsNullOrEmpty(marking))
-            return;
-
-        _humanoid.AddMarking(component.Target.Value, marking, Color.Black);
-
-        UpdateInterface(uid, component.Target.Value, component);
-
-    }
-
-    private void OnUiClosed(Entity<MagicMirrorComponent> ent, ref BoundUIClosedEvent args)
-    {
-        ent.Comp.Target = null;
-        Dirty(ent);
-    }
-
-    /// <summary>
-    /// Helper function that checks if the wearer has anything on their head
-    /// Or if they have any clothes that hides their hair
-    /// </summary>
-    private bool CheckHeadSlotOrClothes(EntityUid user, EntityUid target)
-    {
-        if (TryComp<InventoryComponent>(target, out var inventoryComp))
-        {
-            // any hat whatsoever will block haircutting
-            if (_inventory.TryGetSlotEntity(target, "head", out var hat, inventoryComp))
-            {
-                return true;
-            }
-
-            // maybe there's some kind of armor that has the HidesHair tag as well, so check every slot for it
-            var slots = _inventory.GetSlotEnumerator((target, inventoryComp), SlotFlags.WITHOUT_POCKET);
-            while (slots.MoveNext(out var slot))
-            {
-                if (slot.ContainedEntity != null && _tagSystem.HasTag(slot.ContainedEntity.Value, HidesHairTag))
-                {
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-}
index 7af39b84a1c5c30f711aca8d43729117ca2f7ce2..c00c938865f6d95f1a8a6a827e5d2416f4093031 100644 (file)
@@ -189,7 +189,7 @@ public sealed class MaterialReclaimerSystem : SharedMaterialReclaimerSystem
 
         if (CanGib(uid, item, component))
         {
-            var logImpact = HasComp<HumanoidAppearanceComponent>(item) ? LogImpact.Extreme : LogImpact.Medium;
+            var logImpact = HasComp<HumanoidProfileComponent>(item) ? LogImpact.Extreme : LogImpact.Medium;
             _adminLogger.Add(LogType.Gib, logImpact, $"{ToPrettyString(item):victim} was gibbed by {ToPrettyString(uid):entity} ");
             if (component.ReclaimSolutions)
                 SpawnChemicalsFromComposition(uid, item, completion, false, component, xform);
index 83dcd9cf7c836c3ad71c7e0deac5ecc65527e1eb..f4b7ec838aff7f73b0361c8f9ebc28e4b60a0586 100644 (file)
@@ -254,7 +254,7 @@ namespace Content.Server.Medical.BiomassReclaimer
 
             // Reject souled bodies in easy mode.
             if (_configManager.GetCVar(CCVars.BiomassEasyMode) &&
-                HasComp<HumanoidAppearanceComponent>(dragged) &&
+                HasComp<HumanoidProfileComponent>(dragged) &&
                 _minds.TryGetMind(dragged, out _, out var mind))
             {
                 if (mind.UserId != null && _playerManager.TryGetSessionById(mind.UserId.Value, out _))
index 9e0d2c3d5b513b173f091d8753fe9947de142488..063d5199262c05a54d625c35d73d21075c90dcd1 100644 (file)
@@ -61,7 +61,7 @@ public sealed class HijackShuttleConditionSystem : EntitySystem
     private bool IsShuttleHijacked(EntityUid shuttleGridId, EntityUid mindId)
     {
         var gridPlayers = Filter.BroadcastGrid(shuttleGridId).Recipients;
-        var humanoids = GetEntityQuery<HumanoidAppearanceComponent>();
+        var humanoids = GetEntityQuery<HumanoidProfileComponent>();
         var cuffable = GetEntityQuery<CuffableComponent>();
         EntityQuery<MobStateComponent>();
 
index ab2d1d4370309ca04d52e08753d7372220d2eb82..1a3826d662a04958c6747f996e16ce1ec07086e0 100644 (file)
@@ -21,7 +21,7 @@ public sealed class SpeciesRequirementSystem : EntitySystem
         if (args.Cancelled)
             return;
 
-        if (!TryComp<HumanoidAppearanceComponent>(args.Mind.OwnedEntity, out var appearance)) {
+        if (!TryComp<HumanoidProfileComponent>(args.Mind.OwnedEntity, out var appearance)) {
             args.Cancelled = true;
             return;
         }
index 897ad720471fe68657c8c6f271e912a290fdc729..3df9b07d12c73bb773abe12db85e6503ef8571d2 100644 (file)
@@ -2,6 +2,7 @@ using Content.Server.Actions;
 using Content.Server.Humanoid;
 using Content.Server.Inventory;
 using Content.Server.Polymorph.Components;
+using Content.Shared.Body;
 using Content.Shared.Buckle;
 using Content.Shared.Coordinates;
 using Content.Shared.Damage.Components;
@@ -34,13 +35,13 @@ public sealed partial class PolymorphSystem : EntitySystem
     [Dependency] private readonly SharedBuckleSystem _buckle = default!;
     [Dependency] private readonly ContainerSystem _container = default!;
     [Dependency] private readonly DamageableSystem _damageable = default!;
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
     [Dependency] private readonly MobStateSystem _mobState = default!;
     [Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
     [Dependency] private readonly ServerInventorySystem _inventory = default!;
     [Dependency] private readonly SharedHandsSystem _hands = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
     [Dependency] private readonly TransformSystem _transform = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
     [Dependency] private readonly SharedMindSystem _mindSystem = default!;
     [Dependency] private readonly MetaDataSystem _metaData = default!;
 
@@ -262,7 +263,7 @@ public sealed partial class PolymorphSystem : EntitySystem
 
         if (configuration.TransferHumanoidAppearance)
         {
-            _humanoid.CloneAppearance(uid, child);
+            _visualBody.CopyAppearanceFrom(uid, child);
         }
 
         if (_mindSystem.TryGetMind(uid, out var mindId, out var mind))
index 82e6f026e63caf1106f9177a121da3f4f9ca11e5..6bf94327992ebc10021ba493cf15a44349549a75 100644 (file)
@@ -76,7 +76,7 @@ public sealed partial class RevenantSystem
             return;
         }
 
-        if (!HasComp<MobStateComponent>(target) || !HasComp<HumanoidAppearanceComponent>(target) || HasComp<RevenantComponent>(target))
+        if (!HasComp<MobStateComponent>(target) || !HasComp<HumanoidProfileComponent>(target) || HasComp<RevenantComponent>(target))
             return;
 
         args.Handled = true;
index ceee9c378445c7fdc53be2826cc576de36a60da2..e838ceb08ec4b888e0a166f24bb643cadd21e703 100644 (file)
@@ -40,7 +40,7 @@ public sealed partial class SalvageSystem
         }
 
         // TODO: This is terrible but need bluespace harnesses or something.
-        var query = EntityQueryEnumerator<HumanoidAppearanceComponent, MobStateComponent, TransformComponent>();
+        var query = EntityQueryEnumerator<HumanoidProfileComponent, MobStateComponent, TransformComponent>();
 
         while (query.MoveNext(out var uid, out _, out var mobState, out var mobXform))
         {
index 2c831088980cc681ae9b1a896e5bab387760bf60..3709ab5e3be6a7dccbe2993bc2b9d9194894b994 100644 (file)
@@ -117,7 +117,7 @@ public sealed class VocalSystem : EntitySystem
         if (component.Sounds == null)
             return;
 
-        sex ??= CompOrNull<HumanoidAppearanceComponent>(uid)?.Sex ?? Sex.Unsexed;
+        sex ??= CompOrNull<HumanoidProfileComponent>(uid)?.Sex ?? Sex.Unsexed;
 
         if (!component.Sounds.TryGetValue(sex.Value, out var protoId))
             return;
index ba9487b031f5e6feb1a92a734af7752a345dd1cd..0c89fa8d87afc517218874676d36b90c37b9f487 100644 (file)
@@ -5,6 +5,7 @@ using Content.Server.PDA;
 using Content.Server.Station.Components;
 using Content.Shared.Access.Components;
 using Content.Shared.Access.Systems;
+using Content.Shared.Body;
 using Content.Shared.CCVar;
 using Content.Shared.Clothing;
 using Content.Shared.DetailExaminable;
@@ -36,7 +37,8 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
     [Dependency] private readonly ActorSystem _actors = default!;
     [Dependency] private readonly IdCardSystem _cardSystem = default!;
     [Dependency] private readonly IConfigurationManager _configurationManager = default!;
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
+    [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
     [Dependency] private readonly IdentitySystem _identity = default!;
     [Dependency] private readonly MetaDataSystem _metaSystem = default!;
     [Dependency] private readonly PdaSystem _pdaSystem = default!;
@@ -124,7 +126,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
             return jobEntity;
         }
 
-        string speciesId = profile != null ? profile.Species : SharedHumanoidAppearanceSystem.DefaultSpecies;
+        string speciesId = profile != null ? profile.Species : HumanoidCharacterProfile.DefaultSpecies;
 
         if (!_prototypeManager.TryIndex<SpeciesPrototype>(speciesId, out var species))
             throw new ArgumentException($"Invalid species prototype was used: {speciesId}");
@@ -133,7 +135,8 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
 
         if (profile != null)
         {
-            _humanoidSystem.LoadProfile(entity.Value, profile);
+            _visualBody.ApplyProfileTo(entity.Value, profile);
+            _humanoidProfile.ApplyProfileTo(entity.Value, profile);
             _metaSystem.SetEntityName(entity.Value, profile.Name);
 
             if (profile.FlavorText != "" && _configurationManager.GetCVar(CCVars.FlavorText))
index b03231b5bcd71eef0c45dc8ae191edc3a1bd8729..ef1bd696e9143a3e08f5129cb5473d4b1b1253b3 100644 (file)
@@ -17,7 +17,7 @@ public sealed class MassHallucinationsRule : StationEventSystem<MassHallucinatio
     {
         base.Started(uid, component, gameRule, args);
 
-        var query = EntityQueryEnumerator<MindContainerComponent, HumanoidAppearanceComponent>();
+        var query = EntityQueryEnumerator<MindContainerComponent, HumanoidProfileComponent>();
         while (query.MoveNext(out var ent, out _, out _))
         {
             if (!EnsureComp<ParacusiaComponent>(ent, out var paracusia))
index a4f1227eccf88daa17dc3b88feba62f04a4a00ca..f7595bc836a09c7f450d6d7a0be0eb8b26a4950c 100644 (file)
@@ -31,18 +31,18 @@ public sealed partial class BuyerSpeciesCondition : ListingCondition
         if (!ent.TryGetComponent<MindComponent>(args.Buyer, out var mind))
             return true; // needed to obtain body entityuid to check for humanoid appearance
 
-        if (!ent.TryGetComponent<HumanoidAppearanceComponent>(mind.OwnedEntity, out var appearance))
+        if (!ent.TryGetComponent<HumanoidProfileComponent>(mind.OwnedEntity, out var humanoid))
             return true; // inanimate or non-humanoid entities should be handled elsewhere, main example being surplus crates
 
         if (Blacklist != null)
         {
-            if (Blacklist.Contains(appearance.Species))
+            if (Blacklist.Contains(humanoid.Species))
                 return false;
         }
 
         if (Whitelist != null)
         {
-            if (!Whitelist.Contains(appearance.Species))
+            if (!Whitelist.Contains(humanoid.Species))
                 return false;
         }
 
index d7434c48c9dc09535491a1d6aa89abd8f3a15c73..8092381a1720f4c5981e7b0ef35781a12f6a8677 100644 (file)
@@ -1,6 +1,7 @@
 using Content.Server.Administration;
 using Content.Server.Humanoid;
 using Content.Shared.Administration;
+using Content.Shared.Body;
 using Content.Shared.Cloning;
 using Content.Shared.Inventory;
 using Robust.Shared.Prototypes;
@@ -11,19 +12,19 @@ namespace Content.Server.Cloning.Commands;
 [ToolshedCommand, AdminCommand(AdminFlags.Fun)]
 public sealed class CloneCommand : ToolshedCommand
 {
-    private HumanoidAppearanceSystem? _appearance;
+    private SharedVisualBodySystem? _visualBody;
     private CloningSystem? _cloning;
     private MetaDataSystem? _metadata;
 
     [CommandImplementation("humanoidappearance")]
     public IEnumerable<EntityUid> HumanoidAppearance([PipedArgument] IEnumerable<EntityUid> targets, EntityUid source, bool rename)
     {
-        _appearance ??= GetSys<HumanoidAppearanceSystem>();
+        _visualBody ??= GetSys<SharedVisualBodySystem>();
         _metadata ??= GetSys<MetaDataSystem>();
 
         foreach (var ent in targets)
         {
-            _appearance.CloneAppearance(source, ent);
+            _visualBody.CopyAppearanceFrom(source, ent);
 
             if (rename)
                 _metadata.SetEntityName(ent, MetaData(source).EntityName, raiseEvents: true);
index 88b82a9ca5d534dde03e9ab426fa1250f64a1076..bfa509fece6b6daf0d217bcd9e9e6c9118d541c0 100644 (file)
@@ -1,12 +1,12 @@
 using Content.Server.Actions;
-using Content.Server.Humanoid;
+using Content.Shared.Body;
 using Content.Shared.Cloning.Events;
-using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Mobs;
 using Content.Shared.Toggleable;
 using Content.Shared.Wagging;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
 
 namespace Content.Server.Wagging;
 
@@ -16,7 +16,7 @@ namespace Content.Server.Wagging;
 public sealed class WaggingSystem : EntitySystem
 {
     [Dependency] private readonly ActionsSystem _actions = default!;
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
     [Dependency] private readonly IPrototypeManager _prototype = default!;
 
     public override void Initialize()
@@ -38,75 +38,91 @@ public sealed class WaggingSystem : EntitySystem
         EnsureComp<WaggingComponent>(args.CloneUid);
     }
 
-    private void OnWaggingMapInit(EntityUid uid, WaggingComponent component, MapInitEvent args)
+    private void OnWaggingMapInit(Entity<WaggingComponent> ent, ref MapInitEvent args)
     {
-        _actions.AddAction(uid, ref component.ActionEntity, component.Action, uid);
+        _actions.AddAction(ent, ref ent.Comp.ActionEntity, ent.Comp.Action, ent);
     }
 
-    private void OnWaggingShutdown(EntityUid uid, WaggingComponent component, ComponentShutdown args)
+    private void OnWaggingShutdown(Entity<WaggingComponent> ent, ref ComponentShutdown args)
     {
-        _actions.RemoveAction(uid, component.ActionEntity);
+        _actions.RemoveAction(ent.Owner, ent.Comp.ActionEntity);
     }
 
-    private void OnWaggingToggle(EntityUid uid, WaggingComponent component, ref ToggleActionEvent args)
+    private void OnWaggingToggle(Entity<WaggingComponent> ent, ref ToggleActionEvent args)
     {
         if (args.Handled)
             return;
 
-        TryToggleWagging(uid, wagging: component);
+        TryToggleWagging(ent.AsNullable());
     }
 
-    private void OnMobStateChanged(EntityUid uid, WaggingComponent component, MobStateChangedEvent args)
+    private void OnMobStateChanged(Entity<WaggingComponent> ent, ref MobStateChangedEvent args)
     {
-        if (component.Wagging)
-            TryToggleWagging(uid, wagging: component);
+        if (ent.Comp.Wagging)
+            TryToggleWagging(ent.AsNullable());
     }
 
-    public bool TryToggleWagging(EntityUid uid, WaggingComponent? wagging = null, HumanoidAppearanceComponent? humanoid = null)
+    private bool TryToggleWagging(Entity<WaggingComponent?> ent)
     {
-        if (!Resolve(uid, ref wagging, ref humanoid))
+        if (!Resolve(ent, ref ent.Comp))
             return false;
 
-        if (!humanoid.MarkingSet.Markings.TryGetValue(MarkingCategories.Tail, out var markings))
+        if (!_visualBody.TryGatherMarkingsData(ent.Owner,
+                [ent.Comp.Layer],
+                out _,
+                out _,
+                out var applied))
+        {
             return false;
+        }
 
-        if (markings.Count == 0)
+        if (!applied.TryGetValue(ent.Comp.Organ, out var markingsSet))
             return false;
 
-        wagging.Wagging = !wagging.Wagging;
+        ent.Comp.Wagging = !ent.Comp.Wagging;
 
-        for (var idx = 0; idx < markings.Count; idx++) // Animate all possible tails
+        markingsSet = markingsSet.ShallowClone();
+        foreach (var (layers, markings) in markingsSet)
         {
-            var currentMarkingId = markings[idx].MarkingId;
-            string newMarkingId;
+            markingsSet[layers] = markingsSet[layers].ShallowClone();
+            var layerMarkings = markingsSet[layers];
 
-            if (wagging.Wagging)
-            {
-                newMarkingId = $"{currentMarkingId}{wagging.Suffix}";
-            }
-            else
+            for (int i = 0; i < layerMarkings.Count; i++)
             {
-                if (currentMarkingId.EndsWith(wagging.Suffix))
+                var currentMarkingId = layerMarkings[i].MarkingId;
+                string newMarkingId;
+
+                if (ent.Comp.Wagging)
                 {
-                    newMarkingId = currentMarkingId[..^wagging.Suffix.Length];
+                    newMarkingId = $"{currentMarkingId}{ent.Comp.Suffix}";
                 }
                 else
                 {
-                    newMarkingId = currentMarkingId;
-                    Log.Warning($"Unable to revert wagging for {currentMarkingId}");
+                    if (currentMarkingId.EndsWith(ent.Comp.Suffix))
+                    {
+                        newMarkingId = currentMarkingId[..^ent.Comp.Suffix.Length];
+                    }
+                    else
+                    {
+                        newMarkingId = currentMarkingId;
+                        Log.Warning($"Unable to revert wagging for {currentMarkingId}");
+                    }
                 }
-            }
 
-            if (!_prototype.HasIndex<MarkingPrototype>(newMarkingId))
-            {
-                Log.Warning($"{ToPrettyString(uid)} tried toggling wagging but {newMarkingId} marking doesn't exist");
-                continue;
-            }
+                if (!_prototype.HasIndex<MarkingPrototype>(newMarkingId))
+                {
+                    Log.Warning($"{ToPrettyString(ent):ent} tried toggling wagging but {newMarkingId} marking doesn't exist");
+                    continue;
+                }
 
-            _humanoidAppearance.SetMarkingId(uid, MarkingCategories.Tail, idx, newMarkingId,
-                humanoid: humanoid);
+                layerMarkings[i] = new Marking(newMarkingId, layerMarkings[i].MarkingColors);
+            }
         }
 
+        _visualBody.ApplyMarkings(ent, new()
+        {
+            [ent.Comp.Organ] = markingsSet
+        });
         return true;
     }
 }
index 8418a3baf94319835798f3d62de86909110a9607..faf9c3a384d3820979f29fdb7854a5148d88dbfc 100644 (file)
@@ -19,7 +19,7 @@ public sealed class XAEPolymorphSystem : BaseXAESystem<XAEPolymorphComponent>
     [Dependency] private readonly SharedAudioSystem _audio = default!;
 
     /// <summary> Pre-allocated and re-used collection.</summary>
-    private readonly HashSet<Entity<HumanoidAppearanceComponent>> _humanoids = new();
+    private readonly HashSet<Entity<HumanoidProfileComponent>> _humanoids = new();
 
     /// <inheritdoc />
     protected override void OnActivated(Entity<XAEPolymorphComponent> ent, ref XenoArtifactNodeActivatedEvent args)
index f39487bb2455f329c8c1b0d3f14fdb83435cb202..16c89b54564ccb8a1698cc8f52ad919c815faf11 100644 (file)
@@ -1,3 +1,4 @@
+using System.Linq;
 using Content.Server.Administration.Managers;
 using Content.Server.Atmos.Components;
 using Content.Server.Body.Components;
@@ -13,6 +14,7 @@ using Content.Server.NPC.HTN;
 using Content.Server.NPC.Systems;
 using Content.Server.StationEvents.Components;
 using Content.Server.Speech.Components;
+using Content.Shared.Body;
 using Content.Shared.Body.Components;
 using Content.Shared.CombatMode;
 using Content.Shared.CombatMode.Pacification;
@@ -35,6 +37,7 @@ using Content.Shared.Prying.Components;
 using Content.Shared.Traits.Assorted;
 using Robust.Shared.Audio.Systems;
 using Content.Shared.Ghost.Roles.Components;
+using Content.Shared.Humanoid.Markings;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Tag;
 using Robust.Shared.Player;
@@ -60,7 +63,7 @@ public sealed partial class ZombieSystem
     [Dependency] private readonly NpcFactionSystem _faction = default!;
     [Dependency] private readonly GhostSystem _ghost = default!;
     [Dependency] private readonly SharedHandsSystem _hands = default!;
-    [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
     [Dependency] private readonly IdentitySystem _identity = default!;
     [Dependency] private readonly ServerInventorySystem _inventory = default!;
     [Dependency] private readonly MindSystem _mind = default!;
@@ -75,6 +78,7 @@ public sealed partial class ZombieSystem
     private static readonly ProtoId<NpcFactionPrototype> ZombieFaction = "Zombie";
     private static readonly string MindRoleZombie = "MindRoleZombie";
     private static readonly List<ProtoId<AntagPrototype>> BannableZombiePrototypes = ["Zombie"];
+    private static readonly HashSet<HumanoidVisualLayers> AdditionalZombieLayers = [HumanoidVisualLayers.Tail, HumanoidVisualLayers.HeadSide, HumanoidVisualLayers.HeadTop, HumanoidVisualLayers.Snout];
 
     /// <summary>
     /// Handles an entity turning into a zombie when they die or go into crit
@@ -186,27 +190,43 @@ public sealed partial class ZombieSystem
             _autoEmote.AddEmote(target, "ZombieGroan");
         }
 
-        //We have specific stuff for humanoid zombies because they matter more
-        if (TryComp<HumanoidAppearanceComponent>(target, out var huApComp)) //huapcomp
-        {
-            //store some values before changing them in case the humanoid get cloned later
-            zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
-            zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
-            zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
-            if (TryComp<BloodstreamComponent>(target, out var stream) && stream.BloodReferenceSolution is { } reagents)
-                zombiecomp.BeforeZombifiedBloodReagents = reagents.Clone();
-
-            _humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
+        if (TryComp<BloodstreamComponent>(target, out var stream) && stream.BloodReferenceSolution is { } reagents)
+            zombiecomp.BeforeZombifiedBloodReagents = reagents.Clone();
 
-            // Messing with the eye layer made it vanish upon cloning, and also it didn't even appear right
-            huApComp.EyeColor = zombiecomp.EyeColor;
+        if (_visualBody.TryGatherMarkingsData(target, null, out var profiles, out _, out var markings))
+        {
+            // TODO: My kingdom for ZombieSystem just using cloning system
+            zombiecomp.BeforeZombifiedProfiles = profiles;
+            zombiecomp.BeforeZombifiedMarkings = markings.ToDictionary(
+                kvp => kvp.Key,
+                kvp => kvp.Value.ToDictionary(
+                    it => it.Key,
+                    it => it.Value.Select(marking => new Marking(marking)).ToList()));
+
+            var zombifiedProfiles = profiles.ToDictionary(pair => pair.Key,
+                pair => pair.Value with { EyeColor = zombiecomp.EyeColor, SkinColor = zombiecomp.SkinColor });
+            _visualBody.ApplyProfiles(target, zombifiedProfiles);
+
+            foreach (var markingSet in markings.Values)
+            {
+                foreach (var (layer, layerMarkings) in markingSet)
+                {
+                    if (!AdditionalZombieLayers.Contains(layer))
+                        continue;
+
+                    foreach (var marking in layerMarkings)
+                    {
+                        marking.SetColor(zombiecomp.SkinColor);
+                    }
+                }
+            }
 
-            // this might not resync on clone?
-            _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
-            _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
-            _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
-            _humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
+            _visualBody.ApplyMarkings(target, markings);
+        }
 
+        //We have specific stuff for humanoid zombies because they matter more
+        if (HasComp<HumanoidProfileComponent>(target))
+        {
             //This is done here because non-humanoids shouldn't get baller damage
             melee.Damage = zombiecomp.DamageOnBite;
 
index 542c065e4b21219cca3f6779db4469fa61fc8c49..af9b3c4bdce3701f1ffb138e2ada714bf14ee8c7 100644 (file)
@@ -295,16 +295,9 @@ namespace Content.Server.Zombies
             if (!Resolve(source, ref zombiecomp))
                 return false;
 
-            foreach (var (layer, info) in zombiecomp.BeforeZombifiedCustomBaseLayers)
-            {
-                _humanoidAppearance.SetBaseLayerColor(target, layer, info.Color);
-                _humanoidAppearance.SetBaseLayerId(target, layer, info.Id);
-            }
-            if (TryComp<HumanoidAppearanceComponent>(target, out var appcomp))
-            {
-                appcomp.EyeColor = zombiecomp.BeforeZombifiedEyeColor;
-            }
-            _humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
+            _visualBody.ApplyProfiles(target, zombiecomp.BeforeZombifiedProfiles);
+            _visualBody.ApplyMarkings(target, zombiecomp.BeforeZombifiedMarkings);
+
             _bloodstream.ChangeBloodReagents(target, zombiecomp.BeforeZombifiedBloodReagents);
 
             return true;
index c6a025ace260b75617bc0050d32a10e14def7ceb..82587aa737dd9b8904d23a5b6bdf73ba44a6ca5a 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Body.Events;
 using Content.Shared.Gibbing;
+using Content.Shared.Humanoid;
 using Content.Shared.Medical;
 
 namespace Content.Shared.Body;
@@ -11,6 +12,10 @@ public sealed partial class BodySystem
         SubscribeLocalEvent<BodyComponent, ApplyMetabolicMultiplierEvent>(RefRelayBodyEvent);
         SubscribeLocalEvent<BodyComponent, TryVomitEvent>(RefRelayBodyEvent);
         SubscribeLocalEvent<BodyComponent, BeingGibbedEvent>(RefRelayBodyEvent);
+        SubscribeLocalEvent<BodyComponent, ApplyOrganProfileDataEvent>(RefRelayBodyEvent);
+        SubscribeLocalEvent<BodyComponent, ApplyOrganMarkingsEvent>(RefRelayBodyEvent);
+        SubscribeLocalEvent<BodyComponent, OrganCopyAppearanceEvent>(RefRelayBodyEvent);
+        SubscribeLocalEvent<BodyComponent, HumanoidLayerVisibilityChangedEvent>(RefRelayBodyEvent);
     }
 
     private void RefRelayBodyEvent<T>(EntityUid uid, BodyComponent component, ref T args) where T : struct
diff --git a/Content.Shared/Body/InitialBodyComponent.cs b/Content.Shared/Body/InitialBodyComponent.cs
new file mode 100644 (file)
index 0000000..cfb7a26
--- /dev/null
@@ -0,0 +1,14 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Body;
+
+/// <summary>
+/// On map initialization, spawns the given organs into the body.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(InitialBodySystem))]
+public sealed partial class InitialBodyComponent : Component
+{
+    [DataField(required: true)]
+    public Dictionary<ProtoId<OrganCategoryPrototype>, EntProtoId<OrganComponent>> Organs;
+}
diff --git a/Content.Shared/Body/InitialBodySystem.cs b/Content.Shared/Body/InitialBodySystem.cs
new file mode 100644 (file)
index 0000000..a062d1b
--- /dev/null
@@ -0,0 +1,48 @@
+using System.Numerics;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+
+namespace Content.Shared.Body;
+
+public sealed class InitialBodySystem : EntitySystem
+{
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<InitialBodyComponent, MapInitEvent>(OnMapInit);
+    }
+
+    private void OnMapInit(Entity<InitialBodyComponent> ent, ref MapInitEvent args)
+    {
+        if (!TryComp<ContainerManagerComponent>(ent, out var containerComp))
+            return;
+
+        if (TerminatingOrDeleted(ent) || !Exists(ent))
+            return;
+
+        if (!_container.TryGetContainer(ent, BodyComponent.ContainerID, out var container, containerComp))
+        {
+            Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(InitialBodyComponent)} is missing a container ({BodyComponent.ContainerID}).");
+            return;
+        }
+
+        var xform = Transform(ent);
+        var coords = new EntityCoordinates(ent, Vector2.Zero);
+
+        foreach (var proto in ent.Comp.Organs.Values)
+        {
+            // TODO: When e#6192 is merged replace this all with TrySpawnInContainer...
+            var spawn = Spawn(proto, coords);
+
+            if (!_container.Insert(spawn, container, containerXform: xform))
+            {
+                Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(InitialBodyComponent)} failed to insert an entity: {ToPrettyString(spawn)}.\n");
+                Del(spawn);
+            }
+        }
+    }
+}
diff --git a/Content.Shared/Body/SharedVisualBodySystem.Initial.cs b/Content.Shared/Body/SharedVisualBodySystem.Initial.cs
new file mode 100644 (file)
index 0000000..b2ed4a9
--- /dev/null
@@ -0,0 +1,19 @@
+using Content.Shared.Humanoid;
+
+namespace Content.Shared.Body;
+
+public abstract partial class SharedVisualBodySystem
+{
+    private void InitializeInitial()
+    {
+        SubscribeLocalEvent<VisualBodyComponent, MapInitEvent>(OnVisualMapInit, after: [typeof(InitialBodySystem)]);
+    }
+
+    private void OnVisualMapInit(Entity<VisualBodyComponent> ent, ref MapInitEvent args)
+    {
+        if (!TryComp<HumanoidProfileComponent>(ent, out var humanoidProfile))
+            return;
+
+        ApplyAppearanceTo(ent.AsNullable(), HumanoidCharacterAppearance.DefaultWithSpecies(humanoidProfile.Species, humanoidProfile.Sex), humanoidProfile.Sex);
+    }
+}
diff --git a/Content.Shared/Body/SharedVisualBodySystem.Modifiers.cs b/Content.Shared/Body/SharedVisualBodySystem.Modifiers.cs
new file mode 100644 (file)
index 0000000..0eb0d10
--- /dev/null
@@ -0,0 +1,153 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Shared.Administration.Managers;
+using Content.Shared.Administration;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Content.Shared.Preferences;
+using Content.Shared.Verbs;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Body;
+
+public abstract partial class SharedVisualBodySystem
+{
+    [Dependency] private readonly ISharedAdminManager _admin = default!;
+    [Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
+
+    private void InitializeModifiers()
+    {
+        SubscribeLocalEvent<VisualBodyComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
+
+        Subs.BuiEvents<VisualBodyComponent>(HumanoidMarkingModifierKey.Key,
+            subs =>
+            {
+                subs.Event<BoundUIOpenedEvent>(OnModifiersOpened);
+                subs.Event<HumanoidMarkingModifierMarkingSetMessage>(OnSetModifiers);
+            });
+    }
+
+    private void OnGetVerbs(Entity<VisualBodyComponent> ent, ref GetVerbsEvent<Verb> args)
+    {
+        if (!_admin.HasAdminFlag(args.User, AdminFlags.Fun))
+            return;
+
+        var user = args.User;
+        args.Verbs.Add(new Verb
+        {
+            Text = "Modify markings",
+            Category = VerbCategory.Tricks,
+            Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Customization/reptilian_parts.rsi"), "tail_smooth"),
+            Act = () =>
+            {
+                _userInterface.OpenUi(ent.Owner, HumanoidMarkingModifierKey.Key, user);
+            }
+        });
+    }
+
+    /// <summary>
+    /// Gathers all the markings-relevant data from this entity
+    /// </summary>
+    /// <param name="ent">The entity to sample</param>
+    /// <param name="filter">If set, only returns data concerning the given layers</param>
+    /// <param name="profiles">The profiles for the various organs</param>
+    /// <param name="markings">The marking parameters for the various organs</param>
+    /// <param name="applied">The markings that are applied to the entity</param>
+    public bool TryGatherMarkingsData(Entity<VisualBodyComponent?> ent,
+        HashSet<HumanoidVisualLayers>? filter,
+        [NotNullWhen(true)] out Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData>? profiles,
+        [NotNullWhen(true)] out Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData>? markings,
+        [NotNullWhen(true)] out Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>? applied)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+        {
+            profiles = null;
+            markings = null;
+            applied = null;
+            return false;
+        }
+
+        profiles = new();
+        markings = new();
+        applied = new();
+
+        var organContainer = _container.EnsureContainer<Container>(ent, BodyComponent.ContainerID);
+
+        foreach (var organ in organContainer.ContainedEntities)
+        {
+            if (!TryComp<OrganComponent>(organ, out var organComp) || organComp.Category is not { } category)
+                continue;
+
+            if (TryComp<VisualOrganComponent>(organ, out var visualOrgan))
+            {
+                profiles.TryAdd(category, visualOrgan.Profile);
+            }
+
+            if (TryComp<VisualOrganMarkingsComponent>(organ, out var visualOrganMarkings))
+            {
+                markings.TryAdd(category, visualOrganMarkings.MarkingData);
+                if (filter is not null)
+                    applied.TryAdd(category, visualOrganMarkings.Markings.Where(kvp => filter.Contains(kvp.Key)).ToDictionary());
+                else
+                    applied.TryAdd(category, visualOrganMarkings.Markings);
+            }
+        }
+
+        return true;
+    }
+
+    private void OnModifiersOpened(Entity<VisualBodyComponent> ent, ref BoundUIOpenedEvent args)
+    {
+        TryGatherMarkingsData(ent.AsNullable(), null, out var profiles, out var markings, out var applied);
+
+        _userInterface.SetUiState(ent.Owner, HumanoidMarkingModifierKey.Key, new HumanoidMarkingModifierState(applied!, markings!, profiles!));
+    }
+
+    private void OnSetModifiers(Entity<VisualBodyComponent> ent, ref HumanoidMarkingModifierMarkingSetMessage args)
+    {
+        var markingsEvt = new ApplyOrganMarkingsEvent(args.Markings);
+        RaiseLocalEvent(ent, ref markingsEvt);
+    }
+
+    public void ApplyMarkings(EntityUid ent, Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> markings)
+    {
+        var markingsEvt = new ApplyOrganMarkingsEvent(markings);
+        RaiseLocalEvent(ent, ref markingsEvt);
+    }
+
+    private void ApplyAppearanceTo(Entity<VisualBodyComponent?> ent, HumanoidCharacterAppearance appearance, Sex sex)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return;
+
+        ApplyProfile(ent,
+            new()
+        {
+            Sex = sex,
+            SkinColor = appearance.SkinColor,
+            EyeColor = appearance.EyeColor,
+        });
+
+        var markingsEvt = new ApplyOrganMarkingsEvent(appearance.Markings);
+        RaiseLocalEvent(ent, ref markingsEvt);
+    }
+
+    public void ApplyProfileTo(Entity<VisualBodyComponent?> ent, HumanoidCharacterProfile profile)
+    {
+        ApplyAppearanceTo(ent, profile.Appearance, profile.Sex);
+    }
+
+    public void ApplyProfile(EntityUid ent, OrganProfileData profile)
+    {
+        var profileEvt = new ApplyOrganProfileDataEvent(profile, null);
+        RaiseLocalEvent(ent, ref profileEvt);
+    }
+
+    public void ApplyProfiles(EntityUid ent, Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> profiles)
+    {
+        var profileEvt = new ApplyOrganProfileDataEvent(null, profiles);
+        RaiseLocalEvent(ent, ref profileEvt);
+    }
+}
diff --git a/Content.Shared/Body/SharedVisualBodySystem.cs b/Content.Shared/Body/SharedVisualBodySystem.cs
new file mode 100644 (file)
index 0000000..52f5bbf
--- /dev/null
@@ -0,0 +1,198 @@
+using System.Linq;
+using Content.Shared.Humanoid.Markings;
+using Content.Shared.Humanoid;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Body;
+
+public abstract partial class SharedVisualBodySystem : EntitySystem
+{
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+    [Dependency] private readonly MarkingManager _marking = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<VisualOrganComponent, BodyRelayedEvent<OrganCopyAppearanceEvent>>(OnVisualOrganCopyAppearance);
+        SubscribeLocalEvent<VisualOrganMarkingsComponent, BodyRelayedEvent<OrganCopyAppearanceEvent>>(OnMarkingsOrganCopyAppearance);
+        SubscribeLocalEvent<VisualOrganComponent, BodyRelayedEvent<ApplyOrganProfileDataEvent>>(OnVisualOrganApplyProfile);
+        SubscribeLocalEvent<VisualOrganMarkingsComponent, BodyRelayedEvent<ApplyOrganMarkingsEvent>>(OnMarkingsOrganApplyMarkings);
+
+        InitializeModifiers();
+        InitializeInitial();
+    }
+
+    private List<Marking> ResolveMarkings(List<Marking> markings, Color? skinColor, Color? eyeColor, Dictionary<Enum, MarkingsAppearance> appearances)
+    {
+        var ret = new List<Marking>();
+        var forcedColors = new List<(Marking, MarkingPrototype)>();
+
+        // This method uses two loops since some marking with constrained colors care about the colors of previous markings.
+        // As such we want to ensure we can apply the markings they rely on first.
+        foreach (var marking in markings)
+        {
+            if (!_marking.TryGetMarking(marking, out var proto))
+                continue;
+
+            if (!proto.ForcedColoring && appearances.GetValueOrDefault(proto.BodyPart)?.MatchSkin != true)
+                ret.Add(marking);
+            else
+                forcedColors.Add((marking, proto));
+        }
+
+        foreach (var (marking, prototype) in forcedColors)
+        {
+            var colors = MarkingColoring.GetMarkingLayerColors(
+                prototype,
+                skinColor,
+                eyeColor,
+                ret);
+
+            var markingWithColor = new Marking(marking.MarkingId, colors)
+            {
+                Forced = marking.Forced,
+            };
+            if (appearances.GetValueOrDefault(prototype.BodyPart) is { MatchSkin: true } appearance && skinColor is { } color)
+            {
+                markingWithColor.SetColor(color.WithAlpha(appearance.LayerAlpha));
+            }
+            ret.Add(markingWithColor);
+        }
+
+        return ret;
+    }
+
+    protected virtual void SetOrganColor(Entity<VisualOrganComponent> ent, Color color)
+    {
+        ent.Comp.Data.Color = color;
+        Dirty(ent);
+    }
+
+    protected virtual void SetOrganAppearance(Entity<VisualOrganComponent> ent, PrototypeLayerData data)
+    {
+        ent.Comp.Data = data;
+        Dirty(ent);
+    }
+
+    protected virtual void SetOrganMarkings(Entity<VisualOrganMarkingsComponent> ent, Dictionary<HumanoidVisualLayers, List<Marking>> markings)
+    {
+        ent.Comp.Markings = markings;
+        Dirty(ent);
+    }
+
+    public void CopyAppearanceFrom(Entity<BodyComponent?> source, Entity<BodyComponent?> target)
+    {
+        if (!Resolve(source, ref source.Comp) || !Resolve(target, ref target.Comp))
+            return;
+
+        var sourceOrgans = _container.EnsureContainer<Container>(source, BodyComponent.ContainerID);
+
+        foreach (var sourceOrgan in sourceOrgans.ContainedEntities)
+        {
+            var evt = new OrganCopyAppearanceEvent(sourceOrgan);
+            RaiseLocalEvent(target, ref evt);
+        }
+    }
+
+    private void OnVisualOrganCopyAppearance(Entity<VisualOrganComponent> ent, ref BodyRelayedEvent<OrganCopyAppearanceEvent> args)
+    {
+        if (!TryComp<VisualOrganComponent>(args.Args.Organ, out var other))
+            return;
+
+        if (!other.Layer.Equals(ent.Comp.Layer))
+            return;
+
+        SetOrganAppearance(ent, other.Data);
+    }
+
+    private void OnMarkingsOrganCopyAppearance(Entity<VisualOrganMarkingsComponent> ent, ref BodyRelayedEvent<OrganCopyAppearanceEvent> args)
+    {
+        if (!TryComp<VisualOrganMarkingsComponent>(args.Args.Organ, out var other))
+            return;
+
+        if (!other.MarkingData.Layers.SetEquals(ent.Comp.MarkingData.Layers))
+            return;
+
+        SetOrganMarkings(ent, other.Markings);
+    }
+
+    private void OnVisualOrganApplyProfile(Entity<VisualOrganComponent> ent, ref BodyRelayedEvent<ApplyOrganProfileDataEvent> args)
+    {
+        if (Comp<OrganComponent>(ent).Category is not { } category)
+            return;
+
+        var relevantData = args.Args.Base;
+        if (args.Args.Profiles?.TryGetValue(category, out var profile) == true)
+            relevantData = profile;
+
+        if (relevantData is not { } data)
+            return;
+
+        ent.Comp.Profile = data;
+
+        if (ent.Comp.Layer.Equals(HumanoidVisualLayers.Eyes))
+            SetOrganColor(ent, ent.Comp.Profile.EyeColor);
+        else
+            SetOrganColor(ent, ent.Comp.Profile.SkinColor);
+    }
+
+    private void OnMarkingsOrganApplyMarkings(Entity<VisualOrganMarkingsComponent> ent, ref BodyRelayedEvent<ApplyOrganMarkingsEvent> args)
+    {
+        if (Comp<OrganComponent>(ent).Category is not { } category)
+            return;
+
+        if (!args.Args.Markings.TryGetValue(category, out var markingSet))
+            return;
+
+        var groupProto = _prototype.Index(ent.Comp.MarkingData.Group);
+        var organMarkings = ent.Comp.Markings.ShallowClone();
+
+        foreach (var layer in ent.Comp.MarkingData.Layers)
+        {
+            if (!markingSet.TryGetValue(layer, out var markings))
+                continue;
+
+            var okSet = new List<Marking>();
+
+            foreach (var marking in markings)
+            {
+                if (!_marking.TryGetMarking(marking, out _))
+                    continue;
+
+                okSet.Add(marking);
+            }
+
+            organMarkings[layer] = okSet;
+        }
+
+        var profile = Comp<VisualOrganComponent>(ent).Profile;
+        var resolved = organMarkings.ToDictionary(
+            kvp => kvp.Key,
+            kvp => ResolveMarkings(kvp.Value, profile.SkinColor, profile.EyeColor, groupProto.Appearances));
+
+        SetOrganMarkings(ent, resolved);
+    }
+}
+
+/// <summary>
+/// Raised on body entity, when an organ is having its appearance copied to it
+/// </summary>
+[ByRefEvent]
+public readonly record struct OrganCopyAppearanceEvent(EntityUid Organ);
+
+/// <summary>
+/// Raised on body entity when profiles are being applied to it
+/// </summary>
+[ByRefEvent]
+public readonly record struct ApplyOrganProfileDataEvent(OrganProfileData? Base, Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData>? Profiles);
+
+/// <summary>
+/// Raised on body entity when a profile is being applied to it
+/// </summary>
+[ByRefEvent]
+public readonly record struct ApplyOrganMarkingsEvent(Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> Markings);
+
diff --git a/Content.Shared/Body/VisualBodyComponent.cs b/Content.Shared/Body/VisualBodyComponent.cs
new file mode 100644 (file)
index 0000000..030b287
--- /dev/null
@@ -0,0 +1,7 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Body;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedVisualBodySystem))]
+public sealed partial class VisualBodyComponent : Component;
diff --git a/Content.Shared/Body/VisualOrganComponent.cs b/Content.Shared/Body/VisualOrganComponent.cs
new file mode 100644 (file)
index 0000000..10214f9
--- /dev/null
@@ -0,0 +1,52 @@
+using Content.Shared.Humanoid;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Body;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+[Access(typeof(SharedVisualBodySystem))]
+public sealed partial class VisualOrganComponent : Component
+{
+    /// <summary>
+    /// The layer on the entity that this contributes to
+    /// </summary>
+    [DataField(required: true)]
+    public Enum Layer;
+
+    /// <summary>
+    /// The data for the layer
+    /// </summary>
+    [DataField(required: true), AutoNetworkedField, AlwaysPushInheritance]
+    public PrototypeLayerData Data;
+
+    [DataField, AutoNetworkedField]
+    public OrganProfileData Profile = new();
+}
+
+/// <summary>
+/// Defines the coloration, sex, etc. of organs
+/// </summary>
+[DataDefinition]
+[Serializable, NetSerializable]
+public partial record struct OrganProfileData
+{
+    /// <summary>
+    /// The "sex" of this organ
+    /// </summary>
+    [DataField]
+    public Sex Sex;
+
+    /// <summary>
+    /// The "eye color" of this organ
+    /// </summary>
+    [DataField]
+    public Color EyeColor = Color.White;
+
+    /// <summary>
+    /// The "skin color" of this organ
+    /// </summary>
+    [DataField]
+    public Color SkinColor = Color.White;
+}
+
diff --git a/Content.Shared/Body/VisualOrganMarkingsComponent.cs b/Content.Shared/Body/VisualOrganMarkingsComponent.cs
new file mode 100644 (file)
index 0000000..e0ec567
--- /dev/null
@@ -0,0 +1,47 @@
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Body;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true, fieldDeltas: true)]
+[Access(typeof(SharedVisualBodySystem))]
+public sealed partial class VisualOrganMarkingsComponent : Component
+{
+    /// <summary>
+    /// What markings this organ can take
+    /// </summary>
+    [DataField(required: true), AlwaysPushInheritance]
+    public OrganMarkingData MarkingData = default!;
+
+    /// <summary>
+    /// The list of markings to apply to the entity
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public Dictionary<HumanoidVisualLayers, List<Marking>> Markings = new();
+
+    /// <summary>
+    /// Client only - the last markings applied by this component
+    /// </summary>
+    [ViewVariables]
+    public List<Marking> AppliedMarkings = new();
+}
+
+/// <summary>
+/// Defines the layers and group an organ takes markings for
+/// </summary>
+[DataDefinition]
+[Serializable, NetSerializable]
+public partial record struct OrganMarkingData
+{
+    [DataField(required: true)]
+    public HashSet<HumanoidVisualLayers> Layers = default!;
+
+    /// <summary>
+    /// The type of organ this is for markings
+    /// </summary>
+    [DataField(required: true)]
+    public ProtoId<MarkingsGroupPrototype> Group = default!;
+}
index a874afe9ce6b608be2761ce4d16d0ac99b2e9d33..af1651f8dc2446b13c9260d0c6c057181c5d4a56 100644 (file)
@@ -39,7 +39,7 @@ public sealed partial class ChangelingDevourComponent : Component
         Components =
         [
             "MobState",
-            "HumanoidAppearance",
+            "HumanoidProfile",
         ],
     };
 
index d65d39ca40c57f1c3b7a9d107f7a234291218e14..385ed5c9e98591e19d0df3851fe589a7b8dcb42b 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Administration.Logs;
+using Content.Shared.Body;
 using Content.Shared.Changeling.Components;
 using Content.Shared.Cloning;
 using Content.Shared.Database;
@@ -19,7 +20,6 @@ namespace Content.Shared.Changeling.Systems;
 public sealed class ChangelingClonerSystem : EntitySystem
 {
     [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
-    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
     [Dependency] private readonly MetaDataSystem _metaData = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
@@ -29,6 +29,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
     [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
     [Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentity = default!;
     [Dependency] private readonly SharedForensicsSystem _forensics = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
 
     public override void Initialize()
     {
@@ -132,7 +133,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
         if (ent.Comp.State != ChangelingClonerState.Empty)
             return false;
 
-        if (!HasComp<HumanoidAppearanceComponent>(target))
+        if (!HasComp<HumanoidProfileComponent>(target))
             return false; // cloning only works for humanoids at the moment
 
         var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerDrawDoAfterEvent(), ent, target: target, used: ent)
@@ -168,7 +169,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
         if (ent.Comp.State != ChangelingClonerState.Filled)
             return false;
 
-        if (!HasComp<HumanoidAppearanceComponent>(target))
+        if (!HasComp<HumanoidProfileComponent>(target))
             return false; // cloning only works for humanoids at the moment
 
         var args = new DoAfterArgs(EntityManager, user, ent.Comp.DoAfter, new ClonerInjectDoAfterEvent(), ent, target: target, used: ent)
@@ -205,7 +206,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
         if (ent.Comp.State != ChangelingClonerState.Empty)
             return;
 
-        if (!HasComp<HumanoidAppearanceComponent>(target))
+        if (!HasComp<HumanoidProfileComponent>(target))
             return; // cloning only works for humanoids at the moment
 
         if (!_prototype.Resolve(ent.Comp.Settings, out var settings))
@@ -235,7 +236,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
         if (ent.Comp.State != ChangelingClonerState.Filled)
             return;
 
-        if (!HasComp<HumanoidAppearanceComponent>(target))
+        if (!HasComp<HumanoidProfileComponent>(target))
             return; // cloning only works for humanoids at the moment
 
         if (!_prototype.Resolve(ent.Comp.Settings, out var settings))
@@ -258,7 +259,7 @@ public sealed class ChangelingClonerSystem : EntitySystem
             $"{user} is using {ent.Owner} to inject DNA into {target} changing their identity to {ent.Comp.ClonedBackup.Value}.");
 
         // Do the actual transformation.
-        _humanoidAppearance.CloneAppearance(ent.Comp.ClonedBackup.Value, target);
+        _visualBody.CopyAppearanceFrom(ent.Comp.ClonedBackup.Value, target);
         _cloning.CloneComponents(ent.Comp.ClonedBackup.Value, target, settings);
         _metaData.SetEntityName(target, Name(ent.Comp.ClonedBackup.Value), raiseEvents: ent.Comp.RaiseNameChangeEvents);
 
index 3b5b07d3f94a35a80f1475500334a925283639ff..3e6ad03e19dd1e7bb97b46a6dc1a843c6f453d22 100644 (file)
@@ -249,7 +249,7 @@ public sealed class ChangelingDevourSystem : EntitySystem
 
         if (_mobState.IsDead(target.Value)
             && TryComp<BodyComponent>(target, out var body)
-            && HasComp<HumanoidAppearanceComponent>(target)
+            && HasComp<HumanoidProfileComponent>(target)
             && TryComp<ChangelingIdentityComponent>(args.User, out var identityStorage))
         {
             _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player}  successfully devoured {ToPrettyString(args.Target):player}'s identity");
index cf8d9d7cb6ee247a278eceb2b273ca435dd3d8a7..60015c79c9b80c418257e562dce038ac30a7efc3 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Actions;
 using Content.Shared.Administration.Logs;
+using Content.Shared.Body;
 using Content.Shared.Changeling.Components;
 using Content.Shared.Cloning;
 using Content.Shared.Database;
@@ -19,12 +20,12 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
     [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
     [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
     [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
-    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearanceSystem = default!;
     [Dependency] private readonly MetaDataSystem _metaSystem = default!;
     [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
     [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly SharedCloningSystem _cloningSystem = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
     [Dependency] private readonly IPrototypeManager _prototype = default!;
 
     private const string ChangelingBuiXmlGeneratedName = "ChangelingTransformBoundUserInterface";
@@ -152,7 +153,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
         if (args.Target is not { } targetIdentity)
             return;
 
-        _humanoidAppearanceSystem.CloneAppearance(targetIdentity, args.User);
+        _visualBody.CopyAppearanceFrom(targetIdentity, args.User);
         _cloningSystem.CloneComponents(targetIdentity, args.User, settings);
 
         if (TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
index 57439d0a0b214e64eb92d59124a3eaf4728dfa69..a3b620d68ad5913165e149a16c81410cbcdd4b77 100644 (file)
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Shared.Body;
 using Content.Shared.Changeling.Components;
 using Content.Shared.Cloning;
 using Content.Shared.Humanoid;
@@ -18,8 +19,8 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
     [Dependency] private readonly MetaDataSystem _metaSystem = default!;
     [Dependency] private readonly NameModifierSystem _nameMod = default!;
     [Dependency] private readonly SharedCloningSystem _cloningSystem = default!;
-    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!;
     [Dependency] private readonly SharedMapSystem _map = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
     [Dependency] private readonly SharedPvsOverrideSystem _pvsOverrideSystem = default!;
 
     public MapId? PausedMapId;
@@ -93,7 +94,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
         if (_net.IsClient)
             return null;
 
-        if (!TryComp<HumanoidAppearanceComponent>(target, out var humanoid)
+        if (!TryComp<HumanoidProfileComponent>(target, out var humanoid)
             || !_prototype.Resolve(humanoid.Species, out var speciesPrototype))
             return null;
 
@@ -106,7 +107,7 @@ public abstract class SharedChangelingIdentitySystem : EntitySystem
         if (TryComp<ActorComponent>(target, out var actor))
             storedIdentity.OriginalSession = actor.PlayerSession;
 
-        _humanoidSystem.CloneAppearance(target, clone);
+        _visualBody.CopyAppearanceFrom(target, clone);
         _cloningSystem.CloneComponents(target, clone, settings);
 
         var targetName = _nameMod.GetBaseName(target);
index 44d72b248b489a73292d1749113eb1d4e48107ba..1d58e7071c1abbfb1851086c0955d0ef136f0324 100644 (file)
@@ -8,7 +8,7 @@ namespace Content.Shared.Clothing.EntitySystems;
 
 public sealed class HideLayerClothingSystem : EntitySystem
 {
-    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!;
+    [Dependency] private readonly SharedHideableHumanoidLayersSystem _hideableHumanoidLayers = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
 
     public override void Initialize()
@@ -36,7 +36,7 @@ public sealed class HideLayerClothingSystem : EntitySystem
 
     private void SetLayerVisibility(
         Entity<HideLayerClothingComponent?, ClothingComponent?> clothing,
-        Entity<HumanoidAppearanceComponent?> user,
+        Entity<HideableHumanoidLayersComponent?> user,
         bool hideLayers)
     {
         if (_timing.ApplyingState)
@@ -60,8 +60,6 @@ public sealed class HideLayerClothingSystem : EntitySystem
         DebugTools.AssertNotNull(clothing.Comp2.InSlotFlag);
         DebugTools.AssertNotEqual(inSlot, SlotFlags.NONE);
 
-        var dirty = false;
-
         // iterate the HideLayerClothingComponent's layers map and check that
         // the clothing is (or was)equipped in a matching slot.
         foreach (var (layer, validSlots) in clothing.Comp1.Layers)
@@ -71,7 +69,7 @@ public sealed class HideLayerClothingSystem : EntitySystem
 
             // Only update this layer if we are currently equipped to the relevant slot.
             if (validSlots.HasFlag(inSlot))
-                _humanoid.SetLayerVisibility(user!, layer, !hideLayers, inSlot, ref dirty);
+                _hideableHumanoidLayers.SetLayerVisibility(user, layer, !hideLayers, inSlot);
         }
 
         // Fallback for obsolete field: assume we want to hide **all** layers, as long as we are equipped to any
@@ -83,12 +81,9 @@ public sealed class HideLayerClothingSystem : EntitySystem
             foreach (var layer in slots)
             {
                 if (hideable.Contains(layer))
-                    _humanoid.SetLayerVisibility(user!, layer, !hideLayers, inSlot, ref dirty);
+                    _hideableHumanoidLayers.SetLayerVisibility(user, layer, !hideLayers, inSlot);
             }
         }
-
-        if (dirty)
-            Dirty(user!);
     }
 
     private bool IsEnabled(Entity<HideLayerClothingComponent, ClothingComponent> clothing)
index 760c9f39d8b30764fe7fd6925dab8f2fda2e939c..84df71cfa4801838438ae2673e6e9998fedfe15a 100644 (file)
@@ -1,4 +1,5 @@
 using System.Linq;
+using Content.Shared.Body;
 using Content.Shared.Clothing.Components;
 using Content.Shared.Humanoid;
 using Content.Shared.Preferences;
@@ -28,7 +29,7 @@ public sealed class LoadoutSystem : EntitySystem
         base.Initialize();
 
         // Wait until the character has all their organs before we give them their loadout
-        SubscribeLocalEvent<LoadoutComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<LoadoutComponent, MapInitEvent>(OnMapInit, after: [typeof(InitialBodySystem)]);
     }
 
     public static string GetJobPrototype(string? loadout)
@@ -176,11 +177,8 @@ public sealed class LoadoutSystem : EntitySystem
 
     public HumanoidCharacterProfile GetProfile(EntityUid? uid)
     {
-        if (TryComp(uid, out HumanoidAppearanceComponent? appearance))
-        {
-            return HumanoidCharacterProfile.DefaultWithSpecies(appearance.Species);
-        }
-
-        return HumanoidCharacterProfile.Random();
+        return TryComp<HumanoidProfileComponent>(uid, out var profile)
+            ? HumanoidCharacterProfile.DefaultWithSpecies(profile.Species, profile.Sex)
+            : HumanoidCharacterProfile.Random();
     }
 }
diff --git a/Content.Shared/Humanoid/HideableHumanoidLayersComponent.cs b/Content.Shared/Humanoid/HideableHumanoidLayersComponent.cs
new file mode 100644 (file)
index 0000000..8fa0998
--- /dev/null
@@ -0,0 +1,35 @@
+using Content.Shared.Inventory;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Humanoid;
+
+[NetworkedComponent, RegisterComponent, AutoGenerateComponentState(true)]
+[Access(typeof(SharedHideableHumanoidLayersSystem))]
+public sealed partial class HideableHumanoidLayersComponent : Component
+{
+    /// <summary>
+    ///     A map of the visual layers currently hidden to the equipment
+    ///     slots that are currently hiding them. This will affect the base
+    ///     sprite on this humanoid layer, and any markings that sit above it.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public Dictionary<HumanoidVisualLayers, SlotFlags> HiddenLayers = new();
+
+    /// <summary>
+    ///     Which layers of this humanoid that should be hidden on equipping a corresponding item..
+    /// </summary>
+    [DataField]
+    public HashSet<HumanoidVisualLayers> HideLayersOnEquip = [HumanoidVisualLayers.Hair];
+
+    /// <summary>
+    ///     Client only - which layers were last hidden
+    /// </summary>
+    [ViewVariables]
+    public HashSet<HumanoidVisualLayers> LastHiddenLayers = new();
+}
+
+/// <summary>
+/// Raised on an entity when one of its humanoid layers changes its visibility
+/// </summary>
+[ByRefEvent]
+public readonly record struct HumanoidLayerVisibilityChangedEvent(HumanoidVisualLayers Layer, bool Visible);
diff --git a/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs b/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs
deleted file mode 100644 (file)
index aeb8d67..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-using Content.Shared.DisplacementMap;
-using Content.Shared.Humanoid.Markings;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Inventory;
-using Robust.Shared.Enums;
-using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.Utility;
-
-namespace Content.Shared.Humanoid;
-
-[NetworkedComponent, RegisterComponent, AutoGenerateComponentState(true)]
-public sealed partial class HumanoidAppearanceComponent : Component
-{
-    public MarkingSet ClientOldMarkings = new();
-
-    [DataField, AutoNetworkedField]
-    public MarkingSet MarkingSet = new();
-
-    [DataField]
-    public Dictionary<HumanoidVisualLayers, HumanoidSpeciesSpriteLayer> BaseLayers = new();
-
-    [DataField, AutoNetworkedField]
-    public HashSet<HumanoidVisualLayers> PermanentlyHidden = new();
-
-    // Couldn't these be somewhere else?
-
-    [DataField, AutoNetworkedField]
-    public Gender Gender;
-
-    [DataField, AutoNetworkedField]
-    public int Age = 18;
-
-    /// <summary>
-    ///     Any custom base layers this humanoid might have. See:
-    ///     limb transplants (potentially), robotic arms, etc.
-    ///     Stored on the server, this is merged in the client into
-    ///     all layer settings.
-    /// </summary>
-    [DataField, AutoNetworkedField]
-    public Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> CustomBaseLayers = new();
-
-    /// <summary>
-    ///     Current species. Dictates things like base body sprites,
-    ///     base humanoid to spawn, etc.
-    /// </summary>
-    [DataField(required: true), AutoNetworkedField]
-    public ProtoId<SpeciesPrototype> Species { get; set; }
-
-    /// <summary>
-    ///     The initial profile and base layers to apply to this humanoid.
-    /// </summary>
-    [DataField]
-    public ProtoId<HumanoidProfilePrototype>? Initial { get; private set; }
-
-    /// <summary>
-    ///     Skin color of this humanoid.
-    /// </summary>
-    [DataField, AutoNetworkedField]
-    public Color SkinColor { get; set; } = Color.FromHex("#C0967F");
-
-    /// <summary>
-    ///     A map of the visual layers currently hidden to the equipment
-    ///     slots that are currently hiding them. This will affect the base
-    ///     sprite on this humanoid layer, and any markings that sit above it.
-    /// </summary>
-    [DataField, AutoNetworkedField]
-    public Dictionary<HumanoidVisualLayers, SlotFlags> HiddenLayers = new();
-
-    [DataField, AutoNetworkedField]
-    public Sex Sex = Sex.Male;
-
-    [DataField, AutoNetworkedField]
-    public Color EyeColor = Color.Brown;
-
-    /// <summary>
-    ///     Hair color of this humanoid. Used to avoid looping through all markings
-    /// </summary>
-    [ViewVariables(VVAccess.ReadOnly)]
-    public Color? CachedHairColor;
-
-    /// <summary>
-    ///     Facial Hair color of this humanoid. Used to avoid looping through all markings
-    /// </summary>
-    [ViewVariables(VVAccess.ReadOnly)]
-    public Color? CachedFacialHairColor;
-
-    /// <summary>
-    ///     Which layers of this humanoid that should be hidden on equipping a corresponding item..
-    /// </summary>
-    [DataField]
-    public HashSet<HumanoidVisualLayers> HideLayersOnEquip = [HumanoidVisualLayers.Hair];
-
-    /// <summary>
-    ///     Which markings the humanoid defaults to when nudity is toggled off.
-    /// </summary>
-    [DataField]
-    public ProtoId<MarkingPrototype>? UndergarmentTop = new ProtoId<MarkingPrototype>("UndergarmentTopTanktop");
-
-    [DataField]
-    public ProtoId<MarkingPrototype>? UndergarmentBottom = new ProtoId<MarkingPrototype>("UndergarmentBottomBoxers");
-
-    /// <summary>
-    ///     The displacement maps that will be applied to specific layers of the humanoid.
-    /// </summary>
-    [DataField]
-    public Dictionary<HumanoidVisualLayers, DisplacementData> MarkingsDisplacement = new();
-}
-
-[DataDefinition]
-[Serializable, NetSerializable]
-public readonly partial struct CustomBaseLayerInfo
-{
-    public CustomBaseLayerInfo(string? id, Color? color = null)
-    {
-        DebugTools.Assert(id == null || IoCManager.Resolve<IPrototypeManager>().HasIndex<HumanoidSpeciesSpriteLayer>(id));
-        Id = id;
-        Color = color;
-    }
-
-    /// <summary>
-    ///     ID of this custom base layer. Must be a <see cref="HumanoidSpeciesSpriteLayer"/>.
-    /// </summary>
-    [DataField]
-    public ProtoId<HumanoidSpeciesSpriteLayer>? Id { get; init; }
-
-    /// <summary>
-    ///     Color of this custom base layer. Null implies skin colour if the corresponding <see cref="HumanoidSpeciesSpriteLayer"/> is set to match skin.
-    /// </summary>
-    [DataField]
-    public Color? Color { get; init; }
-}
index a9dd6d8755b83594a4f7c78946e582b892908de0..b2123db04bf2eb54d501d40d955b5400165ae1eb 100644 (file)
@@ -1,10 +1,12 @@
 using System.Linq;
 using System.Numerics;
+using Content.Shared.Body;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Humanoid.Prototypes;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Humanoid;
 
@@ -12,18 +14,6 @@ namespace Content.Shared.Humanoid;
 [Serializable, NetSerializable]
 public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, IEquatable<HumanoidCharacterAppearance>
 {
-    [DataField("hair")]
-    public string HairStyleId { get; set; } = HairStyles.DefaultHairStyle;
-
-    [DataField]
-    public Color HairColor { get; set; } = Color.Black;
-
-    [DataField("facialHair")]
-    public string FacialHairStyleId { get; set; } = HairStyles.DefaultFacialHairStyle;
-
-    [DataField]
-    public Color FacialHairColor { get; set; } = Color.Black;
-
     [DataField]
     public Color EyeColor { get; set; } = Color.Black;
 
@@ -31,67 +21,40 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance,
     public Color SkinColor { get; set; } = Color.FromHsv(new Vector4(0.07f, 0.2f, 1f, 1f));
 
     [DataField]
-    public List<Marking> Markings { get; set; } = new();
+    public Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> Markings { get; set; } = new();
 
-    public HumanoidCharacterAppearance(string hairStyleId,
-        Color hairColor,
-        string facialHairStyleId,
-        Color facialHairColor,
+    public HumanoidCharacterAppearance(
         Color eyeColor,
         Color skinColor,
-        List<Marking> markings)
+        Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> markings)
     {
-        HairStyleId = hairStyleId;
-        HairColor = ClampColor(hairColor);
-        FacialHairStyleId = facialHairStyleId;
-        FacialHairColor = ClampColor(facialHairColor);
         EyeColor = ClampColor(eyeColor);
         SkinColor = ClampColor(skinColor);
         Markings = markings;
     }
 
     public HumanoidCharacterAppearance(HumanoidCharacterAppearance other) :
-        this(other.HairStyleId, other.HairColor, other.FacialHairStyleId, other.FacialHairColor, other.EyeColor, other.SkinColor, new(other.Markings))
-    {
-
-    }
-
-    public HumanoidCharacterAppearance WithHairStyleName(string newName)
-    {
-        return new(newName, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings);
-    }
-
-    public HumanoidCharacterAppearance WithHairColor(Color newColor)
-    {
-        return new(HairStyleId, newColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings);
-    }
-
-    public HumanoidCharacterAppearance WithFacialHairStyleName(string newName)
+        this(other.EyeColor, other.SkinColor, new(other.Markings))
     {
-        return new(HairStyleId, HairColor, newName, FacialHairColor, EyeColor, SkinColor, Markings);
-    }
 
-    public HumanoidCharacterAppearance WithFacialHairColor(Color newColor)
-    {
-        return new(HairStyleId, HairColor, FacialHairStyleId, newColor, EyeColor, SkinColor, Markings);
     }
 
     public HumanoidCharacterAppearance WithEyeColor(Color newColor)
     {
-        return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, newColor, SkinColor, Markings);
+        return new(newColor, SkinColor, Markings);
     }
 
     public HumanoidCharacterAppearance WithSkinColor(Color newColor)
     {
-        return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, newColor, Markings);
+        return new(EyeColor, newColor, Markings);
     }
 
-    public HumanoidCharacterAppearance WithMarkings(List<Marking> newMarkings)
+    public HumanoidCharacterAppearance WithMarkings(Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> newMarkings)
     {
-        return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, newMarkings);
+        return new(EyeColor, SkinColor, newMarkings);
     }
 
-    public static HumanoidCharacterAppearance DefaultWithSpecies(string species)
+    public static HumanoidCharacterAppearance DefaultWithSpecies(ProtoId<SpeciesPrototype> species, Sex sex)
     {
         var protoMan = IoCManager.Resolve<IPrototypeManager>();
         var speciesPrototype = protoMan.Index<SpeciesPrototype>(species);
@@ -103,15 +66,12 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance,
             _ => skinColoration.ClosestSkinColor(speciesPrototype.DefaultSkinTone),
         };
 
-        return new(
-            HairStyles.DefaultHairStyle,
-            Color.Black,
-            HairStyles.DefaultFacialHairStyle,
-            Color.Black,
+        var appearance = new HumanoidCharacterAppearance(
             Color.Black,
             skinColor,
             new()
         );
+        return EnsureValid(appearance, species, sex);
     }
 
     private static IReadOnlyList<Color> _realisticEyeColors = new List<Color>
@@ -127,22 +87,6 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance,
     {
         var random = IoCManager.Resolve<IRobustRandom>();
         var markingManager = IoCManager.Resolve<MarkingManager>();
-        var hairStyles = markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.Hair, species).Keys.ToList();
-        var facialHairStyles = markingManager.MarkingsByCategoryAndSpecies(MarkingCategories.FacialHair, species).Keys.ToList();
-
-        var newHairStyle = hairStyles.Count > 0
-            ? random.Pick(hairStyles)
-            : HairStyles.DefaultHairStyle.Id;
-
-        var newFacialHairStyle = facialHairStyles.Count == 0 || sex == Sex.Female
-            ? HairStyles.DefaultFacialHairStyle.Id
-            : random.Pick(facialHairStyles);
-
-        var newHairColor = random.Pick(HairStyles.RealisticHairColors);
-        newHairColor = newHairColor
-            .WithRed(RandomizeColor(newHairColor.R))
-            .WithGreen(RandomizeColor(newHairColor.G))
-            .WithBlue(RandomizeColor(newHairColor.B));
 
         // TODO: Add random markings
 
@@ -159,7 +103,7 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance,
             _ => strategy.ClosestSkinColor(new Color(random.NextFloat(1), random.NextFloat(1), random.NextFloat(1), 1)),
         };
 
-        return new HumanoidCharacterAppearance(newHairStyle, newHairColor, newFacialHairStyle, newHairColor, newEyeColor, newSkinColor, new());
+        return new HumanoidCharacterAppearance(newEyeColor, newSkinColor, new());
 
         float RandomizeColor(float channel)
         {
@@ -172,62 +116,59 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance,
         return new(color.RByte, color.GByte, color.BByte);
     }
 
-    public static HumanoidCharacterAppearance EnsureValid(HumanoidCharacterAppearance appearance, string species, Sex sex)
+    public static HumanoidCharacterAppearance EnsureValid(HumanoidCharacterAppearance appearance, ProtoId<SpeciesPrototype> species, Sex sex)
     {
-        var hairStyleId = appearance.HairStyleId;
-        var facialHairStyleId = appearance.FacialHairStyleId;
-
-        var hairColor = ClampColor(appearance.HairColor);
-        var facialHairColor = ClampColor(appearance.FacialHairColor);
         var eyeColor = ClampColor(appearance.EyeColor);
 
         var proto = IoCManager.Resolve<IPrototypeManager>();
         var markingManager = IoCManager.Resolve<MarkingManager>();
 
-        if (!markingManager.MarkingsByCategory(MarkingCategories.Hair).ContainsKey(hairStyleId))
-        {
-            hairStyleId = HairStyles.DefaultHairStyle;
-        }
-
-        if (!markingManager.MarkingsByCategory(MarkingCategories.FacialHair).ContainsKey(facialHairStyleId))
-        {
-            facialHairStyleId = HairStyles.DefaultFacialHairStyle;
-        }
-
-        var markingSet = new MarkingSet();
         var skinColor = appearance.SkinColor;
-        if (proto.TryIndex(species, out SpeciesPrototype? speciesProto))
-        {
-            markingSet = new MarkingSet(appearance.Markings, speciesProto.MarkingPoints, markingManager, proto);
-            markingSet.EnsureValid(markingManager);
+        var validatedMarkings = appearance.Markings.ShallowClone();
 
+        if (proto.TryIndex(species, out var speciesProto))
+        {
             var strategy = proto.Index(speciesProto.SkinColoration).Strategy;
+            var organs = markingManager.GetOrgans(species);
             skinColor = strategy.EnsureVerified(skinColor);
 
-            markingSet.EnsureSpecies(species, skinColor, markingManager);
-            markingSet.EnsureSexes(sex, markingManager);
+            foreach (var (organ, markings) in appearance.Markings)
+            {
+                if (!organs.ContainsKey(organ))
+                    validatedMarkings.Remove(organ);
+            }
+
+            foreach (var (organ, organProtoID) in organs)
+            {
+                if (!markingManager.TryGetMarkingData(organProtoID, out var organData))
+                {
+                    validatedMarkings.Remove(organ);
+                    continue;
+                }
+
+                var actualMarkings = appearance.Markings.GetValueOrDefault(organ)?.ShallowClone() ?? [];
+
+                markingManager.EnsureValidColors(actualMarkings);
+                markingManager.EnsureValidGroupAndSex(actualMarkings, organData.Value.Group, sex);
+                markingManager.EnsureValidLayers(actualMarkings, organData.Value.Layers);
+                markingManager.EnsureValidLimits(actualMarkings, organData.Value.Group, organData.Value.Layers, skinColor, eyeColor);
+
+                validatedMarkings[organ] = actualMarkings;
+            }
         }
 
         return new HumanoidCharacterAppearance(
-            hairStyleId,
-            hairColor,
-            facialHairStyleId,
-            facialHairColor,
             eyeColor,
             skinColor,
-            markingSet.GetForwardEnumerator().ToList());
+            validatedMarkings);
     }
 
     public bool MemberwiseEquals(ICharacterAppearance maybeOther)
     {
         if (maybeOther is not HumanoidCharacterAppearance other) return false;
-        if (HairStyleId != other.HairStyleId) return false;
-        if (!HairColor.Equals(other.HairColor)) return false;
-        if (FacialHairStyleId != other.FacialHairStyleId) return false;
-        if (!FacialHairColor.Equals(other.FacialHairColor)) return false;
         if (!EyeColor.Equals(other.EyeColor)) return false;
         if (!SkinColor.Equals(other.SkinColor)) return false;
-        if (!Markings.SequenceEqual(other.Markings)) return false;
+        if (!MarkingManager.MarkingsAreEqual(Markings, other.Markings)) return false;
         return true;
     }
 
@@ -235,13 +176,9 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance,
     {
         if (ReferenceEquals(null, other)) return false;
         if (ReferenceEquals(this, other)) return true;
-        return HairStyleId == other.HairStyleId &&
-               HairColor.Equals(other.HairColor) &&
-               FacialHairStyleId == other.FacialHairStyleId &&
-               FacialHairColor.Equals(other.FacialHairColor) &&
-               EyeColor.Equals(other.EyeColor) &&
+        return EyeColor.Equals(other.EyeColor) &&
                SkinColor.Equals(other.SkinColor) &&
-               Markings.SequenceEqual(other.Markings);
+               MarkingManager.MarkingsAreEqual(Markings, other.Markings);
     }
 
     public override bool Equals(object? obj)
@@ -251,7 +188,7 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance,
 
     public override int GetHashCode()
     {
-        return HashCode.Combine(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings);
+        return HashCode.Combine(EyeColor, SkinColor, Markings);
     }
 
     public HumanoidCharacterAppearance Clone()
diff --git a/Content.Shared/Humanoid/HumanoidProfileComponent.cs b/Content.Shared/Humanoid/HumanoidProfileComponent.cs
new file mode 100644 (file)
index 0000000..d050e24
--- /dev/null
@@ -0,0 +1,27 @@
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Preferences;
+using Robust.Shared.Enums;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Humanoid;
+
+/// <summary>
+/// Dictates what species and age this character "looks like"
+/// </summary>
+[NetworkedComponent, RegisterComponent, AutoGenerateComponentState(true)]
+[Access(typeof(HumanoidProfileSystem))]
+public sealed partial class HumanoidProfileComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public Gender Gender;
+
+    [DataField, AutoNetworkedField]
+    public Sex Sex;
+
+    [DataField, AutoNetworkedField]
+    public int Age = 18;
+
+    [DataField, AutoNetworkedField]
+    public ProtoId<SpeciesPrototype> Species = HumanoidCharacterProfile.DefaultSpecies;
+}
diff --git a/Content.Shared/Humanoid/HumanoidProfileExportV1.cs b/Content.Shared/Humanoid/HumanoidProfileExportV1.cs
new file mode 100644 (file)
index 0000000..59b48ab
--- /dev/null
@@ -0,0 +1,125 @@
+using System.Numerics;
+using Content.Shared.Humanoid.Markings;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Roles;
+using Content.Shared.Traits;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Humanoid;
+
+/// <summary>
+/// Holds all of the data for importing / exporting character profiles.
+/// </summary>
+[DataDefinition]
+public sealed partial class HumanoidProfileExportV1
+{
+    [DataField]
+    public string ForkId;
+
+    [DataField]
+    public int Version = 1;
+
+    [DataField(required: true)]
+    public HumanoidCharacterProfileV1 Profile = default!;
+
+    public HumanoidProfileExportV2 ToV2()
+    {
+        return new()
+        {
+            ForkId = ForkId,
+            Version = 2,
+            Profile = Profile.ToV2()
+        };
+    }
+}
+
+[DataDefinition, Serializable]
+public sealed partial class HumanoidCharacterProfileV1
+{
+    [DataField("_jobPriorities")]
+    public Dictionary<ProtoId<JobPrototype>, JobPriority> JobPriorities = new();
+
+    [DataField("_antagPreferences")]
+    public HashSet<ProtoId<AntagPrototype>> AntagPreferences = new();
+
+    [DataField("_traitPreferences")]
+    public HashSet<ProtoId<TraitPrototype>> TraitPreferences = new();
+
+    [DataField("_loadouts")]
+    public Dictionary<string, RoleLoadout> Loadouts = new();
+
+    [DataField]
+    public string Name;
+
+    [DataField]
+    public string FlavorText;
+
+    [DataField]
+    public ProtoId<SpeciesPrototype> Species;
+
+    [DataField]
+    public int Age;
+
+    [DataField]
+    public Sex Sex;
+
+    [DataField]
+    public Gender Gender;
+
+    [DataField]
+    public HumanoidCharacterAppearanceV1 Appearance;
+
+    [DataField]
+    public SpawnPriorityPreference SpawnPriority;
+
+    [DataField]
+    public PreferenceUnavailableMode PreferenceUnavailable;
+
+    public HumanoidCharacterProfile ToV2()
+    {
+        return new(Name, FlavorText, Species, Age, Sex, Gender, Appearance.ToV2(Species), SpawnPriority, JobPriorities, PreferenceUnavailable, AntagPreferences, TraitPreferences, Loadouts);
+    }
+}
+
+
+[DataDefinition, Serializable]
+public sealed partial class HumanoidCharacterAppearanceV1
+{
+    [DataField("hair")]
+    public string HairStyleId;
+
+    [DataField]
+    public Color HairColor;
+
+    [DataField("facialHair")]
+    public string FacialHairStyleId;
+
+    [DataField]
+    public Color FacialHairColor;
+
+    [DataField]
+    public Color EyeColor;
+
+    [DataField]
+    public Color SkinColor;
+
+    [DataField]
+    public List<Marking> Markings = new();
+
+    public HumanoidCharacterAppearance ToV2(ProtoId<SpeciesPrototype> species)
+    {
+        var markingManager = IoCManager.Resolve<MarkingManager>();
+
+        var incomingMarkings = Markings.ShallowClone();
+        if (HairStyleId != string.Empty)
+            incomingMarkings.Add(new(HairStyleId, new List<Color>() { HairColor }));
+        if (FacialHairStyleId != string.Empty)
+            incomingMarkings.Add(new(FacialHairStyleId, new List<Color>() { FacialHairColor }));
+
+        return new HumanoidCharacterAppearance(EyeColor, SkinColor, markingManager.ConvertMarkings(incomingMarkings, species));
+    }
+}
similarity index 80%
rename from Content.Shared/Humanoid/HumanoidProfileExport.cs
rename to Content.Shared/Humanoid/HumanoidProfileExportV2.cs
index 2b7f9acad6a11e5204593b02d191f65d32ad469b..4e4a3f7349c5774f4646b8b2b9fe78d8c9298888 100644 (file)
@@ -6,13 +6,13 @@ namespace Content.Shared.Humanoid;
 /// Holds all of the data for importing / exporting character profiles.
 /// </summary>
 [DataDefinition]
-public sealed partial class HumanoidProfileExport
+public sealed partial class HumanoidProfileExportV2
 {
     [DataField]
     public string ForkId;
 
     [DataField]
-    public int Version = 1;
+    public int Version = 2;
 
     [DataField(required: true)]
     public HumanoidCharacterProfile Profile = default!;
diff --git a/Content.Shared/Humanoid/HumanoidProfileSystem.cs b/Content.Shared/Humanoid/HumanoidProfileSystem.cs
new file mode 100644 (file)
index 0000000..881fd63
--- /dev/null
@@ -0,0 +1,83 @@
+using Content.Shared.Examine;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Preferences;
+using Robust.Shared.GameObjects.Components.Localization;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Humanoid;
+
+public sealed class HumanoidProfileSystem : EntitySystem
+{
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+    [Dependency] private readonly GrammarSystem _grammar = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<HumanoidProfileComponent, ExaminedEvent>(OnExamined);
+    }
+
+    public void ApplyProfileTo(Entity<HumanoidProfileComponent?> ent, HumanoidCharacterProfile profile)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return;
+
+        ent.Comp.Gender = profile.Gender;
+        ent.Comp.Age = profile.Age;
+        ent.Comp.Species = profile.Species;
+        ent.Comp.Sex = profile.Sex;
+        Dirty(ent);
+
+        if (TryComp<GrammarComponent>(ent, out var grammar))
+        {
+            _grammar.SetGender((ent, grammar), profile.Gender);
+        }
+    }
+
+    private void OnExamined(Entity<HumanoidProfileComponent> ent, ref ExaminedEvent args)
+    {
+        var identity = Identity.Entity(ent, EntityManager);
+        var species = GetSpeciesRepresentation(ent.Comp.Species).ToLower();
+        var age = GetAgeRepresentation(ent.Comp.Species, ent.Comp.Age);
+
+        args.PushText(Loc.GetString("humanoid-appearance-component-examine", ("user", identity), ("age", age), ("species", species)));
+    }
+
+    /// <summary>
+    /// Takes ID of the species prototype, returns UI-friendly name of the species.
+    /// </summary>
+    public string GetSpeciesRepresentation(ProtoId<SpeciesPrototype> species)
+    {
+        if (_prototype.TryIndex(species, out var speciesPrototype))
+            return Loc.GetString(speciesPrototype.Name);
+
+        Log.Error("Tried to get representation of unknown species: {speciesId}");
+        return Loc.GetString("humanoid-appearance-component-unknown-species");
+    }
+
+    /// <summary>
+    /// Takes ID of the species prototype and an age, returns an approximate description
+    /// </summary>
+    public string GetAgeRepresentation(ProtoId<SpeciesPrototype> species, int age)
+    {
+        if (!_prototype.TryIndex(species, out var speciesPrototype))
+        {
+            Log.Error("Tried to get age representation of species that couldn't be indexed: " + species);
+            return Loc.GetString("identity-age-young");
+        }
+
+        if (age < speciesPrototype.YoungAge)
+        {
+            return Loc.GetString("identity-age-young");
+        }
+
+        if (age < speciesPrototype.OldAge)
+        {
+            return Loc.GetString("identity-age-middle-aged");
+        }
+
+        return Loc.GetString("identity-age-old");
+    }
+}
index 6b5f16300082225a14d743f18eb9aced2dd589c7..c041f75d81c843af9020e2076a181deb1cb66f0e 100644 (file)
@@ -27,6 +27,7 @@ namespace Content.Shared.Humanoid
         LLeg,
         RFoot,
         LFoot,
+        Overlay,
         Handcuffs,
         StencilMask,
         Ensnare,
index 1f71b9a461854f3a4e01c52308a36027ceadcc20..be7e56510201c53247d6b3aff23c1143763193ca 100644 (file)
@@ -1,6 +1,6 @@
 using System.Linq;
 
-namespace Content.Shared.Humanoid.Markings;
+namespace Content.Shared.Humanoid.Markings.ColoringTypes;
 
 /// <summary>
 ///     Colors marking in color of first defined marking from specified category (in e.x. from Hair category)
@@ -8,15 +8,15 @@ namespace Content.Shared.Humanoid.Markings;
 public sealed partial class CategoryColoring : LayerColoringType
 {
     [DataField("category", required: true)]
-    public MarkingCategories Category;
+    public HumanoidVisualLayers Category;
 
-    public override Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet)
+    public override Color? GetCleanColor(Color? skin, Color? eyes, List<Marking> otherMarkings)
     {
         Color? outColor = null;
-        if (markingSet.TryGetCategory(Category, out var markings) &&
-            markings.Count > 0)
+
+        if (otherMarkings.Count > 0)
         {
-            outColor = markings[0].MarkingColors.FirstOrDefault();
+            outColor = otherMarkings[0].MarkingColors.FirstOrDefault();
         }
 
         return outColor;
index 0b18653a4c1515b54dbe226455961962179f0b43..8132532b2f98c232de58e0cca61fef22ff9661bc 100644 (file)
@@ -1,11 +1,11 @@
-namespace Content.Shared.Humanoid.Markings;
+namespace Content.Shared.Humanoid.Markings.ColoringTypes;
 
 /// <summary>
 ///     Colors layer in an eye color
 /// </summary>
 public sealed partial class EyeColoring : LayerColoringType
 {
-    public override Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet)
+    public override Color? GetCleanColor(Color? skin, Color? eyes, List<Marking> otherMarkings)
     {
         return eyes;
     }
index 9051c26dd7efb72f191d81bda683e8a33ad60593..f1d161657bf89805181cac19b7afff5892f0d11b 100644 (file)
@@ -1,4 +1,4 @@
-namespace Content.Shared.Humanoid.Markings;
+namespace Content.Shared.Humanoid.Markings.ColoringTypes;
 
 /// <summary>
 ///     Colors layer in a specified color
@@ -8,7 +8,7 @@ public sealed partial class SimpleColoring : LayerColoringType
     [DataField("color", required: true)]
     public Color Color = Color.White;
 
-    public override Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet)
+    public override Color? GetCleanColor(Color? skin, Color? eyes, List<Marking> otherMarkings)
     {
         return Color;
     }
index 79a09a39cefd8642ea8515b458b3132d788e29cc..ec602417d30091fc6c38b55304390a7fe7d773a4 100644 (file)
@@ -1,11 +1,11 @@
-namespace Content.Shared.Humanoid.Markings;
+namespace Content.Shared.Humanoid.Markings.ColoringTypes;
 
 /// <summary>
 ///     Colors layer in a skin color
 /// </summary>
 public sealed partial class SkinColoring : LayerColoringType
 {
-    public override Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet)
+    public override Color? GetCleanColor(Color? skin, Color? eyes, List<Marking> otherMarkings)
     {
         return skin;
     }
index 51962cef3bfdc4fe3d64906d9b51b2edf117749d..333aaef008b0fd9d0de985a5aa14172b358e1d36 100644 (file)
@@ -1,11 +1,11 @@
-namespace Content.Shared.Humanoid.Markings;
+namespace Content.Shared.Humanoid.Markings.ColoringTypes;
 
 /// <summary>
 ///     Colors layer in skin color but much darker.
 /// </summary>
 public sealed partial class TattooColoring : LayerColoringType
 {
-    public override Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet)
+    public override Color? GetCleanColor(Color? skin, Color? eye, List<Marking> otherMarkings)
     {
         if (skin == null)
         {
index 767d4a6482dce19ff785b78bd24262d8f45bcd78..3c1e868e77396e7522b9e92ca0a8f513d39a836a 100644 (file)
@@ -42,7 +42,6 @@ namespace Content.Shared.Humanoid.Markings
         {
             MarkingId = other.MarkingId;
             _markingColors = new(other.MarkingColors);
-            Visible = other.Visible;
             Forced = other.Forced;
         }
 
@@ -58,12 +57,6 @@ namespace Content.Shared.Humanoid.Markings
         [ViewVariables]
         public IReadOnlyList<Color> MarkingColors => _markingColors;
 
-        /// <summary>
-        ///     If this marking is currently visible.
-        /// </summary>
-        [DataField("visible")]
-        public bool Visible = true;
-
         /// <summary>
         ///     If this marking should be forcefully applied, regardless of points.
         /// </summary>
@@ -107,7 +100,6 @@ namespace Content.Shared.Humanoid.Markings
             }
             return MarkingId.Equals(other.MarkingId)
                 && _markingColors.SequenceEqual(other._markingColors)
-                && Visible.Equals(other.Visible)
                 && Forced.Equals(other.Forced);
         }
 
diff --git a/Content.Shared/Humanoid/Markings/MarkingCategories.cs b/Content.Shared/Humanoid/Markings/MarkingCategories.cs
deleted file mode 100644 (file)
index a49ac7d..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Humanoid.Markings
-{
-    [Serializable, NetSerializable]
-    public enum MarkingCategories : byte
-    {
-        Special,
-        Hair,
-        FacialHair,
-        Head,
-        HeadTop,
-        HeadSide,
-        Snout,
-        SnoutCover,
-        Chest,
-        UndergarmentTop,
-        UndergarmentBottom,
-        Arms,
-        Legs,
-        Tail,
-        Overlay
-    }
-
-    public static class MarkingCategoriesConversion
-    {
-        public static MarkingCategories FromHumanoidVisualLayers(HumanoidVisualLayers layer)
-        {
-            return layer switch
-            {
-                HumanoidVisualLayers.Special => MarkingCategories.Special,
-                HumanoidVisualLayers.Hair => MarkingCategories.Hair,
-                HumanoidVisualLayers.FacialHair => MarkingCategories.FacialHair,
-                HumanoidVisualLayers.Head => MarkingCategories.Head,
-                HumanoidVisualLayers.HeadTop => MarkingCategories.HeadTop,
-                HumanoidVisualLayers.HeadSide => MarkingCategories.HeadSide,
-                HumanoidVisualLayers.Snout => MarkingCategories.Snout,
-                HumanoidVisualLayers.Chest => MarkingCategories.Chest,
-                HumanoidVisualLayers.UndergarmentTop => MarkingCategories.UndergarmentTop,
-                HumanoidVisualLayers.UndergarmentBottom => MarkingCategories.UndergarmentBottom,
-                HumanoidVisualLayers.RArm => MarkingCategories.Arms,
-                HumanoidVisualLayers.LArm => MarkingCategories.Arms,
-                HumanoidVisualLayers.RHand => MarkingCategories.Arms,
-                HumanoidVisualLayers.LHand => MarkingCategories.Arms,
-                HumanoidVisualLayers.LLeg => MarkingCategories.Legs,
-                HumanoidVisualLayers.RLeg => MarkingCategories.Legs,
-                HumanoidVisualLayers.LFoot => MarkingCategories.Legs,
-                HumanoidVisualLayers.RFoot => MarkingCategories.Legs,
-                HumanoidVisualLayers.Tail => MarkingCategories.Tail,
-                _ => MarkingCategories.Overlay
-            };
-        }
-    }
-}
index fa47475a232b75666e647e967e9fd87df0f3ac3e..518ea046fb7229bd0be49e5bdd49d4d809ad1172 100644 (file)
@@ -31,13 +31,13 @@ public static class MarkingColoring
         MarkingPrototype prototype,
         Color? skinColor,
         Color? eyeColor,
-        MarkingSet markingSet
+        List<Marking> otherMarkings
     )
     {
         var colors = new List<Color>();
 
         // Coloring from default properties
-        var defaultColor = prototype.Coloring.Default.GetColor(skinColor, eyeColor, markingSet);
+        var defaultColor = prototype.Coloring.Default.GetColor(skinColor, eyeColor, otherMarkings);
 
         if (prototype.Coloring.Layers == null)
         {
@@ -69,7 +69,7 @@ public static class MarkingColoring
                 // All specified layers must be colored separately, all unspecified must depend on default coloring
                 if (prototype.Coloring.Layers.TryGetValue(name, out var layerColoring))
                 {
-                    var marking_color = layerColoring.GetColor(skinColor, eyeColor, markingSet);
+                    var marking_color = layerColoring.GetColor(skinColor, eyeColor, otherMarkings);
                     colors.Add(marking_color);
                 }
                 else
@@ -89,7 +89,7 @@ public static class MarkingColoring
 public sealed partial class LayerColoringDefinition
 {
     [DataField("type")]
-    public LayerColoringType? Type = new SkinColoring();
+    public LayerColoringType? Type = new ColoringTypes.SkinColoring();
 
     /// <summary>
     ///     Coloring types that will be used if main coloring type will return nil
@@ -103,16 +103,16 @@ public sealed partial class LayerColoringDefinition
     [DataField("fallbackColor")]
     public Color FallbackColor = Color.White;
 
-    public Color GetColor(Color? skin, Color? eyes, MarkingSet markingSet)
+    public Color GetColor(Color? skin, Color? eyes, List<Marking> otherMarkings)
     {
         Color? color = null;
         if (Type != null)
-            color = Type.GetColor(skin, eyes, markingSet);
+            color = Type.GetColor(skin, eyes, otherMarkings);
         if (color == null)
         {
             foreach (var type in FallbackTypes)
             {
-                color = type.GetColor(skin, eyes, markingSet);
+                color = type.GetColor(skin, eyes, otherMarkings);
                 if (color != null) break;
             }
         }
@@ -131,10 +131,10 @@ public abstract partial class LayerColoringType
     /// </summary>
     [DataField("negative")]
     public bool Negative { get; private set; } = false;
-    public abstract Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet);
-    public Color? GetColor(Color? skin, Color? eyes, MarkingSet markingSet)
+    public abstract Color? GetCleanColor(Color? skin, Color? eyes, List<Marking> otherMarkings);
+    public Color? GetColor(Color? skin, Color? eyes, List<Marking> otherMarkings)
     {
-        var color = GetCleanColor(skin, eyes, markingSet);
+        var color = GetCleanColor(skin, eyes, otherMarkings);
         // Negative color
         if (color != null && Negative)
         {
index 28637f930349693b74b7b59cf211ebbf86b156e6..175d1f978b984d2169a63f0a1c7bcd14b7c64095 100644 (file)
 using System.Collections.Frozen;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
+using Content.Shared.Body;
 using Content.Shared.Humanoid.Prototypes;
 using Robust.Shared.Prototypes;
 
-namespace Content.Shared.Humanoid.Markings
+namespace Content.Shared.Humanoid.Markings;
+
+public sealed class MarkingManager
 {
-    public sealed class MarkingManager
+    [Dependency] private readonly IComponentFactory _component = default!;
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+    private FrozenDictionary<HumanoidVisualLayers, FrozenDictionary<string, MarkingPrototype>> _categorizedMarkings = default!;
+    private FrozenDictionary<string, MarkingPrototype> _markings = default!;
+
+    public void Initialize()
     {
-        [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+        _prototype.PrototypesReloaded += OnPrototypeReload;
+        CachePrototypes();
+    }
 
-        private readonly List<MarkingPrototype> _index = new();
-        public FrozenDictionary<MarkingCategories, FrozenDictionary<string, MarkingPrototype>> CategorizedMarkings = default!;
-        public FrozenDictionary<string, MarkingPrototype> Markings = default!;
+    private void CachePrototypes()
+    {
+        var markingDict = new Dictionary<HumanoidVisualLayers, Dictionary<string, MarkingPrototype>>();
 
-        public void Initialize()
+        foreach (var category in Enum.GetValues<HumanoidVisualLayers>())
         {
-            _prototypeManager.PrototypesReloaded += OnPrototypeReload;
-            CachePrototypes();
+            markingDict.Add(category, new());
         }
 
-        private void CachePrototypes()
+        foreach (var prototype in _prototype.EnumeratePrototypes<MarkingPrototype>())
         {
-            _index.Clear();
-            var markingDict = new Dictionary<MarkingCategories, Dictionary<string, MarkingPrototype>>();
-
-            foreach (var category in Enum.GetValues<MarkingCategories>())
+            try
             {
-                markingDict.Add(category, new());
+                markingDict[prototype.BodyPart].Add(prototype.ID, prototype);
             }
-
-            foreach (var prototype in _prototypeManager.EnumeratePrototypes<MarkingPrototype>())
+            catch (Exception e)
             {
-                _index.Add(prototype);
-                markingDict[prototype.MarkingCategory].Add(prototype.ID, prototype);
+                throw new Exception($"failed to process {prototype.ID}", e);
             }
-
-            Markings = _prototypeManager.EnumeratePrototypes<MarkingPrototype>().ToFrozenDictionary(x => x.ID);
-            CategorizedMarkings = markingDict.ToFrozenDictionary(
-                x => x.Key,
-                x => x.Value.ToFrozenDictionary());
         }
 
-        public FrozenDictionary<string, MarkingPrototype> MarkingsByCategory(MarkingCategories category)
+        _markings = _prototype.EnumeratePrototypes<MarkingPrototype>().ToFrozenDictionary(x => x.ID);
+        _categorizedMarkings = markingDict.ToFrozenDictionary(
+            x => x.Key,
+            x => x.Value.ToFrozenDictionary());
+    }
+
+    public FrozenDictionary<string, MarkingPrototype> MarkingsByLayer(HumanoidVisualLayers category)
+    {
+        // all marking categories are guaranteed to have a dict entry
+        return _categorizedMarkings[category];
+    }
+
+    /// <summary>
+    ///     Markings by category, species and sex.
+    /// </summary>
+    /// <remarks>
+    ///     This is done per category, as enumerating over every single marking by group isn't useful.
+    ///     Please make a pull request if you find a use case for that behavior.
+    /// </remarks>
+    /// <returns></returns>
+    public IReadOnlyDictionary<string, MarkingPrototype> MarkingsByLayerAndGroupAndSex(HumanoidVisualLayers layer,
+        ProtoId<MarkingsGroupPrototype> group,
+        Sex sex)
+    {
+        var groupProto = _prototype.Index(group);
+        var whitelisted = groupProto.Limits.GetValueOrDefault(layer)?.OnlyGroupWhitelisted ?? groupProto.OnlyGroupWhitelisted;
+        var res = new Dictionary<string, MarkingPrototype>();
+
+        foreach (var (key, marking) in MarkingsByLayer(layer))
         {
-            // all marking categories are guaranteed to have a dict entry
-            return CategorizedMarkings[category];
+            if (!CanBeApplied(groupProto, sex, marking, whitelisted))
+                continue;
+
+            res.Add(key, marking);
         }
 
-        /// <summary>
-        ///     Markings by category and species.
-        /// </summary>
-        /// <param name="category"></param>
-        /// <param name="species"></param>
-        /// <remarks>
-        ///     This is done per category, as enumerating over every single marking by species isn't useful.
-        ///     Please make a pull request if you find a use case for that behavior.
-        /// </remarks>
-        /// <returns></returns>
-        public IReadOnlyDictionary<string, MarkingPrototype> MarkingsByCategoryAndSpecies(MarkingCategories category,
-            string species)
+        return res;
+    }
+
+    public bool TryGetMarking(Marking marking, [NotNullWhen(true)] out MarkingPrototype? markingResult)
+    {
+        return _markings.TryGetValue(marking.MarkingId, out markingResult);
+    }
+
+    private void OnPrototypeReload(PrototypesReloadedEventArgs args)
+    {
+        if (args.WasModified<MarkingPrototype>())
+            CachePrototypes();
+    }
+
+
+    public bool CanBeApplied(ProtoId<MarkingsGroupPrototype> group, Sex sex, MarkingPrototype prototype)
+    {
+        var groupProto = _prototype.Index(group);
+        var whitelisted = groupProto.Limits.GetValueOrDefault(prototype.BodyPart)?.OnlyGroupWhitelisted ?? groupProto.OnlyGroupWhitelisted;
+
+        return CanBeApplied(groupProto, sex, prototype, whitelisted);
+    }
+
+    private bool CanBeApplied(MarkingsGroupPrototype group, Sex sex, MarkingPrototype prototype, bool whitelisted)
+    {
+        if (prototype.GroupWhitelist == null)
         {
-            var speciesProto = _prototypeManager.Index<SpeciesPrototype>(species);
-            var markingPoints = _prototypeManager.Index(speciesProto.MarkingPoints);
-            var res = new Dictionary<string, MarkingPrototype>();
+            if (whitelisted)
+                return false;
+        }
+        else
+        {
+            if (!prototype.GroupWhitelist.Contains(group))
+                return false;
+        }
 
-            foreach (var (key, marking) in MarkingsByCategory(category))
+        return prototype.SexRestriction == null || prototype.SexRestriction == sex;
+    }
+
+    /// <summary>
+    /// Ensures that the <see cref="markingSets"/> have a valid amount of colors
+    /// </summary>
+    public void EnsureValidColors(Dictionary<HumanoidVisualLayers, List<Marking>> markingSets)
+    {
+        foreach (var markings in markingSets.Values)
+        {
+            for (var i = markings.Count - 1; i >= 0; i--)
             {
-                if ((markingPoints.OnlyWhitelisted || markingPoints.Points[category].OnlyWhitelisted) && marking.SpeciesRestrictions == null)
+                if (!TryGetMarking(markings[i], out var marking))
                 {
+                    markings.RemoveAt(i);
                     continue;
                 }
 
-                if (marking.SpeciesRestrictions != null && !marking.SpeciesRestrictions.Contains(species))
+                if (marking.Sprites.Count != markings[i].MarkingColors.Count)
                 {
-                    continue;
+                    markings[i] = new Marking(marking.ID, marking.Sprites.Count);
                 }
-                res.Add(key, marking);
             }
-
-            return res;
         }
+    }
 
-        /// <summary>
-        ///     Markings by category and sex.
-        /// </summary>
-        /// <param name="category"></param>
-        /// <param name="sex"></param>
-        /// <remarks>
-        ///     This is done per category, as enumerating over every single marking by species isn't useful.
-        ///     Please make a pull request if you find a use case for that behavior.
-        /// </remarks>
-        /// <returns></returns>
-        public IReadOnlyDictionary<string, MarkingPrototype> MarkingsByCategoryAndSex(MarkingCategories category,
-            Sex sex)
+    /// <summary>
+    /// Ensures that the <see cref="markingSets"/> are valid per the constraints on <see cref="group"/> and <see cref="sex"/>
+    /// </summary>
+    public void EnsureValidGroupAndSex(Dictionary<HumanoidVisualLayers, List<Marking>> markingSets, ProtoId<MarkingsGroupPrototype> group, Sex sex)
+    {
+        foreach (var markings in markingSets.Values)
         {
-            var res = new Dictionary<string, MarkingPrototype>();
-
-            foreach (var (key, marking) in MarkingsByCategory(category))
+            for (var i = markings.Count - 1; i >= 0; i--)
             {
-                if (marking.SexRestriction != null && marking.SexRestriction != sex)
-                {
-                    continue;
-                }
-
-                res.Add(key, marking);
+                if (!TryGetMarking(markings[i], out var marking) || !CanBeApplied(group, sex, marking))
+                    markings.RemoveAt(i);
             }
-
-            return res;
         }
+    }
 
-        /// <summary>
-        ///     Markings by category, species and sex.
-        /// </summary>
-        /// <param name="category"></param>
-        /// <param name="species"></param>
-        /// <param name="sex"></param>
-        /// <remarks>
-        ///     This is done per category, as enumerating over every single marking by species isn't useful.
-        ///     Please make a pull request if you find a use case for that behavior.
-        /// </remarks>
-        /// <returns></returns>
-        public IReadOnlyDictionary<string, MarkingPrototype> MarkingsByCategoryAndSpeciesAndSex(MarkingCategories category,
-            string species, Sex sex)
+    /// <summary>
+    /// Ensures that the <see cref="markingSets"/> only belong to the <see cref="layers"/>
+    /// </summary>
+    public void EnsureValidLayers(Dictionary<HumanoidVisualLayers, List<Marking>> markingSets, HashSet<HumanoidVisualLayers> layers)
+    {
+        foreach (var markings in markingSets.Values)
         {
-            var speciesProto = _prototypeManager.Index<SpeciesPrototype>(species);
-            var onlyWhitelisted = _prototypeManager.Index(speciesProto.MarkingPoints).OnlyWhitelisted;
-            var res = new Dictionary<string, MarkingPrototype>();
+            for (var i = markings.Count - 1; i >= 0; i--)
+            {
+                if (!TryGetMarking(markings[i], out var marking) || !layers.Contains(marking.BodyPart))
+                    markings.RemoveAt(i);
+            }
+        }
+    }
 
-            foreach (var (key, marking) in MarkingsByCategory(category))
+    /// <summary>
+    /// Ensures the list of <see cref="markingSets"/> is valid per the limits of the <see cref="group"/>
+    /// </summary>
+    public void EnsureValidLimits(Dictionary<HumanoidVisualLayers, List<Marking>> markingSets, ProtoId<MarkingsGroupPrototype> group, HashSet<HumanoidVisualLayers> layers, Color? skinColor, Color? eyeColor)
+    {
+        var groupProto = _prototype.Index(group);
+        var counts = new Dictionary<HumanoidVisualLayers, int>();
+
+        foreach (var (_, markings) in markingSets)
+        {
+            for (var i = markings.Count - 1; i >= 0; i--)
             {
-                if (onlyWhitelisted && marking.SpeciesRestrictions == null)
+                if (!TryGetMarking(markings[i], out var marking))
                 {
+                    markings.RemoveAt(i);
                     continue;
                 }
 
-                if (marking.SpeciesRestrictions != null && !marking.SpeciesRestrictions.Contains(species))
-                {
+                if (!groupProto.Limits.TryGetValue(marking.BodyPart, out var limit))
                     continue;
-                }
 
-                if (marking.SexRestriction != null && marking.SexRestriction != sex)
+                var count = counts.GetValueOrDefault(marking.BodyPart);
+                if (count >= limit.Limit)
                 {
+                    markings.RemoveAt(i);
                     continue;
                 }
 
-                res.Add(key, marking);
+                counts[marking.BodyPart] = counts.GetValueOrDefault(marking.BodyPart) + 1;
             }
-
-            return res;
         }
 
-        public bool TryGetMarking(Marking marking, [NotNullWhen(true)] out MarkingPrototype? markingResult)
+        foreach (var layer in layers)
         {
-            return Markings.TryGetValue(marking.MarkingId, out markingResult);
-        }
+            if (!groupProto.Limits.TryGetValue(layer, out var layerLimit))
+                continue;
 
-        /// <summary>
-        ///     Check if a marking is valid according to the category, species, and current data this marking has.
-        /// </summary>
-        /// <param name="marking"></param>
-        /// <param name="category"></param>
-        /// <param name="species"></param>
-        /// <param name="sex"></param>
-        /// <returns></returns>
-        public bool IsValidMarking(Marking marking, MarkingCategories category, string species, Sex sex)
-        {
-            if (!TryGetMarking(marking, out var proto))
-            {
-                return false;
-            }
+            var layerCounts = counts.GetValueOrDefault(layer);
+            if (layerCounts > 0 || !layerLimit.Required)
+                continue;
 
-            if (proto.MarkingCategory != category ||
-                proto.SpeciesRestrictions != null && !proto.SpeciesRestrictions.Contains(species) ||
-                proto.SexRestriction != null && proto.SexRestriction != sex)
+            foreach (var marking in layerLimit.Default)
             {
-                return false;
-            }
+                if (!_markings.TryGetValue(marking, out var markingProto))
+                    continue;
 
-            if (marking.MarkingColors.Count != proto.Sprites.Count)
-            {
-                return false;
+                markingSets[layer] = markingSets.GetValueOrDefault(layer) ?? [];
+                var colors = MarkingColoring.GetMarkingLayerColors(markingProto, skinColor, eyeColor, markingSets[layer]);
+                markingSets[layer].Add(new(marking, colors));
             }
-
-            return true;
         }
+    }
+
+    public Dictionary<ProtoId<OrganCategoryPrototype>, EntProtoId<OrganComponent>> GetOrgans(ProtoId<SpeciesPrototype> species)
+    {
+        var speciesPrototype = _prototype.Index(species);
+        var appearancePrototype = _prototype.Index(speciesPrototype.DollPrototype);
+
+        if (!appearancePrototype.TryGetComponent<InitialBodyComponent>(out var initialBody, _component))
+            return new();
+
+        return initialBody.Organs;
+    }
+
+    public Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData> GetMarkingData(ProtoId<SpeciesPrototype> species)
+    {
+        var ret = new Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData>();
 
-        private void OnPrototypeReload(PrototypesReloadedEventArgs args)
+        foreach (var (organ, proto) in GetOrgans(species))
         {
-            if (args.WasModified<MarkingPrototype>())
-                CachePrototypes();
+            if (!TryGetMarkingData(proto, out var organData))
+                continue;
+
+            ret[organ] = organData.Value;
         }
 
-        public bool CanBeApplied(string species, Sex sex, Marking marking, IPrototypeManager? prototypeManager = null)
-        {
-            IoCManager.Resolve(ref prototypeManager);
+        return ret;
+    }
 
-            var speciesProto = prototypeManager.Index<SpeciesPrototype>(species);
-            var onlyWhitelisted = prototypeManager.Index(speciesProto.MarkingPoints).OnlyWhitelisted;
+    public Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> GetProfileData(ProtoId<SpeciesPrototype> species,
+        Sex sex,
+        Color skinColor,
+        Color eyeColor)
+    {
+        var ret = new Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData>();
 
-            if (!TryGetMarking(marking, out var prototype))
+        foreach (var organ in GetOrgans(species).Keys)
+        {
+            ret[organ] = new()
             {
-                return false;
-            }
+                Sex = sex,
+                EyeColor = eyeColor,
+                SkinColor = skinColor,
+            };
+        }
 
-            if (onlyWhitelisted && prototype.SpeciesRestrictions == null)
-            {
-                return false;
-            }
+        return ret;
+    }
 
-            if (prototype.SpeciesRestrictions != null
-                && !prototype.SpeciesRestrictions.Contains(species))
-            {
-                return false;
-            }
+    public bool TryGetMarkingData(EntProtoId organ, [NotNullWhen(true)] out OrganMarkingData? organData)
+    {
+        organData = null;
 
-            if (prototype.SexRestriction != null && prototype.SexRestriction != sex)
-            {
-                return false;
-            }
+        if (!_prototype.TryIndex(organ, out var organProto))
+            return false;
 
-            return true;
-        }
+        if (!organProto.TryGetComponent<VisualOrganMarkingsComponent>(out var comp, _component))
+            return false;
 
-        public bool CanBeApplied(string species, Sex sex, MarkingPrototype prototype, IPrototypeManager? prototypeManager = null)
-        {
-            IoCManager.Resolve(ref prototypeManager);
+        organData = comp.MarkingData;
 
-            var speciesProto = prototypeManager.Index<SpeciesPrototype>(species);
-            var onlyWhitelisted = prototypeManager.Index(speciesProto.MarkingPoints).OnlyWhitelisted;
+        return true;
+    }
 
-            if (onlyWhitelisted && prototype.SpeciesRestrictions == null)
-            {
-                return false;
-            }
+    public Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> ConvertMarkings(List<Marking> markings,
+        ProtoId<SpeciesPrototype> species)
+    {
+        var ret = new Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>();
 
-            if (prototype.SpeciesRestrictions != null &&
-                !prototype.SpeciesRestrictions.Contains(species))
-            {
-                return false;
-            }
+        var data = GetMarkingData(species);
+        var layersToOrgans = data.SelectMany(kvp => kvp.Value.Layers.Select(layer => (layer, kvp.Key))).ToDictionary(pair => pair.layer, pair => pair.Key);
 
-            if (prototype.SexRestriction != null && prototype.SexRestriction != sex)
-            {
-                return false;
-            }
+        foreach (var marking in markings)
+        {
+            if (!_prototype.TryIndex<MarkingPrototype>(marking.MarkingId, out var markingProto))
+                continue;
+
+            if (!layersToOrgans.TryGetValue(markingProto.BodyPart, out var organ))
+                continue;
+
+            var organDict = ret.GetValueOrDefault(organ) ?? [];
+            ret[organ] = organDict;
+            var markingList = organDict.GetValueOrDefault(markingProto.BodyPart) ?? [];
+            organDict[markingProto.BodyPart] = markingList;
 
-            return true;
+            markingList.Add(marking);
         }
 
-        public bool MustMatchSkin(string species, HumanoidVisualLayers layer, out float alpha, IPrototypeManager? prototypeManager = null)
+        return ret;
+    }
+
+    /// <summary>
+    /// Recursively compares two markings dictionaries for equality.
+    /// </summary>
+    /// <param name="a">The first markings dictionary.</param>
+    /// <param name="b">The second markings dictionary.</param>
+    /// <returns>Whether the dictionaries are equivalent.</returns>
+    public static bool MarkingsAreEqual(Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> a,
+        Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> b)
+    {
+        if (a.Count != b.Count)
+            return false;
+
+        foreach (var (organ, aDictionary) in a)
         {
-            IoCManager.Resolve(ref prototypeManager);
-            var speciesProto = prototypeManager.Index<SpeciesPrototype>(species);
-            if (
-                !prototypeManager.Resolve(speciesProto.SpriteSet, out var baseSprites) ||
-                !baseSprites.Sprites.TryGetValue(layer, out var spriteName) ||
-                !prototypeManager.Resolve(spriteName, out HumanoidSpeciesSpriteLayer? sprite) ||
-                sprite == null ||
-                !sprite.MarkingsMatchSkin
-            )
-            {
-                alpha = 1f;
+            if (!b.TryGetValue(organ, out var bDictionary))
+                return false;
+
+            if (aDictionary.Count != bDictionary.Count)
                 return false;
-            }
 
-            alpha = sprite.LayerAlpha;
-            return true;
+            foreach (var (layer, aMarkings) in aDictionary)
+            {
+                if (!bDictionary.TryGetValue(layer, out var bMarkings))
+                    return false;
+
+                if (!aMarkings.SequenceEqual(bMarkings))
+                    return false;
+            }
         }
+
+        return true;
     }
 }
diff --git a/Content.Shared/Humanoid/Markings/MarkingPoints.cs b/Content.Shared/Humanoid/Markings/MarkingPoints.cs
deleted file mode 100644 (file)
index 6b16968..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Humanoid.Markings;
-
-[DataDefinition]
-[Serializable, NetSerializable]
-public sealed partial class MarkingPoints
-{
-    [DataField(required: true)]
-    public int Points = 0;
-
-    [DataField(required: true)]
-    public bool Required;
-
-    /// <summary>
-    ///     If the user of this marking point set is only allowed to
-    ///     use whitelisted markings, and not globally usable markings.
-    ///     Only used for validation and profile construction. Ignored anywhere else.
-    /// </summary>
-    [DataField]
-    public bool OnlyWhitelisted;
-
-    // Default markings for this layer.
-    [DataField]
-    public List<ProtoId<MarkingPrototype>> DefaultMarkings = new();
-
-    public static Dictionary<MarkingCategories, MarkingPoints> CloneMarkingPointDictionary(Dictionary<MarkingCategories, MarkingPoints> self)
-    {
-        var clone = new Dictionary<MarkingCategories, MarkingPoints>();
-
-        foreach (var (category, points) in self)
-        {
-            clone[category] = new MarkingPoints()
-            {
-                Points = points.Points,
-                Required = points.Required,
-                OnlyWhitelisted = points.OnlyWhitelisted,
-                DefaultMarkings = points.DefaultMarkings
-            };
-        }
-
-        return clone;
-    }
-}
-
-[Prototype]
-public sealed partial class MarkingPointsPrototype : IPrototype
-{
-    [IdDataField] public string ID { get; private set; } = default!;
-
-    /// <summary>
-    ///     If the user of this marking point set is only allowed to
-    ///     use whitelisted markings, and not globally usable markings.
-    ///     Only used for validation and profile construction. Ignored anywhere else.
-    /// </summary>
-    [DataField]
-    public bool OnlyWhitelisted;
-
-    [DataField(required: true)]
-    public Dictionary<MarkingCategories, MarkingPoints> Points { get; private set; } = default!;
-}
index a6c578015ab71befe331cac701929e576dba95bd..10da06d86084ff49ed7b641dc11e4436fb4630f1 100644 (file)
@@ -14,18 +14,12 @@ namespace Content.Shared.Humanoid.Markings
         [DataField("bodyPart", required: true)]
         public HumanoidVisualLayers BodyPart { get; private set; } = default!;
 
-        [DataField("markingCategory", required: true)]
-        public MarkingCategories MarkingCategory { get; private set; } = default!;
-
-        [DataField("speciesRestriction")]
-        public List<string>? SpeciesRestrictions { get; private set; }
+        [DataField]
+        public List<ProtoId<MarkingsGroupPrototype>>? GroupWhitelist;
 
         [DataField("sexRestriction")]
         public Sex? SexRestriction { get; private set; }
 
-        [DataField("followSkinColor")]
-        public bool FollowSkinColor { get; private set; } = false;
-
         [DataField("forcedColoring")]
         public bool ForcedColoring { get; private set; } = false;
 
diff --git a/Content.Shared/Humanoid/Markings/MarkingsComponent.cs b/Content.Shared/Humanoid/Markings/MarkingsComponent.cs
deleted file mode 100644 (file)
index b22f25c..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-namespace Content.Shared.Humanoid.Markings
-{
-    [RegisterComponent]
-    public sealed partial class MarkingsComponent : Component
-    {
-        public Dictionary<HumanoidVisualLayers, List<Marking>> ActiveMarkings = new();
-
-        // Layer points for the attached mob. This is verified client side (but should be verified server side, eventually as well),
-        // but upon render for the given entity with this component, it will start subtracting
-        // points from this set. Upon depletion, no more sprites in this layer will be
-        // rendered. If an entry is null, however, it is considered 'unlimited points' for
-        // that layer.
-        //
-        // Layer points are useful for restricting the amount of markings a specific layer can use
-        // for specific mobs (i.e., a lizard should only use one set of horns and maybe two frills),
-        // and all species with selectable tails should have exactly one tail)
-        //
-        // If something is required, then something must be selected in that category. Otherwise,
-        // the first instance of a marking in that category will be added to a character
-        // upon round start.
-        [DataField("layerPoints")]
-        public Dictionary<MarkingCategories, MarkingPoints> LayerPoints = new();
-    }
-
-
-}
diff --git a/Content.Shared/Humanoid/Markings/MarkingsGroupPrototype.cs b/Content.Shared/Humanoid/Markings/MarkingsGroupPrototype.cs
new file mode 100644 (file)
index 0000000..640d5e5
--- /dev/null
@@ -0,0 +1,91 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Humanoid.Markings;
+
+/// <summary>
+/// Marker prototype that defines well-known types of markings, e.g. "human", "NT prosthetic", "moth", etc.
+/// </summary>
+[Prototype]
+public sealed partial class MarkingsGroupPrototype : IPrototype, IInheritingPrototype
+{
+    /// <inheritdoc />
+    [IdDataField]
+    public string ID { get; private set; } = default!;
+
+    /// <inheritdoc />
+    [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<MarkingsGroupPrototype>))]
+    public string[]? Parents { get; private set; }
+
+    /// <inheritdoc />
+    [NeverPushInheritance]
+    [AbstractDataField]
+    public bool Abstract { get; private set; }
+
+    /// <summary>
+    /// If only markings that explicitly list the group of this organ are permitted
+    /// </summary>
+    [DataField]
+    public bool OnlyGroupWhitelisted = false;
+
+    [DataField]
+    [AlwaysPushInheritance]
+    public Dictionary<Enum, MarkingsLimits> Limits = new();
+
+    [DataField]
+    [AlwaysPushInheritance]
+    public Dictionary<Enum, MarkingsAppearance> Appearances = new();
+}
+
+[DataDefinition]
+[Serializable, NetSerializable]
+public sealed partial class MarkingsLimits
+{
+    /// <summary>
+    /// How many markings this layer can take
+    /// </summary>
+    [DataField(required: true)]
+    public int Limit = 0;
+
+    /// <summary>
+    /// Whether or not this layer is required to have a marking
+    /// </summary>
+    [DataField(required: true)]
+    public bool Required;
+
+    /// <summary>
+    /// If only markings that explicitly list the group of this organ are permitted
+    /// </summary>
+    [DataField]
+    public bool? OnlyGroupWhitelisted;
+
+    /// <summary>
+    /// Default markings for this layer.
+    /// </summary>
+    [DataField]
+    public List<ProtoId<MarkingPrototype>> Default = new();
+
+    /// <summary>
+    /// Nudity markings for this layer that will be ensured if it is being enforced.
+    /// </summary>
+    [DataField]
+    public List<ProtoId<MarkingPrototype>> NudityDefault = new();
+}
+
+[DataDefinition]
+[Serializable, NetSerializable]
+public sealed partial class MarkingsAppearance
+{
+    /// <summary>
+    /// The transparency that markings have.
+    /// </summary>
+    [DataField]
+    public float LayerAlpha = 1f;
+
+    /// <summary>
+    /// Whether markings should be forced to match the skin color.
+    /// </summary>
+    [DataField]
+    public bool MatchSkin;
+}
diff --git a/Content.Shared/Humanoid/Markings/MarkingsSet.cs b/Content.Shared/Humanoid/Markings/MarkingsSet.cs
deleted file mode 100644 (file)
index 0ffd8c4..0000000
+++ /dev/null
@@ -1,838 +0,0 @@
-using System.Collections;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using Content.Shared.Humanoid.Prototypes;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Robust.Shared.Utility;
-
-namespace Content.Shared.Humanoid.Markings;
-
-// the better version of MarkingsSet
-// This one should ensure that a set is valid. Dependency retrieval is
-// probably not a good idea, and any dependency references should last
-// only for the length of a call, and not the lifetime of the set itself.
-//
-// Compared to MarkingsSet, this should allow for server-side authority.
-// Instead of sending the set over, we can instead just send the dictionary
-// and build the set from there. We can also just send a list and rebuild
-// the set without validating points (we're assuming that the server
-
-/// <summary>
-///     Marking set. For humanoid markings.
-/// </summary>
-/// <remarks>
-///     This is serializable for the admin panel that sets markings on demand for a player.
-///     Most APIs that accept a set of markings usually use a List of type Marking instead.
-/// </remarks>
-[DataDefinition]
-[Serializable, NetSerializable]
-public sealed partial class MarkingSet
-{
-    /// <summary>
-    ///     Every single marking in this set.
-    /// </summary>
-    /// <remarks>
-    ///     The original version of MarkingSet preserved ordering across all
-    ///     markings - this one should instead preserve ordering across all
-    ///     categories, but not marking categories themselves. This is because
-    ///     the layers that markings appear in are guaranteed to be in the correct
-    ///     order. This is here to make lookups slightly faster, even if the n of
-    ///     a marking set is relatively small, and to encapsulate another important
-    ///     feature of markings, which is the limit of markings you can put on a
-    ///     humanoid.
-    /// </remarks>
-    [DataField("markings")]
-    public Dictionary<MarkingCategories, List<Marking>> Markings = new();
-
-    /// <summary>
-    ///     Marking points for each category.
-    /// </summary>
-    [DataField("points")]
-    public Dictionary<MarkingCategories, MarkingPoints> Points = new();
-
-    public MarkingSet()
-    {}
-
-    /// <summary>
-    ///     Construct a MarkingSet using a list of markings, and a points
-    ///     dictionary. This will set up the points dictionary, and
-    ///     process the list, truncating if necessary. Markings that
-    ///     do not exist as a prototype will be removed.
-    /// </summary>
-    /// <param name="markings">The lists of markings to use.</param>
-    /// <param name="pointsPrototype">The ID of the points dictionary prototype.</param>
-    public MarkingSet(List<Marking> markings, string pointsPrototype, MarkingManager? markingManager = null, IPrototypeManager? prototypeManager = null)
-    {
-        IoCManager.Resolve(ref markingManager, ref prototypeManager);
-
-        if (!prototypeManager.TryIndex(pointsPrototype, out MarkingPointsPrototype? points))
-        {
-            return;
-        }
-
-        Points = MarkingPoints.CloneMarkingPointDictionary(points.Points);
-
-        foreach (var marking in markings)
-        {
-            if (!markingManager.TryGetMarking(marking, out var prototype))
-            {
-                continue;
-            }
-
-            AddBack(prototype.MarkingCategory, marking);
-        }
-    }
-
-    /// <summary>
-    ///     Construct a MarkingSet using a dictionary of markings,
-    ///     without point validation. This will still validate every
-    ///     marking, to ensure that it can be placed into the set.
-    /// </summary>
-    /// <param name="markings">The list of markings to use.</param>
-    public MarkingSet(List<Marking> markings, MarkingManager? markingManager = null)
-    {
-        IoCManager.Resolve(ref markingManager);
-
-        foreach (var marking in markings)
-        {
-            if (!markingManager.TryGetMarking(marking, out var prototype))
-            {
-                continue;
-            }
-
-            AddBack(prototype.MarkingCategory, marking);
-        }
-    }
-
-    /// <summary>
-    ///     Construct a MarkingSet only with a points dictionary.
-    /// </summary>
-    /// <param name="pointsPrototype">The ID of the points dictionary prototype.</param>
-    public MarkingSet(string pointsPrototype, MarkingManager? markingManager = null, IPrototypeManager? prototypeManager = null)
-    {
-        IoCManager.Resolve(ref markingManager, ref prototypeManager);
-
-        if (!prototypeManager.TryIndex(pointsPrototype, out MarkingPointsPrototype? points))
-        {
-            return;
-        }
-
-        Points = MarkingPoints.CloneMarkingPointDictionary(points.Points);
-    }
-
-    /// <summary>
-    ///     Construct a MarkingSet by deep cloning another set.
-    /// </summary>
-    /// <param name="other">The other marking set.</param>
-    public MarkingSet(MarkingSet other)
-    {
-        foreach (var (key, list) in other.Markings)
-        {
-            foreach (var marking in list)
-            {
-                AddBack(key, new(marking));
-            }
-        }
-
-        Points = MarkingPoints.CloneMarkingPointDictionary(other.Points);
-    }
-
-    /// <summary>
-    ///     Filters and colors markings based on species and it's restrictions in the marking's prototype from this marking set.
-    /// </summary>
-    /// <param name="species">The species to filter.</param>
-    /// <param name="skinColor">The skin color for recoloring (i.e. slimes). Use null if you want only filter markings</param>
-    /// <param name="markingManager">Marking manager.</param>
-    /// <param name="prototypeManager">Prototype manager.</param>
-    public void EnsureSpecies(string species, Color? skinColor, MarkingManager? markingManager = null, IPrototypeManager? prototypeManager = null)
-    {
-        IoCManager.Resolve(ref markingManager);
-        IoCManager.Resolve(ref prototypeManager);
-
-        var toRemove = new List<(MarkingCategories category, string id)>();
-        var speciesProto = prototypeManager.Index<SpeciesPrototype>(species);
-        var onlyWhitelisted = prototypeManager.Index(speciesProto.MarkingPoints).OnlyWhitelisted;
-
-        foreach (var (category, list) in Markings)
-        {
-            foreach (var marking in list)
-            {
-                if (!markingManager.TryGetMarking(marking, out var prototype))
-                {
-                    toRemove.Add((category, marking.MarkingId));
-                    continue;
-                }
-
-                if (onlyWhitelisted && prototype.SpeciesRestrictions == null)
-                {
-                    toRemove.Add((category, marking.MarkingId));
-                }
-
-                if (prototype.SpeciesRestrictions != null
-                    && !prototype.SpeciesRestrictions.Contains(species))
-                {
-                    toRemove.Add((category, marking.MarkingId));
-                }
-            }
-        }
-
-        foreach (var remove in toRemove)
-        {
-            Remove(remove.category, remove.id);
-        }
-
-        // Re-color left markings them into skin color if needed (i.e. for slimes)
-        if (skinColor != null)
-        {
-            foreach (var (category, list) in Markings)
-            {
-                foreach (var marking in list)
-                {
-                    if (markingManager.TryGetMarking(marking, out var prototype) &&
-                        markingManager.MustMatchSkin(species, prototype.BodyPart, out var alpha, prototypeManager))
-                    {
-                        marking.SetColor(skinColor.Value.WithAlpha(alpha));
-                    }
-                }
-            }
-        }
-    }
-
-    /// <summary>
-    ///     Filters markings based on sex and it's restrictions in the marking's prototype from this marking set.
-    /// </summary>
-    /// <param name="sex">The species to filter.</param>
-    /// <param name="markingManager">Marking manager.</param>
-    public void EnsureSexes(Sex sex, MarkingManager? markingManager = null)
-    {
-        IoCManager.Resolve(ref markingManager);
-
-        var toRemove = new List<(MarkingCategories category, string id)>();
-
-        foreach (var (category, list) in Markings)
-        {
-            foreach (var marking in list)
-            {
-                if (!markingManager.TryGetMarking(marking, out var prototype))
-                {
-                    toRemove.Add((category, marking.MarkingId));
-                    continue;
-                }
-
-                if (prototype.SexRestriction != null && prototype.SexRestriction != sex)
-                {
-                    toRemove.Add((category, marking.MarkingId));
-                }
-            }
-        }
-
-        foreach (var remove in toRemove)
-        {
-            Remove(remove.category, remove.id);
-        }
-    }
-
-    /// <summary>
-    ///     Ensures that all markings in this set are valid.
-    /// </summary>
-    /// <param name="markingManager">Marking manager.</param>
-    public void EnsureValid(MarkingManager? markingManager = null)
-    {
-        IoCManager.Resolve(ref markingManager);
-
-        var toRemove = new List<int>();
-        foreach (var (category, list) in Markings)
-        {
-            for (var i = 0; i < list.Count; i++)
-            {
-                if (!markingManager.TryGetMarking(list[i], out var marking))
-                {
-                    toRemove.Add(i);
-                    continue;
-                }
-
-                if (marking.Sprites.Count != list[i].MarkingColors.Count)
-                {
-                    list[i] = new Marking(marking.ID, marking.Sprites.Count);
-                }
-            }
-
-            foreach (var i in toRemove)
-            {
-                Remove(category, i);
-            }
-        }
-    }
-
-    /// <summary>
-    ///     Ensures that the default markings as defined by the marking point set in this marking set are applied.
-    /// </summary>
-    /// <param name="skinColor">Skin color for marking coloring.</param>
-    /// <param name="eyeColor">Eye color for marking coloring.</param>
-    /// <param name="hairColor">Hair color for marking coloring.</param>
-    /// <param name="markingManager">Marking manager.</param>
-    public void EnsureDefault(Color? skinColor = null, Color? eyeColor = null, MarkingManager? markingManager = null)
-    {
-        IoCManager.Resolve(ref markingManager);
-
-        foreach (var (category, points) in Points)
-        {
-            if (points.Points <= 0 || points.DefaultMarkings.Count <= 0)
-            {
-                continue;
-            }
-
-            var index = 0;
-            while (points.Points > 0 || index < points.DefaultMarkings.Count)
-            {
-                if (markingManager.Markings.TryGetValue(points.DefaultMarkings[index], out var prototype))
-                {
-                    var colors = MarkingColoring.GetMarkingLayerColors(
-                            prototype,
-                            skinColor,
-                            eyeColor,
-                            this
-                        );
-                    var marking = new Marking(points.DefaultMarkings[index], colors);
-
-                    AddBack(category, marking);
-                }
-
-                index++;
-            }
-        }
-    }
-
-    /// <summary>
-    ///     How many points are left in this marking set's category
-    /// </summary>
-    /// <param name="category">The category to check</param>
-    /// <returns>A number equal or greater than zero if the category exists, -1 otherwise.</returns>
-    public int PointsLeft(MarkingCategories category)
-    {
-        if (!Points.TryGetValue(category, out var points))
-        {
-            return -1;
-        }
-
-        return points.Points;
-    }
-
-    /// <summary>
-    ///     Add a marking to the front of the category's list of markings.
-    /// </summary>
-    /// <param name="category">Category to add the marking to.</param>
-    /// <param name="marking">The marking instance in question.</param>
-    public void AddFront(MarkingCategories category, Marking marking)
-    {
-        if (!marking.Forced && Points.TryGetValue(category, out var points))
-        {
-            if (points.Points <= 0)
-            {
-                return;
-            }
-
-            points.Points--;
-        }
-
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            markings = new();
-            Markings[category] = markings;
-        }
-
-        markings.Insert(0, marking);
-    }
-
-    /// <summary>
-    ///     Add a marking to the back of the category's list of markings.
-    /// </summary>
-    /// <param name="category"></param>
-    /// <param name="marking"></param>
-    public void AddBack(MarkingCategories category, Marking marking)
-    {
-        if (!marking.Forced && Points.TryGetValue(category, out var points))
-        {
-            if (points.Points <= 0)
-            {
-                return;
-            }
-
-            points.Points--;
-        }
-
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            markings = new();
-            Markings[category] = markings;
-        }
-
-
-        markings.Add(marking);
-    }
-
-    /// <summary>
-    ///     Adds a category to this marking set.
-    /// </summary>
-    /// <param name="category"></param>
-    /// <returns></returns>
-    public List<Marking> AddCategory(MarkingCategories category)
-    {
-        var markings = new List<Marking>();
-        Markings.Add(category, markings);
-        return markings;
-    }
-
-    /// <summary>
-    ///     Replace a marking at a given index in a marking category with another marking.
-    /// </summary>
-    /// <param name="category">The category to replace the marking in.</param>
-    /// <param name="index">The index of the marking.</param>
-    /// <param name="marking">The marking to insert.</param>
-    public void Replace(MarkingCategories category, int index, Marking marking)
-    {
-        if (index < 0 || !Markings.TryGetValue(category, out var markings)
-            || index >= markings.Count)
-        {
-            return;
-        }
-
-        markings[index] = marking;
-    }
-
-    /// <summary>
-    ///     Remove a marking by category and ID.
-    /// </summary>
-    /// <param name="category">The category that contains the marking.</param>
-    /// <param name="id">The marking's ID.</param>
-    /// <returns>True if removed, false otherwise.</returns>
-    public bool Remove(MarkingCategories category, string id)
-    {
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            return false;
-        }
-
-        for (var i = 0; i < markings.Count; i++)
-        {
-            if (markings[i].MarkingId != id)
-            {
-                continue;
-            }
-
-            if (!markings[i].Forced && Points.TryGetValue(category, out var points))
-            {
-                points.Points++;
-            }
-
-            markings.RemoveAt(i);
-            return true;
-        }
-
-        return false;
-    }
-
-    /// <summary>
-    ///     Remove a marking by category and index.
-    /// </summary>
-    /// <param name="category">The category that contains the marking.</param>
-    /// <param name="idx">The marking's index.</param>
-    /// <returns>True if removed, false otherwise.</returns>
-    public void Remove(MarkingCategories category, int idx)
-    {
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            return;
-        }
-
-        if (idx < 0 || idx >= markings.Count)
-        {
-            return;
-        }
-
-        if (!markings[idx].Forced && Points.TryGetValue(category, out var points))
-        {
-            points.Points++;
-        }
-
-        markings.RemoveAt(idx);
-    }
-
-    /// <summary>
-    ///     Remove an entire category from this marking set.
-    /// </summary>
-    /// <param name="category">The category to remove.</param>
-    /// <returns>True if removed, false otherwise.</returns>
-    public bool RemoveCategory(MarkingCategories category)
-    {
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            return false;
-        }
-
-        if (Points.TryGetValue(category, out var points))
-        {
-            foreach (var marking in markings)
-            {
-                if (marking.Forced)
-                {
-                    continue;
-                }
-
-                points.Points++;
-            }
-        }
-
-        Markings.Remove(category);
-        return true;
-    }
-
-    /// <summary>
-    ///     Clears all markings from this marking set.
-    /// </summary>
-    public void Clear()
-    {
-        foreach (var category in Enum.GetValues<MarkingCategories>())
-        {
-            RemoveCategory(category);
-        }
-    }
-
-    /// <summary>
-    ///     Attempt to find the index of a marking in a category by ID.
-    /// </summary>
-    /// <param name="category">The category to search in.</param>
-    /// <param name="id">The ID to search for.</param>
-    /// <returns>The index of the marking, otherwise a negative number.</returns>
-    public int FindIndexOf(MarkingCategories category, string id)
-    {
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            return -1;
-        }
-
-        return markings.FindIndex(m => m.MarkingId == id);
-    }
-
-    /// <summary>
-    ///     Tries to get an entire category from this marking set.
-    /// </summary>
-    /// <param name="category">The category to fetch.</param>
-    /// <param name="markings">A read only list of the all markings in that category.</param>
-    /// <returns>True if successful, false otherwise.</returns>
-    public bool TryGetCategory(MarkingCategories category, [NotNullWhen(true)] out IReadOnlyList<Marking>? markings)
-    {
-        markings = null;
-
-        if (Markings.TryGetValue(category, out var list))
-        {
-            markings = list;
-            return true;
-        }
-
-        return false;
-    }
-
-    /// <summary>
-    ///     Tries to get a marking from this marking set, by category.
-    /// </summary>
-    /// <param name="category">The category to search in.</param>
-    /// <param name="id">The ID to search for.</param>
-    /// <param name="marking">The marking, if it was retrieved.</param>
-    /// <returns>True if successful, false otherwise.</returns>
-    public bool TryGetMarking(MarkingCategories category, string id, [NotNullWhen(true)] out Marking? marking)
-    {
-        marking = null;
-
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            return false;
-        }
-
-        foreach (var m in markings)
-        {
-            if (m.MarkingId == id)
-            {
-                marking = m;
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /// <summary>
-    ///     Shifts a marking's rank towards the front of the list
-    /// </summary>
-    /// <param name="category">The category to shift in.</param>
-    /// <param name="idx">Index of the marking.</param>
-    public void ShiftRankUp(MarkingCategories category, int idx)
-    {
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            return;
-        }
-
-        if (idx < 0 || idx >= markings.Count || idx - 1 < 0)
-        {
-            return;
-        }
-
-        (markings[idx - 1], markings[idx]) = (markings[idx], markings[idx - 1]);
-    }
-
-    /// <summary>
-    ///     Shifts a marking's rank upwards from the end of the list
-    /// </summary>
-    /// <param name="category">The category to shift in.</param>
-    /// <param name="idx">Index of the marking from the end</param>
-    public void ShiftRankUpFromEnd(MarkingCategories category, int idx)
-    {
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            return;
-        }
-
-        ShiftRankUp(category, markings.Count - idx - 1);
-    }
-
-    /// <summary>
-    ///     Shifts a marking's rank towards the end of the list
-    /// </summary>
-    /// <param name="category">The category to shift in.</param>
-    /// <param name="idx">Index of the marking.</param>
-    public void ShiftRankDown(MarkingCategories category, int idx)
-    {
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            return;
-        }
-
-        if (idx < 0 || idx >= markings.Count || idx + 1 >= markings.Count)
-        {
-            return;
-        }
-
-        (markings[idx + 1], markings[idx]) = (markings[idx], markings[idx + 1]);
-    }
-
-    /// <summary>
-    ///     Shifts a marking's rank downwards from the end of the list
-    /// </summary>
-    /// <param name="category">The category to shift in.</param>
-    /// <param name="idx">Index of the marking from the end</param>
-    public void ShiftRankDownFromEnd(MarkingCategories category, int idx)
-    {
-        if (!Markings.TryGetValue(category, out var markings))
-        {
-            return;
-        }
-
-        ShiftRankDown(category, markings.Count - idx - 1);
-    }
-
-    /// <summary>
-    ///     Gets all markings in this set as an enumerator. Lists will be organized, but categories may be in any order.
-    /// </summary>
-    /// <returns>An enumerator of <see cref="Marking"/>s.</returns>
-    public ForwardMarkingEnumerator GetForwardEnumerator()
-    {
-        var markings = new List<Marking>();
-        foreach (var (_, list) in Markings)
-        {
-            markings.AddRange(list);
-        }
-
-        return new ForwardMarkingEnumerator(markings);
-    }
-
-    /// <summary>
-    ///     Gets an enumerator of markings in this set, but only for one category.
-    /// </summary>
-    /// <param name="category">The category to fetch.</param>
-    /// <returns>An enumerator of <see cref="Marking"/>s in that category.</returns>
-    public ForwardMarkingEnumerator GetForwardEnumerator(MarkingCategories category)
-    {
-        var markings = new List<Marking>();
-        if (Markings.TryGetValue(category, out var listing))
-        {
-            markings = new(listing);
-        }
-
-        return new ForwardMarkingEnumerator(markings);
-    }
-
-    /// <summary>
-    ///     Gets all markings in this set as an enumerator, but in reverse order. Lists will be in reverse order, but categories may be in any order.
-    /// </summary>
-    /// <returns>An enumerator of <see cref="Marking"/>s in reverse.</returns>
-    public ReverseMarkingEnumerator GetReverseEnumerator()
-    {
-        var markings = new List<Marking>();
-        foreach (var (_, list) in Markings)
-        {
-            markings.AddRange(list);
-        }
-
-        return new ReverseMarkingEnumerator(markings);
-    }
-
-    /// <summary>
-    ///     Gets an enumerator of markings in this set in reverse order, but only for one category.
-    /// </summary>
-    /// <param name="category">The category to fetch.</param>
-    /// <returns>An enumerator of <see cref="Marking"/>s in that category, in reverse order.</returns>
-    public ReverseMarkingEnumerator GetReverseEnumerator(MarkingCategories category)
-    {
-        var markings = new List<Marking>();
-        if (Markings.TryGetValue(category, out var listing))
-        {
-            markings = new(listing);
-        }
-
-        return new ReverseMarkingEnumerator(markings);
-    }
-
-    public bool CategoryEquals(MarkingCategories category, MarkingSet other)
-    {
-        if (!Markings.TryGetValue(category, out var markings)
-            || !other.Markings.TryGetValue(category, out var markingsOther))
-        {
-            return false;
-        }
-
-        return markings.SequenceEqual(markingsOther);
-    }
-
-    public bool Equals(MarkingSet other)
-    {
-        foreach (var (category, _) in Markings)
-        {
-            if (!CategoryEquals(category, other))
-            {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    /// <summary>
-    ///     Gets a difference of marking categories between two marking sets
-    /// </summary>
-    /// <param name="other">The other marking set.</param>
-    /// <returns>Enumerator of marking categories that were different between the two.</returns>
-    public IEnumerable<MarkingCategories> CategoryDifference(MarkingSet other)
-    {
-        foreach (var (category, _) in Markings)
-        {
-            if (!CategoryEquals(category, other))
-            {
-                yield return category;
-            }
-        }
-    }
-}
-
-public sealed class ForwardMarkingEnumerator : IEnumerable<Marking>
-{
-    private List<Marking> _markings;
-
-    public ForwardMarkingEnumerator(List<Marking> markings)
-    {
-        _markings = markings;
-    }
-
-    public IEnumerator<Marking> GetEnumerator()
-    {
-        return new MarkingsEnumerator(_markings, false);
-    }
-
-    IEnumerator IEnumerable.GetEnumerator()
-    {
-        return GetEnumerator();
-    }
-}
-
-public sealed class ReverseMarkingEnumerator : IEnumerable<Marking>
-{
-    private List<Marking> _markings;
-
-    public ReverseMarkingEnumerator(List<Marking> markings)
-    {
-        _markings = markings;
-    }
-
-    public IEnumerator<Marking> GetEnumerator()
-    {
-        return new MarkingsEnumerator(_markings, true);
-    }
-
-    IEnumerator IEnumerable.GetEnumerator()
-    {
-        return GetEnumerator();
-    }
-}
-
-public sealed class MarkingsEnumerator : IEnumerator<Marking>
-{
-    private List<Marking> _markings;
-    private bool _reverse;
-
-    int position;
-
-    public MarkingsEnumerator(List<Marking> markings, bool reverse)
-    {
-        _markings = markings;
-        _reverse = reverse;
-
-        if (_reverse)
-        {
-            position = _markings.Count;
-        }
-        else
-        {
-            position = -1;
-        }
-    }
-
-    public bool MoveNext()
-    {
-        if (_reverse)
-        {
-            position--;
-            return (position >= 0);
-        }
-        else
-        {
-            position++;
-            return (position < _markings.Count);
-        }
-    }
-
-    public void Reset()
-    {
-        if (_reverse)
-        {
-            position = _markings.Count;
-        }
-        else
-        {
-            position = -1;
-        }
-    }
-
-    public void Dispose()
-    {}
-
-    object IEnumerator.Current
-    {
-        get => _markings[position];
-    }
-
-    public Marking Current
-    {
-        get => _markings[position];
-    }
-}
index 6edb2aed0cab46d6199cea883dfca6153b95d519..c8945301414380d76f755d68944ff46bcca30101 100644 (file)
@@ -9,9 +9,6 @@ public sealed partial class HumanoidProfilePrototype : IPrototype
     [IdDataField]
     public string ID { get; private set; } = default!;
 
-    [DataField("customBaseLayers")]
-    public Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> CustomBaseLayers = new();
-
     [DataField("profile")]
     public HumanoidCharacterProfile Profile { get; private set; } = new();
 }
diff --git a/Content.Shared/Humanoid/Prototypes/HumanoidSpritePrototypes.cs b/Content.Shared/Humanoid/Prototypes/HumanoidSpritePrototypes.cs
deleted file mode 100644 (file)
index 097dc9d..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Shared.Humanoid.Prototypes;
-
-/// <summary>
-///     Base sprites for a species (e.g., what replaces the empty tagged layer,
-///     or settings per layer)
-/// </summary>
-[Prototype("speciesBaseSprites")]
-public sealed partial class HumanoidSpeciesBaseSpritesPrototype : IPrototype
-{
-     [IdDataField]
-     public string ID { get; private set; } = default!;
-
-     /// <summary>
-     ///     Sprites that this species will use on the given humanoid
-     ///     visual layer. If a key entry is empty, it is assumed that the
-     ///     visual layer will not be in use on this species, and will
-     ///     be ignored.
-     /// </summary>
-     [DataField("sprites", required: true)]
-     public Dictionary<HumanoidVisualLayers, string> Sprites = new();
-}
-
-/// <summary>
-///     Humanoid species sprite layer. This is what defines the base layer of
-///     a humanoid species sprite, and also defines how markings can appear over
-///     that sprite (or at least, the layer this sprite is on).
-/// </summary>
-[Prototype("humanoidBaseSprite")]
-public sealed partial class HumanoidSpeciesSpriteLayer : IPrototype
-{
-    [IdDataField]
-    public string ID { get; private set; } = default!;
-    /// <summary>
-    ///     The base sprite for this sprite layer. This is what
-    ///     will replace the empty layer tagged by the enum
-    ///     tied to this layer.
-    ///
-    ///     If this is null, no sprite will be displayed, and the
-    ///     layer will be invisible until otherwise set.
-    /// </summary>
-    [DataField("baseSprite")]
-    public SpriteSpecifier? BaseSprite { get; private set; }
-
-    /// <summary>
-    ///     The alpha of this layer. Ensures that
-    ///     this layer will start with this percentage
-    ///     of alpha.
-    /// </summary>
-    [DataField("layerAlpha")]
-    public float LayerAlpha { get; private set; } = 1.0f;
-
-    /// <summary>
-    ///     If this sprite layer should allow markings or not.
-    /// </summary>
-    [DataField("allowsMarkings")]
-    public bool AllowsMarkings { get; private set; } = true;
-
-    /// <summary>
-    ///     If this layer should always match the
-    ///     skin tone in a character profile.
-    /// </summary>
-    [DataField("matchSkin")]
-    public bool MatchSkin { get; private set; } = true;
-
-    /// <summary>
-    ///     If any markings that go on this layer should
-    ///     match the skin tone of this part, including
-    ///     alpha.
-    /// </summary>
-    [DataField("markingsMatchSkin")]
-    public bool MarkingsMatchSkin { get; private set; }
-}
index a23ecdfc535da830305e179952f6d0ca9c54d781..03cabf19d0970b031e629d13b048329fbe0d5e06 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.Body;
 using Content.Shared.Dataset;
 using Content.Shared.Humanoid.Markings;
 using Robust.Shared.Prototypes;
@@ -34,18 +35,6 @@ public sealed partial class SpeciesPrototype : IPrototype
     [DataField(required: true)]
     public bool RoundStart { get; private set; } = false;
 
-    // The below two are to avoid fetching information about the species from the entity
-    // prototype.
-
-    // This one here is a utility field, and is meant to *avoid* having to duplicate
-    // the massive SpriteComponent found in every species.
-    // Species implementors can just override SpriteComponent if they want a custom
-    // sprite layout, and leave this null. Keep in mind that this will disable
-    // sprite accessories.
-
-    [DataField("sprites")]
-    public ProtoId<HumanoidSpeciesBaseSpritesPrototype> SpriteSet { get; private set; } = default!;
-
     /// <summary>
     ///     Default skin tone for this species. This applies for non-human skin tones.
     /// </summary>
@@ -59,12 +48,6 @@ public sealed partial class SpeciesPrototype : IPrototype
     [DataField]
     public int DefaultHumanSkinTone { get; private set; } = 20;
 
-    /// <summary>
-    ///     The limit of body markings that you can place on this species.
-    /// </summary>
-    [DataField("markingLimits")]
-    public ProtoId<MarkingPointsPrototype> MarkingPoints { get; private set; } = default!;
-
     /// <summary>
     ///     Humanoid species variant used by this entity.
     /// </summary>
diff --git a/Content.Shared/Humanoid/SharedHideableHumanoidLayersSystem.cs b/Content.Shared/Humanoid/SharedHideableHumanoidLayersSystem.cs
new file mode 100644 (file)
index 0000000..6e308cc
--- /dev/null
@@ -0,0 +1,58 @@
+using System.Numerics;
+using Content.Shared.Inventory;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Humanoid;
+
+public abstract partial class SharedHideableHumanoidLayersSystem : EntitySystem
+{
+    /// <summary>
+    ///     Toggles a humanoid's sprite layer visibility.
+    /// </summary>
+    /// <param name="ent">Humanoid entity</param>
+    /// <param name="layer">Layer to toggle visibility for</param>
+    /// <param name="visible">Whether to hide or show the layer. If more than once piece of clothing is hiding the layer, it may remain hidden.</param>
+    /// <param name="slot">Equipment slot that has the clothing that is (or was) hiding the layer.</param>
+    public virtual void SetLayerVisibility(
+        Entity<HideableHumanoidLayersComponent?> ent,
+        HumanoidVisualLayers layer,
+        bool visible,
+        SlotFlags slot)
+    {
+        if (!Resolve(ent, ref ent.Comp))
+            return;
+
+#if DEBUG
+        DebugTools.AssertNotEqual(slot, SlotFlags.NONE);
+        // Check that only a single bit in the bitflag is set
+        var powerOfTwo = BitOperations.RoundUpToPowerOf2((uint)slot);
+        DebugTools.AssertEqual((uint)slot, powerOfTwo);
+#endif
+
+        var dirty = false;
+        if (visible)
+        {
+            var oldSlots = ent.Comp.HiddenLayers.GetValueOrDefault(layer);
+            ent.Comp.HiddenLayers[layer] = slot | oldSlots;
+            dirty |= (oldSlots & slot) != slot;
+        }
+        else if (ent.Comp.HiddenLayers.TryGetValue(layer, out var oldSlots))
+        {
+            // This layer might be getting hidden by more than one piece of equipped clothing.
+            // remove slot flag from the set of slots hiding this layer, then check if there are any left.
+            ent.Comp.HiddenLayers[layer] = ~slot & oldSlots;
+            if (ent.Comp.HiddenLayers[layer] == SlotFlags.NONE)
+                ent.Comp.HiddenLayers.Remove(layer);
+
+            dirty |= (oldSlots & slot) != 0;
+        }
+
+        if (!dirty)
+            return;
+
+        Dirty(ent);
+
+        var evt = new HumanoidLayerVisibilityChangedEvent(layer, visible);
+        RaiseLocalEvent(ent, ref evt);
+    }
+}
diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
deleted file mode 100644 (file)
index 401ba04..0000000
+++ /dev/null
@@ -1,568 +0,0 @@
-using System.IO;
-using System.Linq;
-using System.Numerics;
-using Content.Shared.CCVar;
-using Content.Shared.Decals;
-using Content.Shared.Examine;
-using Content.Shared.Humanoid.Markings;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.IdentityManagement;
-using Content.Shared.Inventory;
-using Content.Shared.Preferences;
-using Robust.Shared;
-using Robust.Shared.Configuration;
-using Robust.Shared.Enums;
-using Robust.Shared.GameObjects.Components.Localization;
-using Robust.Shared.Network;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.Manager;
-using Robust.Shared.Serialization.Markdown;
-using Robust.Shared.Utility;
-using YamlDotNet.RepresentationModel;
-
-namespace Content.Shared.Humanoid;
-
-/// <summary>
-///     HumanoidSystem. Primarily deals with the appearance and visual data
-///     of a humanoid entity. HumanoidVisualizer is what deals with actually
-///     organizing the sprites and setting up the sprite component's layers.
-///
-///     This is a shared system, because while it is server authoritative,
-///     you still need a local copy so that players can set up their
-///     characters.
-/// </summary>
-public abstract class SharedHumanoidAppearanceSystem : EntitySystem
-{
-    [Dependency] private readonly IConfigurationManager _cfgManager = default!;
-    [Dependency] private readonly INetManager _netManager = default!;
-    [Dependency] private readonly IPrototypeManager _proto = default!;
-    [Dependency] private readonly ISerializationManager _serManager = default!;
-    [Dependency] private readonly MarkingManager _markingManager = default!;
-    [Dependency] private readonly GrammarSystem _grammarSystem = default!;
-    [Dependency] private readonly IdentitySystem _identity = default!;
-
-    public static readonly ProtoId<SpeciesPrototype> DefaultSpecies = "Human";
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<HumanoidAppearanceComponent, ComponentInit>(OnInit);
-        SubscribeLocalEvent<HumanoidAppearanceComponent, ExaminedEvent>(OnExamined);
-    }
-
-    public DataNode ToDataNode(HumanoidCharacterProfile profile)
-    {
-        var export = new HumanoidProfileExport()
-        {
-            ForkId = _cfgManager.GetCVar(CVars.BuildForkId),
-            Profile = profile,
-        };
-
-        var dataNode = _serManager.WriteValue(export, alwaysWrite: true, notNullableOverride: true);
-        return dataNode;
-    }
-
-    public HumanoidCharacterProfile FromStream(Stream stream, ICommonSession session)
-    {
-        using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
-        var yamlStream = new YamlStream();
-        yamlStream.Load(reader);
-
-        var root = yamlStream.Documents[0].RootNode;
-        var export = _serManager.Read<HumanoidProfileExport>(root.ToDataNode(), notNullableOverride: true);
-
-        /*
-         * Add custom handling here for forks / version numbers if you care.
-         */
-
-        var profile = export.Profile;
-        var collection = IoCManager.Instance;
-        profile.EnsureValid(session, collection!);
-        return profile;
-    }
-
-    private void OnInit(EntityUid uid, HumanoidAppearanceComponent humanoid, ComponentInit args)
-    {
-        if (string.IsNullOrEmpty(humanoid.Species) || _netManager.IsClient && !IsClientSide(uid))
-        {
-            return;
-        }
-
-        if (string.IsNullOrEmpty(humanoid.Initial)
-            || !_proto.Resolve(humanoid.Initial, out HumanoidProfilePrototype? startingSet))
-        {
-            LoadProfile(uid, HumanoidCharacterProfile.DefaultWithSpecies(humanoid.Species), humanoid);
-            return;
-        }
-
-        // Do this first, because profiles currently do not support custom base layers
-        foreach (var (layer, info) in startingSet.CustomBaseLayers)
-        {
-            humanoid.CustomBaseLayers.Add(layer, info);
-        }
-
-        LoadProfile(uid, startingSet.Profile, humanoid);
-    }
-
-    private void OnExamined(EntityUid uid, HumanoidAppearanceComponent component, ExaminedEvent args)
-    {
-        var identity = Identity.Entity(uid, EntityManager);
-        var species = GetSpeciesRepresentation(component.Species).ToLower();
-        var age = GetAgeRepresentation(component.Species, component.Age);
-
-        args.PushText(Loc.GetString("humanoid-appearance-component-examine", ("user", identity), ("age", age), ("species", species)));
-    }
-
-    /// <summary>
-    ///     Toggles a humanoid's sprite layer visibility.
-    /// </summary>
-    /// <param name="ent">Humanoid entity</param>
-    /// <param name="layer">Layer to toggle visibility for</param>
-    /// <param name="visible">Whether to hide or show the layer. If more than once piece of clothing is hiding the layer, it may remain hidden.</param>
-    /// <param name="source">Equipment slot that has the clothing that is (or was) hiding the layer. If not specified, the change is "permanent" (i.e., see <see cref="HumanoidAppearanceComponent.PermanentlyHidden"/>)</param>
-    public void SetLayerVisibility(Entity<HumanoidAppearanceComponent?> ent,
-        HumanoidVisualLayers layer,
-        bool visible,
-        SlotFlags? source = null)
-    {
-        if (!Resolve(ent.Owner, ref ent.Comp, false))
-            return;
-
-        var dirty = false;
-        SetLayerVisibility(ent!, layer, visible, source, ref dirty);
-        if (dirty)
-            Dirty(ent);
-    }
-
-    /// <summary>
-    ///     Clones a humanoid's appearance to a target mob, provided they both have humanoid components.
-    /// </summary>
-    /// <param name="source">Source entity to fetch the original appearance from.</param>
-    /// <param name="target">Target entity to apply the source entity's appearance to.</param>
-    /// <param name="sourceHumanoid">Source entity's humanoid component.</param>
-    /// <param name="targetHumanoid">Target entity's humanoid component.</param>
-    public void CloneAppearance(EntityUid source, EntityUid target, HumanoidAppearanceComponent? sourceHumanoid = null,
-        HumanoidAppearanceComponent? targetHumanoid = null)
-    {
-        if (!Resolve(source, ref sourceHumanoid, false) || !Resolve(target, ref targetHumanoid, false))
-            return;
-
-        targetHumanoid.Species = sourceHumanoid.Species;
-        targetHumanoid.SkinColor = sourceHumanoid.SkinColor;
-        targetHumanoid.EyeColor = sourceHumanoid.EyeColor;
-        targetHumanoid.Age = sourceHumanoid.Age;
-        targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers);
-        targetHumanoid.MarkingSet = new(sourceHumanoid.MarkingSet);
-
-        SetSex(target, sourceHumanoid.Sex, false, targetHumanoid);
-        SetGender((target, targetHumanoid), sourceHumanoid.Gender);
-
-        Dirty(target, targetHumanoid);
-    }
-
-    /// <summary>
-    ///     Sets the visibility for multiple layers at once on a humanoid's sprite.
-    /// </summary>
-    /// <param name="ent">Humanoid entity</param>
-    /// <param name="layers">An enumerable of all sprite layers that are going to have their visibility set</param>
-    /// <param name="visible">The visibility state of the layers given</param>
-    public void SetLayersVisibility(Entity<HumanoidAppearanceComponent?> ent,
-        IEnumerable<HumanoidVisualLayers> layers,
-        bool visible)
-    {
-        if (!Resolve(ent.Owner, ref ent.Comp, false))
-            return;
-
-        var dirty = false;
-
-        foreach (var layer in layers)
-        {
-            SetLayerVisibility(ent!, layer, visible, null, ref dirty);
-        }
-
-        if (dirty)
-            Dirty(ent);
-    }
-
-    /// <inheritdoc cref="SetLayerVisibility(Entity{HumanoidAppearanceComponent?},HumanoidVisualLayers,bool,Nullable{SlotFlags})"/>
-    public virtual void SetLayerVisibility(
-        Entity<HumanoidAppearanceComponent> ent,
-        HumanoidVisualLayers layer,
-        bool visible,
-        SlotFlags? source,
-        ref bool dirty)
-    {
-#if DEBUG
-        if (source is {} s)
-        {
-            DebugTools.AssertNotEqual(s, SlotFlags.NONE);
-            // Check that only a single bit in the bitflag is set
-            var powerOfTwo = BitOperations.RoundUpToPowerOf2((uint)s);
-            DebugTools.AssertEqual((uint)s, powerOfTwo);
-        }
-#endif
-
-        if (visible)
-        {
-            if (source is not {} slot)
-            {
-                dirty |= ent.Comp.PermanentlyHidden.Remove(layer);
-            }
-            else if (ent.Comp.HiddenLayers.TryGetValue(layer, out var oldSlots))
-            {
-                // This layer might be getting hidden by more than one piece of equipped clothing.
-                // remove slot flag from the set of slots hiding this layer, then check if there are any left.
-                ent.Comp.HiddenLayers[layer] = ~slot & oldSlots;
-                if (ent.Comp.HiddenLayers[layer] == SlotFlags.NONE)
-                    ent.Comp.HiddenLayers.Remove(layer);
-
-                dirty |= (oldSlots & slot) != 0;
-            }
-        }
-        else
-        {
-            if (source is not { } slot)
-            {
-                dirty |= ent.Comp.PermanentlyHidden.Add(layer);
-            }
-            else
-            {
-                var oldSlots = ent.Comp.HiddenLayers.GetValueOrDefault(layer);
-                ent.Comp.HiddenLayers[layer] = slot | oldSlots;
-                dirty |= (oldSlots & slot) != slot;
-            }
-
-        }
-    }
-
-    /// <summary>
-    ///     Set a humanoid mob's species. This will change their base sprites, as well as their current
-    ///     set of markings to fit against the mob's new species.
-    /// </summary>
-    /// <param name="uid">The humanoid mob's UID.</param>
-    /// <param name="species">The species to set the mob to. Will return if the species prototype was invalid.</param>
-    /// <param name="sync">Whether to immediately synchronize this to the humanoid mob, or not.</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void SetSpecies(EntityUid uid, string species, bool sync = true, HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (!Resolve(uid, ref humanoid) || !_proto.TryIndex<SpeciesPrototype>(species, out var prototype))
-        {
-            return;
-        }
-
-        humanoid.Species = species;
-        humanoid.MarkingSet.EnsureSpecies(species, humanoid.SkinColor, _markingManager);
-        var oldMarkings = humanoid.MarkingSet.GetForwardEnumerator().ToList();
-        humanoid.MarkingSet = new(oldMarkings, prototype.MarkingPoints, _markingManager, _proto);
-
-        if (sync)
-            Dirty(uid, humanoid);
-    }
-
-    /// <summary>
-    /// Sets the gender in the entity's HumanoidAppearanceComponent and GrammarComponent.
-    /// </summary>
-    public void SetGender(Entity<HumanoidAppearanceComponent?> ent, Gender gender)
-    {
-        if (!Resolve(ent, ref ent.Comp))
-            return;
-
-        ent.Comp.Gender = gender;
-        Dirty(ent);
-
-        if (TryComp<GrammarComponent>(ent, out var grammar))
-            _grammarSystem.SetGender((ent, grammar), gender);
-
-        _identity.QueueIdentityUpdate(ent);
-    }
-
-    /// <summary>
-    ///     Sets the skin color of this humanoid mob. Will only affect base layers that are not custom,
-    ///     custom base layers should use <see cref="SetBaseLayerColor"/> instead.
-    /// </summary>
-    /// <param name="uid">The humanoid mob's UID.</param>
-    /// <param name="skinColor">Skin color to set on the humanoid mob.</param>
-    /// <param name="sync">Whether to synchronize this to the humanoid mob, or not.</param>
-    /// <param name="verify">Whether to verify the skin color can be set on this humanoid or not</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public virtual void SetSkinColor(EntityUid uid, Color skinColor, bool sync = true, bool verify = true, HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (!Resolve(uid, ref humanoid))
-            return;
-
-        if (!_proto.Resolve<SpeciesPrototype>(humanoid.Species, out var species))
-        {
-            return;
-        }
-
-        if (verify && _proto.Resolve(species.SkinColoration, out var index))
-        {
-            var strategy = index.Strategy;
-            skinColor = strategy.EnsureVerified(skinColor);
-        }
-
-        humanoid.SkinColor = skinColor;
-
-        if (sync)
-            Dirty(uid, humanoid);
-    }
-
-    /// <summary>
-    ///     Sets the base layer ID of this humanoid mob. A humanoid mob's 'base layer' is
-    ///     the skin sprite that is applied to the mob's sprite upon appearance refresh.
-    /// </summary>
-    /// <param name="uid">The humanoid mob's UID.</param>
-    /// <param name="layer">The layer to target on this humanoid mob.</param>
-    /// <param name="id">The ID of the sprite to use. See <see cref="HumanoidSpeciesSpriteLayer"/>.</param>
-    /// <param name="sync">Whether to synchronize this to the humanoid mob, or not.</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void SetBaseLayerId(EntityUid uid, HumanoidVisualLayers layer, string? id, bool sync = true,
-        HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (!Resolve(uid, ref humanoid))
-            return;
-
-        if (humanoid.CustomBaseLayers.TryGetValue(layer, out var info))
-            humanoid.CustomBaseLayers[layer] = info with { Id = id };
-        else
-            humanoid.CustomBaseLayers[layer] = new(id);
-
-        if (sync)
-            Dirty(uid, humanoid);
-    }
-
-    /// <summary>
-    ///     Sets the color of this humanoid mob's base layer. See <see cref="SetBaseLayerId"/> for a
-    ///     description of how base layers work.
-    /// </summary>
-    /// <param name="uid">The humanoid mob's UID.</param>
-    /// <param name="layer">The layer to target on this humanoid mob.</param>
-    /// <param name="color">The color to set this base layer to.</param>
-    public void SetBaseLayerColor(EntityUid uid, HumanoidVisualLayers layer, Color? color, bool sync = true, HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (!Resolve(uid, ref humanoid))
-            return;
-
-        if (humanoid.CustomBaseLayers.TryGetValue(layer, out var info))
-            humanoid.CustomBaseLayers[layer] = info with { Color = color };
-        else
-            humanoid.CustomBaseLayers[layer] = new(null, color);
-
-        if (sync)
-            Dirty(uid, humanoid);
-    }
-
-    /// <summary>
-    ///     Set a humanoid mob's sex. This will not change their gender.
-    /// </summary>
-    /// <param name="uid">The humanoid mob's UID.</param>
-    /// <param name="sex">The sex to set the mob to.</param>
-    /// <param name="sync">Whether to immediately synchronize this to the humanoid mob, or not.</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void SetSex(EntityUid uid, Sex sex, bool sync = true, HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (!Resolve(uid, ref humanoid) || humanoid.Sex == sex)
-            return;
-
-        var oldSex = humanoid.Sex;
-        humanoid.Sex = sex;
-        humanoid.MarkingSet.EnsureSexes(sex, _markingManager);
-        RaiseLocalEvent(uid, new SexChangedEvent(oldSex, sex));
-
-        if (sync)
-        {
-            Dirty(uid, humanoid);
-        }
-    }
-
-    /// <summary>
-    ///     Loads a humanoid character profile directly onto this humanoid mob.
-    /// </summary>
-    /// <param name="uid">The mob's entity UID.</param>
-    /// <param name="profile">The character profile to load.</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (profile == null)
-            return;
-
-        if (!Resolve(uid, ref humanoid))
-        {
-            return;
-        }
-
-        SetSpecies(uid, profile.Species, false, humanoid);
-        SetSex(uid, profile.Sex, false, humanoid);
-        humanoid.EyeColor = profile.Appearance.EyeColor;
-
-        SetSkinColor(uid, profile.Appearance.SkinColor, false);
-
-        humanoid.MarkingSet.Clear();
-
-        // 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<Marking, MarkingPrototype>();
-        foreach (var marking in profile.Appearance.Markings)
-        {
-            if (_markingManager.TryGetMarking(marking, out var prototype))
-            {
-                if (!prototype.ForcedColoring)
-                {
-                    AddMarking(uid, marking.MarkingId, marking.MarkingColors, false);
-                }
-                else
-                {
-                    markingFColored.Add(marking, prototype);
-                }
-            }
-        }
-
-        // Hair/facial hair - this may eventually be deprecated.
-        // 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, _proto)
-            ? profile.Appearance.SkinColor.WithAlpha(hairAlpha) : profile.Appearance.HairColor;
-        var facialHairColor = _markingManager.MustMatchSkin(profile.Species, HumanoidVisualLayers.FacialHair, out var facialHairAlpha, _proto)
-            ? profile.Appearance.SkinColor.WithAlpha(facialHairAlpha) : profile.Appearance.FacialHairColor;
-
-        if (_markingManager.Markings.TryGetValue(profile.Appearance.HairStyleId, out var hairPrototype) &&
-            _markingManager.CanBeApplied(profile.Species, profile.Sex, hairPrototype, _proto))
-        {
-            AddMarking(uid, profile.Appearance.HairStyleId, hairColor, false);
-        }
-
-        if (_markingManager.Markings.TryGetValue(profile.Appearance.FacialHairStyleId, out var facialHairPrototype) &&
-            _markingManager.CanBeApplied(profile.Species, profile.Sex, facialHairPrototype, _proto))
-        {
-            AddMarking(uid, profile.Appearance.FacialHairStyleId, facialHairColor, false);
-        }
-
-        humanoid.MarkingSet.EnsureSpecies(profile.Species, profile.Appearance.SkinColor, _markingManager, _proto);
-
-        // Finally adding marking with forced colors
-        foreach (var (marking, prototype) in markingFColored)
-        {
-            var markingColors = MarkingColoring.GetMarkingLayerColors(
-                prototype,
-                profile.Appearance.SkinColor,
-                profile.Appearance.EyeColor,
-                humanoid.MarkingSet
-            );
-            AddMarking(uid, marking.MarkingId, markingColors, false);
-        }
-
-        EnsureDefaultMarkings(uid, humanoid);
-
-        humanoid.Gender = profile.Gender;
-        if (TryComp<GrammarComponent>(uid, out var grammar))
-        {
-            _grammarSystem.SetGender((uid, grammar), profile.Gender);
-        }
-
-        humanoid.Age = profile.Age;
-
-        Dirty(uid, humanoid);
-    }
-
-    /// <summary>
-    ///     Adds a marking to this humanoid.
-    /// </summary>
-    /// <param name="uid">Humanoid mob's UID</param>
-    /// <param name="marking">Marking ID to use</param>
-    /// <param name="color">Color to apply to all marking layers of this marking</param>
-    /// <param name="sync">Whether to immediately sync this marking or not</param>
-    /// <param name="forced">If this marking was forced (ignores marking points)</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void AddMarking(EntityUid uid, string marking, Color? color = null, bool sync = true, bool forced = false, HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (!Resolve(uid, ref humanoid)
-            || !_markingManager.Markings.TryGetValue(marking, out var prototype))
-        {
-            return;
-        }
-
-        var markingObject = prototype.AsMarking();
-        markingObject.Forced = forced;
-        if (color != null)
-        {
-            for (var i = 0; i < prototype.Sprites.Count; i++)
-            {
-                markingObject.SetColor(i, color.Value);
-            }
-        }
-
-        humanoid.MarkingSet.AddBack(prototype.MarkingCategory, markingObject);
-
-        if (sync)
-            Dirty(uid, humanoid);
-    }
-
-    private void EnsureDefaultMarkings(EntityUid uid, HumanoidAppearanceComponent? humanoid)
-    {
-        if (!Resolve(uid, ref humanoid))
-        {
-            return;
-        }
-        humanoid.MarkingSet.EnsureDefault(humanoid.SkinColor, humanoid.EyeColor, _markingManager);
-    }
-
-    /// <summary>
-    ///
-    /// </summary>
-    /// <param name="uid">Humanoid mob's UID</param>
-    /// <param name="marking">Marking ID to use</param>
-    /// <param name="colors">Colors to apply against this marking's set of sprites.</param>
-    /// <param name="sync">Whether to immediately sync this marking or not</param>
-    /// <param name="forced">If this marking was forced (ignores marking points)</param>
-    /// <param name="humanoid">Humanoid component of the entity</param>
-    public void AddMarking(EntityUid uid, string marking, IReadOnlyList<Color> colors, bool sync = true, bool forced = false, HumanoidAppearanceComponent? humanoid = null)
-    {
-        if (!Resolve(uid, ref humanoid)
-            || !_markingManager.Markings.TryGetValue(marking, out var prototype))
-        {
-            return;
-        }
-
-        var markingObject = new Marking(marking, colors);
-        markingObject.Forced = forced;
-        humanoid.MarkingSet.AddBack(prototype.MarkingCategory, markingObject);
-
-        if (sync)
-            Dirty(uid, humanoid);
-    }
-
-    /// <summary>
-    /// Takes ID of the species prototype, returns UI-friendly name of the species.
-    /// </summary>
-    public string GetSpeciesRepresentation(string speciesId)
-    {
-        if (_proto.TryIndex<SpeciesPrototype>(speciesId, out var species))
-        {
-            return Loc.GetString(species.Name);
-        }
-
-        Log.Error("Tried to get representation of unknown species: {speciesId}");
-        return Loc.GetString("humanoid-appearance-component-unknown-species");
-    }
-
-    public string GetAgeRepresentation(string species, int age)
-    {
-        if (!_proto.TryIndex<SpeciesPrototype>(species, out var speciesPrototype))
-        {
-            Log.Error("Tried to get age representation of species that couldn't be indexed: " + species);
-            return Loc.GetString("identity-age-young");
-        }
-
-        if (age < speciesPrototype.YoungAge)
-        {
-            return Loc.GetString("identity-age-young");
-        }
-
-        if (age < speciesPrototype.OldAge)
-        {
-            return Loc.GetString("identity-age-middle-aged");
-        }
-
-        return Loc.GetString("identity-age-old");
-    }
-}
index 68b3533bd4dddb40f1f259402ddfcd95b4882a2f..ae9a18a0d62285f499f8d395d6f98bcf19cb54eb 100644 (file)
@@ -1,4 +1,7 @@
+using Content.Shared.Body;
 using Content.Shared.Humanoid.Markings;
+using Content.Shared.Humanoid.Prototypes;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Humanoid;
@@ -12,56 +15,29 @@ public enum HumanoidMarkingModifierKey
 [Serializable, NetSerializable]
 public sealed class HumanoidMarkingModifierMarkingSetMessage : BoundUserInterfaceMessage
 {
-    public MarkingSet MarkingSet { get; }
-    public bool ResendState { get; }
+    public Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> Markings { get; }
 
-    public HumanoidMarkingModifierMarkingSetMessage(MarkingSet set, bool resendState)
+    public HumanoidMarkingModifierMarkingSetMessage(Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> markings)
     {
-        MarkingSet = set;
-        ResendState = resendState;
+        Markings = markings;
     }
 }
 
-[Serializable, NetSerializable]
-public sealed class HumanoidMarkingModifierBaseLayersSetMessage : BoundUserInterfaceMessage
-{
-    public HumanoidMarkingModifierBaseLayersSetMessage(HumanoidVisualLayers layer, CustomBaseLayerInfo? info, bool resendState)
-    {
-        Layer = layer;
-        Info = info;
-        ResendState = resendState;
-    }
-
-    public HumanoidVisualLayers Layer { get; }
-    public CustomBaseLayerInfo? Info { get; }
-    public bool ResendState { get; }
-}
-
 [Serializable, NetSerializable]
 public sealed class HumanoidMarkingModifierState : BoundUserInterfaceState
 {
-    // TODO just use the component state, remove the BUI state altogether.
     public HumanoidMarkingModifierState(
-        MarkingSet markingSet,
-        string species,
-        Sex sex,
-        Color skinColor,
-        Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> customBaseLayers
+        Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> markings,
+        Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData> organData,
+        Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> organProfileData
     )
     {
-        MarkingSet = markingSet;
-        Species = species;
-        Sex = sex;
-        SkinColor = skinColor;
-        CustomBaseLayers = customBaseLayers;
+        Markings = markings;
+        OrganData = organData;
+        OrganProfileData = organProfileData;
     }
 
-    public MarkingSet MarkingSet { get; }
-    public string Species { get; }
-    public Sex Sex { get; }
-    public Color SkinColor { get; }
-    public Color EyeColor { get; }
-    public Color? HairColor { get; }
-    public Color? FacialHairColor { get; }
-    public Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> CustomBaseLayers { get; }
+    public Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> Markings { get; }
+    public Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData> OrganData { get; }
+    public Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> OrganProfileData { get; }
 }
index 990abd9e70824ddfd701c9544ff7b3b6fa8d9f0a..fdfce093f6aca76b275411297505bb1a62f729ed 100644 (file)
@@ -8,6 +8,7 @@ using Content.Shared.Humanoid;
 using Content.Shared.IdentityManagement.Components;
 using Content.Shared.Inventory;
 using Content.Shared.Inventory.Events;
+using Content.Shared.Preferences;
 using Content.Shared.VoiceMask;
 using Robust.Shared.Containers;
 using Robust.Shared.Enums;
@@ -27,7 +28,7 @@ public sealed class IdentitySystem : EntitySystem
     [Dependency] private readonly MetaDataSystem _metaData = default!;
     [Dependency] private readonly SharedContainerSystem _container = default!;
     [Dependency] private readonly SharedCriminalRecordsConsoleSystem _criminalRecordsConsole = default!;
-    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!;
+    [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
     [Dependency] private readonly SharedIdCardSystem _idCard = default!;
 
     // The name of the container holding the identity entity
@@ -206,11 +207,11 @@ public sealed class IdentitySystem : EntitySystem
     /// Gets an 'identity representation' of an entity, with their true name being the entity name
     /// and their 'presumed name' and 'presumed job' being the name/job on their ID card, if they have one.
     /// </summary>
-    private IdentityRepresentation GetIdentityRepresentation(Entity<InventoryComponent?, HumanoidAppearanceComponent?> target)
+    private IdentityRepresentation GetIdentityRepresentation(Entity<InventoryComponent?, HumanoidProfileComponent?> target)
     {
         var age = 18;
         var gender = Gender.Epicene;
-        var species = SharedHumanoidAppearanceSystem.DefaultSpecies;
+        var species = HumanoidCharacterProfile.DefaultSpecies;
 
         // Always use their actual age and gender, since that can't really be changed by an ID.
         if (Resolve(target, ref target.Comp2, false))
@@ -220,7 +221,7 @@ public sealed class IdentitySystem : EntitySystem
             species = target.Comp2.Species;
         }
 
-        var ageString = _humanoid.GetAgeRepresentation(species, age);
+        var ageString = _humanoidProfile.GetAgeRepresentation(species, age);
         var trueName = Name(target);
         if (!Resolve(target, ref target.Comp1, false))
             return new(trueName, gender, ageString, string.Empty);
index 800cd2de711830d582a1f5011d9b3c8152694b7e..05d8f92f5569f5b1fc77b60e17d64f9fee7c08f1 100644 (file)
@@ -239,7 +239,7 @@ public sealed class SharedKitchenSpikeSystem : EntitySystem
                 ent);
 
             // normally medium severity, but for humanoids high severity, so new players get relay'd to admin alerts.
-            var logSeverity = HasComp<HumanoidAppearanceComponent>(args.Target) ? LogImpact.High : LogImpact.Medium;
+            var logSeverity = HasComp<HumanoidProfileComponent>(args.Target) ? LogImpact.High : LogImpact.Medium;
 
             _logger.Add(LogType.Action,
                 logSeverity,
@@ -319,7 +319,7 @@ public sealed class SharedKitchenSpikeSystem : EntitySystem
         {
             _gibbing.Gib(args.Target.Value);
 
-            var logSeverity = HasComp<HumanoidAppearanceComponent>(args.Target) ? LogImpact.Extreme : LogImpact.High;
+            var logSeverity = HasComp<HumanoidProfileComponent>(args.Target) ? LogImpact.Extreme : LogImpact.High;
 
             _logger.Add(LogType.Gib,
                 logSeverity,
index 97738be228a7806dfcc6562a2504775d41087298..3175127995e0f234439c7d6e6f259292c434bcd9 100644 (file)
@@ -1,6 +1,9 @@
+using Content.Shared.Body;
 using Content.Shared.DoAfter;
+using Content.Shared.Humanoid;
 using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 
 namespace Content.Shared.MagicMirror;
 
@@ -8,10 +11,14 @@ namespace Content.Shared.MagicMirror;
 /// Allows humanoids to change their appearance mid-round.
 /// </summary>
 [RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(MagicMirrorSystem))]
 public sealed partial class MagicMirrorComponent : Component
 {
-    [DataField]
-    public DoAfterId? DoAfter;
+    /// <summary>
+    /// The id for a doAfter our <see cref="Target"/> is doing. Stored as an ushort so it can be networked and one day predicted.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public ushort? DoAfter;
 
     /// <summary>
     /// Magic mirror target, used for validating UI messages.
@@ -19,29 +26,17 @@ public sealed partial class MagicMirrorComponent : Component
     [DataField, AutoNetworkedField]
     public EntityUid? Target;
 
-    /// <summary>
-    /// Do after time to add a new slot, adding hair to a person
-    /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public TimeSpan AddSlotTime = TimeSpan.FromSeconds(7);
+    [DataField(required: true)]
+    public HashSet<ProtoId<OrganCategoryPrototype>> Organs;
 
-    /// <summary>
-    /// Do after time to remove a slot, removing hair from a person
-    /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public TimeSpan RemoveSlotTime = TimeSpan.FromSeconds(7);
-
-    /// <summary>
-    /// Do after time to change a person's hairstyle
-    /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public TimeSpan SelectSlotTime = TimeSpan.FromSeconds(7);
+    [DataField(required: true)]
+    public HashSet<HumanoidVisualLayers> Layers;
 
     /// <summary>
-    /// Do after time to change a person's hair color
+    /// Do after time to modify an entity's markings
     /// </summary>
     [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public TimeSpan ChangeSlotTime = TimeSpan.FromSeconds(7);
+    public TimeSpan ModifyTime = TimeSpan.FromSeconds(7);
 
     /// <summary>
     /// Sound emitted when slots are changed
diff --git a/Content.Shared/MagicMirror/MagicMirrorSystem.cs b/Content.Shared/MagicMirror/MagicMirrorSystem.cs
new file mode 100644 (file)
index 0000000..65a4d11
--- /dev/null
@@ -0,0 +1,288 @@
+using Content.Shared.Body;
+using Content.Shared.DoAfter;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Inventory;
+using Content.Shared.Popups;
+using Content.Shared.Tag;
+using Content.Shared.UserInterface;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.MagicMirror;
+
+public sealed class MagicMirrorSystem : EntitySystem
+{
+    [Dependency] private readonly SharedInteractionSystem _interaction = default!;
+    [Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
+    [Dependency] private readonly InventorySystem _inventory = default!;
+    [Dependency] private readonly TagSystem _tag = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
+
+    private static readonly ProtoId<TagPrototype> HidesHairTag = "HidesHair";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<MagicMirrorComponent, AfterInteractEvent>(OnMagicMirrorInteract);
+        SubscribeLocalEvent<MagicMirrorComponent, BeforeActivatableUIOpenEvent>(OnBeforeUIOpen);
+        SubscribeLocalEvent<MagicMirrorComponent, ActivatableUIOpenAttemptEvent>(OnAttemptOpenUI);
+
+        Subs.BuiEvents<MagicMirrorComponent>(MagicMirrorUiKey.Key,
+            subs =>
+            {
+                subs.Event<BoundUIClosedEvent>(OnUiClosed);
+                subs.Event<MagicMirrorSelectMessage>(OnMagicMirrorSelect);
+            });
+
+        SubscribeLocalEvent<MagicMirrorComponent, MagicMirrorSelectDoAfterEvent>(OnSelectSlotDoAfter);
+
+        SubscribeLocalEvent<MagicMirrorComponent, BoundUserInterfaceCheckRangeEvent>(OnMirrorRangeCheck);
+    }
+
+
+    private void OnMagicMirrorSelect(Entity<MagicMirrorComponent> ent, ref MagicMirrorSelectMessage args)
+    {
+        if (ent.Comp.Target is not { } target)
+            return;
+
+        // Check if the target getting their hair altered has any clothes that hides their hair
+        if (CheckHeadSlotOrClothes(target))
+        {
+            _popup.PopupEntity(
+                ent.Comp.Target == args.Actor
+                    ? Loc.GetString("magic-mirror-blocked-by-hat-self")
+                    : Loc.GetString("magic-mirror-blocked-by-hat-self-target", ("target", Identity.Entity(args.Actor, EntityManager))),
+                args.Actor,
+                args.Actor,
+                PopupType.Medium);
+            return;
+        }
+
+        if (ent.Comp.DoAfter.HasValue)
+        {
+            _doAfter.Cancel(target, ent.Comp.DoAfter.Value);
+            ent.Comp.DoAfter = null;
+        }
+
+        var doafterTime = ent.Comp.ModifyTime;
+        if (ent.Comp.Target == args.Actor)
+            doafterTime /= 3;
+
+        var doAfter = new MagicMirrorSelectDoAfterEvent()
+        {
+            Markings = args.Markings,
+        };
+
+        _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.Actor, doafterTime, doAfter, ent, target: target, used: ent)
+        {
+            DistanceThreshold = SharedInteractionSystem.InteractionRange,
+            BreakOnDamage = true,
+            BreakOnMove = true,
+            NeedHand = true,
+        },
+            out var doAfterId);
+
+        if (target == args.Actor)
+        {
+            _popup.PopupEntity(Loc.GetString("magic-mirror-change-slot-self"), target, target, PopupType.Medium);
+        }
+        else
+        {
+            _popup.PopupEntity(Loc.GetString("magic-mirror-change-slot-target", ("user", Identity.Entity(args.Actor, EntityManager))), target, target, PopupType.Medium);
+        }
+
+        ent.Comp.DoAfter = doAfterId?.Index;
+        _audio.PlayPredicted(ent.Comp.ChangeHairSound, ent, args.Actor);
+    }
+
+    private void OnSelectSlotDoAfter(Entity<MagicMirrorComponent> ent, ref MagicMirrorSelectDoAfterEvent args)
+    {
+        ent.Comp.DoAfter = null;
+
+        if (args.Handled || args.Target == null || args.Cancelled)
+            return;
+
+        if (ent.Comp.Target != args.Target)
+            return;
+
+        foreach (var (organ, markings) in args.Markings)
+        {
+            if (!ent.Comp.Organs.Contains(organ))
+            {
+                args.Markings.Remove(organ);
+                continue;
+            }
+
+            foreach (var layer in markings.Keys)
+            {
+                if (!ent.Comp.Layers.Contains(layer))
+                    markings.Remove(layer);
+            }
+        }
+
+        _visualBody.ApplyMarkings(args.Target.Value, args.Markings);
+    }
+
+    private void OnUiClosed(Entity<MagicMirrorComponent> ent, ref BoundUIClosedEvent args)
+    {
+        ent.Comp.Target = null;
+        Dirty(ent);
+    }
+
+    private void OnMagicMirrorInteract(Entity<MagicMirrorComponent> mirror, ref AfterInteractEvent args)
+    {
+        if (!args.CanReach || args.Target == null)
+            return;
+
+        UpdateInterface(mirror, args.Target.Value);
+        _userInterface.TryOpenUi(mirror.Owner, MagicMirrorUiKey.Key, args.User);
+    }
+
+    private void OnMirrorRangeCheck(EntityUid uid, MagicMirrorComponent component, ref BoundUserInterfaceCheckRangeEvent args)
+    {
+        if (args.Result == BoundUserInterfaceRangeResult.Fail)
+            return;
+
+        if (component.Target == null || !Exists(component.Target))
+        {
+            component.Target = null;
+            args.Result = BoundUserInterfaceRangeResult.Fail;
+            return;
+        }
+
+        if (!_interaction.InRangeUnobstructed(component.Target.Value, uid))
+            args.Result = BoundUserInterfaceRangeResult.Fail;
+    }
+
+    private void OnAttemptOpenUI(EntityUid uid, MagicMirrorComponent component, ref ActivatableUIOpenAttemptEvent args)
+    {
+        var user = component.Target ?? args.User;
+
+        if (!HasComp<VisualBodyComponent>(user))
+            args.Cancel();
+    }
+
+    private void OnBeforeUIOpen(Entity<MagicMirrorComponent> ent, ref BeforeActivatableUIOpenEvent args)
+    {
+        UpdateInterface(ent, args.User);
+    }
+
+    private void UpdateInterface(Entity<MagicMirrorComponent> ent, EntityUid target)
+    {
+        if (!_visualBody.TryGatherMarkingsData(target, ent.Comp.Layers, out var profiles, out var markings, out var applied))
+            return;
+
+        ent.Comp.Target = target;
+
+        foreach (var profile in profiles)
+        {
+            if (!ent.Comp.Organs.Contains(profile.Key))
+                profiles.Remove(profile.Key);
+        }
+
+        foreach (var marking in markings)
+        {
+            if (!ent.Comp.Organs.Contains(marking.Key))
+            {
+                profiles.Remove(marking.Key);
+                continue;
+            }
+
+            marking.Value.Layers.IntersectWith(ent.Comp.Layers);
+        }
+
+        foreach (var appliedPair in applied)
+        {
+            if (!ent.Comp.Organs.Contains(appliedPair.Key))
+                applied.Remove(appliedPair.Key);
+        }
+
+        // TODO: Component states
+        var state = new MagicMirrorUiState(profiles, markings, applied);
+        _userInterface.SetUiState(ent.Owner, MagicMirrorUiKey.Key, state);
+
+        Dirty(ent);
+    }
+
+
+    /// <summary>
+    /// Helper function that checks if the wearer has anything on their head
+    /// Or if they have any clothes that hides their hair
+    /// </summary>
+    private bool CheckHeadSlotOrClothes(EntityUid target)
+    {
+        if (!TryComp<InventoryComponent>(target, out var inventoryComp))
+            return false;
+
+        // any hat whatsoever will block haircutting
+        if (_inventory.TryGetSlotEntity(target, "head", out _, inventoryComp))
+        {
+            return true;
+        }
+
+        // maybe there's some kind of armor that has the HidesHair tag as well, so check every slot for it
+        var slots = _inventory.GetSlotEnumerator((target, inventoryComp), SlotFlags.WITHOUT_POCKET);
+        while (slots.MoveNext(out var slot))
+        {
+            if (slot.ContainedEntity != null && _tag.HasTag(slot.ContainedEntity.Value, HidesHairTag))
+            {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
+
+[Serializable, NetSerializable]
+public enum MagicMirrorUiKey : byte
+{
+    Key
+}
+
+[Serializable, NetSerializable]
+public sealed class MagicMirrorSelectMessage : BoundUserInterfaceMessage
+{
+    public MagicMirrorSelectMessage(Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> markings)
+    {
+        Markings = markings;
+    }
+
+    public Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> Markings { get; }
+}
+
+
+[Serializable, NetSerializable]
+public sealed class MagicMirrorUiState : BoundUserInterfaceState
+{
+    public MagicMirrorUiState(Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> profiles,
+        Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData> markings,
+        Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> applied)
+    {
+        OrganProfileData = profiles;
+        OrganMarkingData = markings;
+        AppliedMarkings = applied;
+    }
+
+    public NetEntity Target;
+
+    public Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> OrganProfileData;
+    public Dictionary<ProtoId<OrganCategoryPrototype>, OrganMarkingData> OrganMarkingData;
+    public Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> AppliedMarkings;
+}
+
+[Serializable, NetSerializable]
+public sealed partial class MagicMirrorSelectDoAfterEvent : DoAfterEvent
+{
+    public Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> Markings;
+
+    public override DoAfterEvent Clone() => this;
+}
diff --git a/Content.Shared/MagicMirror/SharedMagicMirrorSystem.cs b/Content.Shared/MagicMirror/SharedMagicMirrorSystem.cs
deleted file mode 100644 (file)
index 9f5348a..0000000
+++ /dev/null
@@ -1,226 +0,0 @@
-using Content.Shared.DoAfter;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Markings;
-using Content.Shared.Interaction;
-using Content.Shared.UserInterface;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.MagicMirror;
-
-public abstract class SharedMagicMirrorSystem : EntitySystem
-{
-    [Dependency] private readonly SharedInteractionSystem _interaction = default!;
-    [Dependency] protected readonly SharedUserInterfaceSystem UISystem = default!;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-        SubscribeLocalEvent<MagicMirrorComponent, AfterInteractEvent>(OnMagicMirrorInteract);
-        SubscribeLocalEvent<MagicMirrorComponent, BeforeActivatableUIOpenEvent>(OnBeforeUIOpen);
-        SubscribeLocalEvent<MagicMirrorComponent, ActivatableUIOpenAttemptEvent>(OnAttemptOpenUI);
-        SubscribeLocalEvent<MagicMirrorComponent, BoundUserInterfaceCheckRangeEvent>(OnMirrorRangeCheck);
-    }
-
-    private void OnMagicMirrorInteract(Entity<MagicMirrorComponent> mirror, ref AfterInteractEvent args)
-    {
-        if (!args.CanReach || args.Target == null)
-            return;
-
-        UpdateInterface(mirror, args.Target.Value, mirror);
-        UISystem.TryOpenUi(mirror.Owner, MagicMirrorUiKey.Key, args.User);
-    }
-
-    private void OnMirrorRangeCheck(EntityUid uid, MagicMirrorComponent component, ref BoundUserInterfaceCheckRangeEvent args)
-    {
-        if (args.Result == BoundUserInterfaceRangeResult.Fail)
-            return;
-
-        if (component.Target == null || !Exists(component.Target))
-        {
-            component.Target = null;
-            args.Result = BoundUserInterfaceRangeResult.Fail;
-            return;
-        }
-
-        if (!_interaction.InRangeUnobstructed(component.Target.Value, uid))
-            args.Result = BoundUserInterfaceRangeResult.Fail;
-    }
-
-    private void OnAttemptOpenUI(EntityUid uid, MagicMirrorComponent component, ref ActivatableUIOpenAttemptEvent args)
-    {
-        var user = component.Target ?? args.User;
-
-        if (!HasComp<HumanoidAppearanceComponent>(user))
-            args.Cancel();
-    }
-
-    private void OnBeforeUIOpen(Entity<MagicMirrorComponent> ent, ref BeforeActivatableUIOpenEvent args)
-    {
-        UpdateInterface(ent, args.User, ent);
-    }
-
-    protected void UpdateInterface(EntityUid mirrorUid, EntityUid targetUid, MagicMirrorComponent component)
-    {
-        if (!TryComp<HumanoidAppearanceComponent>(targetUid, out var humanoid))
-            return;
-
-        component.Target ??= targetUid;
-
-        var hair = humanoid.MarkingSet.TryGetCategory(MarkingCategories.Hair, out var hairMarkings)
-            ? new List<Marking>(hairMarkings)
-            : new();
-
-        var facialHair = humanoid.MarkingSet.TryGetCategory(MarkingCategories.FacialHair, out var facialHairMarkings)
-            ? new List<Marking>(facialHairMarkings)
-            : new();
-
-        var state = new MagicMirrorUiState(
-            humanoid.Species,
-            hair,
-            humanoid.MarkingSet.PointsLeft(MarkingCategories.Hair) + hair.Count,
-            facialHair,
-            humanoid.MarkingSet.PointsLeft(MarkingCategories.FacialHair) + facialHair.Count);
-
-        // TODO: Component states
-        component.Target = targetUid;
-        UISystem.SetUiState(mirrorUid, MagicMirrorUiKey.Key, state);
-        Dirty(mirrorUid, component);
-    }
-}
-
-[Serializable, NetSerializable]
-public enum MagicMirrorUiKey : byte
-{
-    Key
-}
-
-[Serializable, NetSerializable]
-public enum MagicMirrorCategory : byte
-{
-    Hair,
-    FacialHair
-}
-
-[Serializable, NetSerializable]
-public sealed class MagicMirrorSelectMessage : BoundUserInterfaceMessage
-{
-    public MagicMirrorSelectMessage(MagicMirrorCategory category, string marking, int slot)
-    {
-        Category = category;
-        Marking = marking;
-        Slot = slot;
-    }
-
-    public MagicMirrorCategory Category { get; }
-    public string Marking { get; }
-    public int Slot { get; }
-}
-
-[Serializable, NetSerializable]
-public sealed class MagicMirrorChangeColorMessage : BoundUserInterfaceMessage
-{
-    public MagicMirrorChangeColorMessage(MagicMirrorCategory category, List<Color> colors, int slot)
-    {
-        Category = category;
-        Colors = colors;
-        Slot = slot;
-    }
-
-    public MagicMirrorCategory Category { get; }
-    public List<Color> Colors { get; }
-    public int Slot { get; }
-}
-
-[Serializable, NetSerializable]
-public sealed class MagicMirrorRemoveSlotMessage : BoundUserInterfaceMessage
-{
-    public MagicMirrorRemoveSlotMessage(MagicMirrorCategory category, int slot)
-    {
-        Category = category;
-        Slot = slot;
-    }
-
-    public MagicMirrorCategory Category { get; }
-    public int Slot { get; }
-}
-
-[Serializable, NetSerializable]
-public sealed class MagicMirrorSelectSlotMessage : BoundUserInterfaceMessage
-{
-    public MagicMirrorSelectSlotMessage(MagicMirrorCategory category, int slot)
-    {
-        Category = category;
-        Slot = slot;
-    }
-
-    public MagicMirrorCategory Category { get; }
-    public int Slot { get; }
-}
-
-[Serializable, NetSerializable]
-public sealed class MagicMirrorAddSlotMessage : BoundUserInterfaceMessage
-{
-    public MagicMirrorAddSlotMessage(MagicMirrorCategory category)
-    {
-        Category = category;
-    }
-
-    public MagicMirrorCategory Category { get; }
-}
-
-[Serializable, NetSerializable]
-public sealed class MagicMirrorUiState : BoundUserInterfaceState
-{
-    public MagicMirrorUiState(string species, List<Marking> hair, int hairSlotTotal, List<Marking> facialHair, int facialHairSlotTotal)
-    {
-        Species = species;
-        Hair = hair;
-        HairSlotTotal = hairSlotTotal;
-        FacialHair = facialHair;
-        FacialHairSlotTotal = facialHairSlotTotal;
-    }
-
-    public NetEntity Target;
-
-    public string Species;
-
-    public List<Marking> Hair;
-    public int HairSlotTotal;
-
-    public List<Marking> FacialHair;
-    public int FacialHairSlotTotal;
-}
-
-[Serializable, NetSerializable]
-public sealed partial class MagicMirrorRemoveSlotDoAfterEvent : DoAfterEvent
-{
-    public override DoAfterEvent Clone() => this;
-    public MagicMirrorCategory Category;
-    public int Slot;
-}
-
-[Serializable, NetSerializable]
-public sealed partial class MagicMirrorAddSlotDoAfterEvent : DoAfterEvent
-{
-    public override DoAfterEvent Clone() => this;
-    public MagicMirrorCategory Category;
-}
-
-[Serializable, NetSerializable]
-public sealed partial class MagicMirrorSelectDoAfterEvent : DoAfterEvent
-{
-    public MagicMirrorCategory Category;
-    public int Slot;
-    public string Marking = string.Empty;
-
-    public override DoAfterEvent Clone() => this;
-}
-
-[Serializable, NetSerializable]
-public sealed partial class MagicMirrorChangeColorDoAfterEvent : DoAfterEvent
-{
-    public override DoAfterEvent Clone() => this;
-    public MagicMirrorCategory Category;
-    public int Slot;
-    public List<Color> Colors = new List<Color>();
-}
index 7cd9d4209e7bf1e641ab1062aee9ae4c2a726e99..f2070cd4212204f8dc01c4288250adcee9f80a4e 100644 (file)
@@ -607,8 +607,8 @@ public abstract partial class SharedMindSystem : EntitySystem
     /// </summary>
     public void AddAliveHumans(HashSet<Entity<MindComponent>> allHumans, EntityUid? exclude = null)
     {
-        // HumanoidAppearanceComponent is used to prevent mice, pAIs, etc from being chosen
-        var query = EntityQueryEnumerator<HumanoidAppearanceComponent, MobStateComponent>();
+        // HumanoidProfileComponent is used to prevent mice, pAIs, etc from being chosen
+        var query = EntityQueryEnumerator<HumanoidProfileComponent, MobStateComponent>();
         while (query.MoveNext(out var uid, out _, out var mobState))
         {
             // the player needs to have a mind and not be the excluded one +
index 5c0c3168989b65f99e350c19bdc8abb69ea3352c..a1990fa42e3849fc71d9ceb1dfcbe7b796fccb38 100644 (file)
@@ -1,3 +1,4 @@
+using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
 using Content.Shared.CCVar;
@@ -13,8 +14,12 @@ using Robust.Shared.Enums;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Serialization.Markdown;
 using Robust.Shared.Serialization;
 using Robust.Shared.Utility;
+using Robust.Shared;
+using YamlDotNet.RepresentationModel;
 
 namespace Content.Shared.Preferences
 {
@@ -25,6 +30,7 @@ namespace Content.Shared.Preferences
     [Serializable, NetSerializable]
     public sealed partial class HumanoidCharacterProfile : ICharacterProfile
     {
+        public static readonly ProtoId<SpeciesPrototype> DefaultSpecies = "Human";
         private static readonly Regex RestrictedNameRegex = new(@"[^A-Za-z0-9 '\-]");
         private static readonly Regex ICNameCaseRegex = new(@"^(?<word>\w)|\b(?<word>\w)(?=\w*$)");
 
@@ -72,7 +78,7 @@ namespace Content.Shared.Preferences
         /// Associated <see cref="SpeciesPrototype"/> for this profile.
         /// </summary>
         [DataField]
-        public ProtoId<SpeciesPrototype> Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
+        public ProtoId<SpeciesPrototype> Species { get; set; } = DefaultSpecies;
 
         [DataField]
         public int Age { get; set; } = 18;
@@ -186,7 +192,7 @@ namespace Content.Shared.Preferences
 
         /// <summary>
         ///     Get the default humanoid character profile, using internal constant values.
-        ///     Defaults to <see cref="SharedHumanoidAppearanceSystem.DefaultSpecies"/> for the species.
+        ///     Defaults to <see cref="DefaultSpecies"/> for the species.
         /// </summary>
         /// <returns></returns>
         public HumanoidCharacterProfile()
@@ -196,16 +202,19 @@ namespace Content.Shared.Preferences
         /// <summary>
         ///     Return a default character profile, based on species.
         /// </summary>
-        /// <param name="species">The species to use in this default profile. The default species is <see cref="SharedHumanoidAppearanceSystem.DefaultSpecies"/>.</param>
+        /// <param name="species">The species to use in this default profile. The default species is <see cref="DefaultSpecies"/>.</param>
+        /// <param name="sex">Self explanatory.</param>
         /// <returns>Humanoid character profile with default settings.</returns>
-        public static HumanoidCharacterProfile DefaultWithSpecies(string? species = null)
+        public static HumanoidCharacterProfile DefaultWithSpecies(ProtoId<SpeciesPrototype>? species = null, Sex? sex = null)
         {
-            species ??= SharedHumanoidAppearanceSystem.DefaultSpecies;
+            species ??= HumanoidCharacterProfile.DefaultSpecies;
+            sex ??= Sex.Male;
 
             return new()
             {
-                Species = species,
-                Appearance = HumanoidCharacterAppearance.DefaultWithSpecies(species),
+                Species = species.Value,
+                Sex = sex.Value,
+                Appearance = HumanoidCharacterAppearance.DefaultWithSpecies(species.Value, sex.Value),
             };
         }
 
@@ -226,7 +235,7 @@ namespace Content.Shared.Preferences
 
         public static HumanoidCharacterProfile RandomWithSpecies(string? species = null)
         {
-            species ??= SharedHumanoidAppearanceSystem.DefaultSpecies;
+            species ??= HumanoidCharacterProfile.DefaultSpecies;
 
             var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
             var random = IoCManager.Resolve<IRobustRandom>();
@@ -481,7 +490,7 @@ namespace Content.Shared.Preferences
 
             if (!prototypeManager.TryIndex(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false)
             {
-                Species = SharedHumanoidAppearanceSystem.DefaultSpecies;
+                Species = HumanoidCharacterProfile.DefaultSpecies;
                 speciesPrototype = prototypeManager.Index(Species);
             }
 
@@ -769,5 +778,51 @@ namespace Content.Shared.Preferences
         {
             return new HumanoidCharacterProfile(this);
         }
+
+        public DataNode ToDataNode(ISerializationManager? serialization = null, IConfigurationManager? configuration = null)
+        {
+            IoCManager.Resolve(ref serialization);
+            IoCManager.Resolve(ref configuration);
+
+            var export = new HumanoidProfileExportV2()
+            {
+                ForkId = configuration.GetCVar(CVars.BuildForkId),
+                Profile = this,
+            };
+
+            var dataNode = serialization.WriteValue(export, alwaysWrite: true, notNullableOverride: true);
+            return dataNode;
+        }
+
+        public static HumanoidCharacterProfile FromStream(Stream stream, ICommonSession session, ISerializationManager? serialization = null, IConfigurationManager? configuration = null)
+        {
+            IoCManager.Resolve(ref serialization);
+            IoCManager.Resolve(ref configuration);
+
+            using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
+            var yamlStream = new YamlStream();
+            yamlStream.Load(reader);
+
+            var root = yamlStream.Documents[0].RootNode;
+            HumanoidCharacterProfile profile;
+            if (root["version"].Equals(new YamlScalarNode("1")))
+            {
+                var export = serialization.Read<HumanoidProfileExportV1>(root.ToDataNode(), notNullableOverride: true);
+                profile = export.ToV2().Profile;
+            }
+            else if (root["version"].Equals(new YamlScalarNode("2")))
+            {
+                var export = serialization.Read<HumanoidProfileExportV2>(root.ToDataNode(), notNullableOverride: true);
+                profile = export.Profile;
+            }
+            else
+            {
+                throw new InvalidOperationException($"Unknown version {root["version"]}");
+            }
+
+            var collection = IoCManager.Instance;
+            profile.EnsureValid(session, collection!);
+            return profile;
+        }
     }
 }
index 51bf4e86594feee66624a7e37c3965945fc79ce5..9e2dc0ad9f711dedf44ae1352b0419015125b6cf 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.Body;
 using Content.Shared.DetailExaminable;
 using Content.Shared.Forensics.Systems;
 using Content.Shared.Humanoid;
@@ -12,7 +13,8 @@ namespace Content.Shared.Trigger.Systems;
 public sealed class DnaScrambleOnTriggerSystem : XOnTriggerSystem<DnaScrambleOnTriggerComponent>
 {
     [Dependency] private readonly MetaDataSystem _metaData = default!;
-    [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!;
+    [Dependency] private readonly HumanoidProfileSystem _humanoidProfile = default!;
+    [Dependency] private readonly SharedVisualBodySystem _visualBody = default!;
     [Dependency] private readonly IdentitySystem _identity = default!;
     [Dependency] private readonly SharedForensicsSystem _forensics = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -20,7 +22,7 @@ public sealed class DnaScrambleOnTriggerSystem : XOnTriggerSystem<DnaScrambleOnT
 
     protected override void OnTrigger(Entity<DnaScrambleOnTriggerComponent> ent, EntityUid target, ref TriggerEvent args)
     {
-        if (!TryComp<HumanoidAppearanceComponent>(target, out var humanoid))
+        if (!TryComp<HumanoidProfileComponent>(target, out var humanoid))
             return;
 
         args.Handled = true;
@@ -31,7 +33,8 @@ public sealed class DnaScrambleOnTriggerSystem : XOnTriggerSystem<DnaScrambleOnT
             return;
 
         var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
-        _humanoidAppearance.LoadProfile(target, newProfile, humanoid);
+        _visualBody.ApplyProfileTo(target, newProfile);
+        _humanoidProfile.ApplyProfileTo(target, newProfile);
         _metaData.SetEntityName(target, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
 
         // If the entity has the respective components, then scramble the dna and fingerprint strings.
index 70e7f009c7d94fa4e443dadecd59dcd6c75fec31..4173cb51738e6ebd89229f241f004612cc0cdf9e 100644 (file)
@@ -1,4 +1,6 @@
-using Content.Shared.Chat.Prototypes;
+using Content.Shared.Body;
+using Content.Shared.Chat.Prototypes;
+using Content.Shared.Humanoid;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -17,6 +19,12 @@ public sealed partial class WaggingComponent : Component
     [DataField]
     public EntityUid? ActionEntity;
 
+    [DataField]
+    public HumanoidVisualLayers Layer = HumanoidVisualLayers.Tail;
+
+    [DataField]
+    public ProtoId<OrganCategoryPrototype> Organ = "Torso";
+
     /// <summary>
     /// Suffix to add to get the animated marking.
     /// </summary>
index bbedffd8ecc448d2cd1d2ae30d00da1ce393a6a0..4441cb84c6ef30aa83520ff9c3c814cebcdff40c 100644 (file)
@@ -47,7 +47,7 @@ public sealed class WhistleSystem : EntitySystem
         StealthComponent? stealth = null;
 
         foreach (var iterator in
-            _entityLookup.GetEntitiesInRange<HumanoidAppearanceComponent>(_transform.GetMapCoordinates(uid), component.Distance))
+            _entityLookup.GetEntitiesInRange<HumanoidProfileComponent>(_transform.GetMapCoordinates(uid), component.Distance))
         {
             //Avoid pinging invisible entities
             if (TryComp(iterator, out stealth) && stealth.Enabled)
index 4c941f176771799fa44cbe4cf95367bfbec8fc1f..d1f96c3ad72b93408adeb840d5eefaab40f2ef04 100644 (file)
@@ -1,8 +1,9 @@
+using Content.Shared.Body;
 using Content.Shared.Chat.Prototypes;
 using Content.Shared.Chemistry.Components;
-using Content.Shared.Chemistry.Reagent;
 using Content.Shared.Damage;
 using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Markings;
 using Content.Shared.Roles;
 using Content.Shared.StatusIcon;
 using Robust.Shared.Audio;
@@ -57,12 +58,6 @@ public sealed partial class ZombieComponent : Component
     [DataField("eyeColor")]
     public Color EyeColor = new(0.96f, 0.13f, 0.24f);
 
-    /// <summary>
-    /// The base layer to apply to any 'external' humanoid layers upon zombification.
-    /// </summary>
-    [DataField("baseLayerExternal")]
-    public string BaseLayerExternal = "MobHumanoidMarkingMatchSkin";
-
     /// <summary>
     /// The attack arc of the zombie
     /// </summary>
@@ -75,23 +70,11 @@ public sealed partial class ZombieComponent : Component
     [DataField("zombieRoleId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
     public string ZombieRoleId = "Zombie";
 
-    /// <summary>
-    /// The CustomBaseLayers of the humanoid to restore in case of cloning
-    /// </summary>
-    [DataField("beforeZombifiedCustomBaseLayers")]
-    public Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> BeforeZombifiedCustomBaseLayers = new ();
-
-    /// <summary>
-    /// The skin color of the humanoid to restore in case of cloning
-    /// </summary>
-    [DataField("beforeZombifiedSkinColor")]
-    public Color BeforeZombifiedSkinColor;
+    [DataField]
+    public Dictionary<ProtoId<OrganCategoryPrototype>, OrganProfileData> BeforeZombifiedProfiles;
 
-    /// <summary>
-    /// The eye color of the humanoid to restore in case of cloning
-    /// </summary>
-    [DataField("beforeZombifiedEyeColor")]
-    public Color BeforeZombifiedEyeColor;
+    [DataField]
+    public Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>> BeforeZombifiedMarkings;
 
     [DataField("emoteId")]
     public ProtoId<EmoteSoundsPrototype>? EmoteSoundsId = "Zombie";
index 154b91ee1c71014430bbb3347a1aaac404469297..8982988be4c2a2edd5053d5876fab22e08e1a2b3 100644 (file)
@@ -1,37 +1,63 @@
-markings-used = Used Markings
-markings-unused = Unused Markings
-markings-add = Add Marking
-markings-remove = Remove Marking
-markings-rank-up = Up
-markings-rank-down = Down
 markings-search = Search
-marking-points-remaining = Markings left: {$points}
-marking-used = {$marking-name}
-marking-used-forced = {$marking-name} (Forced)
-marking-slot-add = Add
-marking-slot-remove = Remove
-marking-slot = Slot {$number}
+-markings-selection = { $selectable ->
+    [0] You have no markings remaining.
+    [one] You can select one more marking.
+   *[other] You can select { $selectable } more markings.
+}
+markings-limits = { $required ->
+    [true] { $count ->
+        [0] Select at least one marking.
+        [one] Select one marking.
+       *[other] Select at least one marking and up to {$count} markings. { -markings-selection(selectable: $selectable) }
+    }
+   *[false] { $count ->
+        [0] Select any number of markings.
+        [one] Select up to one marking.
+       *[other] Select up to {$count} markings. { -markings-selection(selectable: $selectable) }
+    }
+}
+markings-reorder = Reorder markings
 
-humanoid-marking-modifier-force = Force
-humanoid-marking-modifier-ignore-species = Ignore Species
+humanoid-marking-modifier-respect-limits = Respect limits
+humanoid-marking-modifier-respect-group-sex = Respect group & sex restrictions
 humanoid-marking-modifier-base-layers = Base layers
 humanoid-marking-modifier-enable = Enable
 humanoid-marking-modifier-prototype-id = Prototype id:
 
 # Categories
 
-markings-category-Special = Special
-markings-category-Hair = Hair
-markings-category-FacialHair = Facial Hair
-markings-category-Head = Head
-markings-category-HeadTop = Head (Top)
-markings-category-HeadSide = Head (Side)
-markings-category-Snout = Snout
-markings-category-SnoutCover = Snout (Cover)
-markings-category-UndergarmentTop = Undergarment (Top)
-markings-category-UndergarmentBottom = Undergarment (Bottom)
-markings-category-Chest = Chest
-markings-category-Arms = Arms
-markings-category-Legs = Legs
-markings-category-Tail = Tail
-markings-category-Overlay = Overlay
+markings-organ-Torso = Torso
+markings-organ-Head = Head
+markings-organ-ArmLeft = Left Arm
+markings-organ-ArmRight = Right Arm
+markings-organ-HandRight = Right Hand
+markings-organ-HandLeft = Left Hand
+markings-organ-LegLeft = Left Leg
+markings-organ-LegRight = Right Leg
+markings-organ-FootLeft = Left Foot
+markings-organ-FootRight = Right Foot
+markings-organ-Eyes = Eyes
+
+markings-layer-Special = Special
+markings-layer-Tail = Tail
+markings-layer-Tail-Moth = Wings
+markings-layer-Hair = Hair
+markings-layer-FacialHair = Facial Hair
+markings-layer-UndergarmentTop = Undershirt
+markings-layer-UndergarmentBottom = Underpants
+markings-layer-Chest = Chest
+markings-layer-Head = Head
+markings-layer-Snout = Snout
+markings-layer-SnoutCover = Snout (Cover)
+markings-layer-HeadSide = Head (Side)
+markings-layer-HeadTop = Head (Top)
+markings-layer-Eyes = Eyes
+markings-layer-RArm = Right Arm
+markings-layer-LArm = Left Arm
+markings-layer-RHand = Right Hand
+markings-layer-LHand = Left Hand
+markings-layer-RLeg = Right Leg
+markings-layer-LLeg = Left Leg
+markings-layer-RFoot = Right Foot
+markings-layer-LFoot = Left Foot
+markings-layer-Overlay = Overlay
index 10c7623a2d2c246dd5b21f0730dba65a824add9a..897dd8babaf075db601e0e71557dbb4eaf6b8da6 100644 (file)
@@ -1,3 +1,38 @@
+- type: markingsGroup
+  parent: Undergarments
+  id: Arachnid
+  onlyGroupWhitelisted: true
+  limits:
+    enum.HumanoidVisualLayers.Hair:
+      limit: 0
+      required: false
+    enum.HumanoidVisualLayers.FacialHair:
+      limit: 0
+      required: false
+    enum.HumanoidVisualLayers.Tail:
+      limit: 1
+      required: true
+      default: [ ArachnidAppendagesDefault ]
+    enum.HumanoidVisualLayers.Chest:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.HeadSide:
+      limit: 1
+      required: true
+      default: [ ArachnidCheliceraeDownwards ]
+    enum.HumanoidVisualLayers.LArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RLeg:
+      limit: 1
+      required: false
+
 - type: entity
   parent: BaseSpeciesAppearance
   id: AppearanceArachnid
   - type: Inventory
     templateId: arachnid
     speciesId: arachnid
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganArachnidTorso
-        - id: OrganArachnidHead
-        - id: OrganArachnidArmLeft
-        - id: OrganArachnidArmRight
-        - id: OrganArachnidHandRight
-        - id: OrganArachnidHandLeft
-        - id: OrganArachnidLegLeft
-        - id: OrganArachnidLegRight
-        - id: OrganArachnidFootLeft
-        - id: OrganArachnidFootRight
-        - id: OrganArachnidBrain
-        - id: OrganArachnidEyes
-        - id: OrganArachnidTongue
-        - id: OrganArachnidAppendix
-        - id: OrganArachnidEars
-        - id: OrganArachnidLungs
-        - id: OrganArachnidHeart
-        - id: OrganArachnidStomach
-        - id: OrganArachnidLiver
-        - id: OrganArachnidKidneys
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganArachnidTorso
+      Head: OrganArachnidHead
+      ArmLeft: OrganArachnidArmLeft
+      ArmRight: OrganArachnidArmRight
+      HandRight: OrganArachnidHandRight
+      HandLeft: OrganArachnidHandLeft
+      LegLeft: OrganArachnidLegLeft
+      LegRight: OrganArachnidLegRight
+      FootLeft: OrganArachnidFootLeft
+      FootRight: OrganArachnidFootRight
+      Brain: OrganArachnidBrain
+      Eyes: OrganArachnidEyes
+      Tongue: OrganArachnidTongue
+      Appendix: OrganArachnidAppendix
+      Ears: OrganArachnidEars
+      Lungs: OrganArachnidLungs
+      Heart: OrganArachnidHeart
+      Stomach: OrganArachnidStomach
+      Liver: OrganArachnidLiver
+      Kidneys: OrganArachnidKidneys
+  - type: HumanoidProfile
     species: Arachnid
 
 - type: entity
     sprite: Mobs/Species/Arachnid/organs.rsi
 
 - type: entity
-  parent: OrganArachnid
+  id: OrganArachnidVisual
+  abstract: true
+  components:
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Species/Arachnid/parts.rsi
+  - type: VisualOrganMarkings
+    markingData:
+      group: Arachnid
+
+- type: entity
+  parent: [ OrganArachnid, OrganArachnidVisual ]
   id: OrganArachnidExternal
   abstract: true
   components:
   id: OrganArachnidBrain
 
 - type: entity
-  parent: [ OrganBaseEyes, OrganArachnidInternal ]
+  parent: [ OrganArachnidVisual, OrganBaseEyes, OrganArachnidInternal ]
   id: OrganArachnidEyes
 
 - type: entity
index 630fd73b9954517f9acfaad2f4234a21538e6cf9..93aa49b36fc35e0e9ed220f37f4b25a312b76da0 100644 (file)
@@ -1,3 +1,37 @@
+- type: markingsGroup
+  parent: Undergarments
+  id: Diona
+  onlyGroupWhitelisted: true
+  limits:
+    enum.HumanoidVisualLayers.Head:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.HeadTop:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.HeadSide:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.Chest:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.LArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.Overlay:
+      limit: 1
+      required: true
+      default: [ DionaVineOverlay ]
+
 - type: entity
   parent: BaseSpeciesAppearance
   id: AppearanceDiona
           32:
             sprite: Mobs/Species/Human/displacement.rsi
             state: jumpsuit-female
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganDionaTorso
-        - id: OrganDionaHead
-        - id: OrganDionaArmLeft
-        - id: OrganDionaArmRight
-        - id: OrganDionaHandRight
-        - id: OrganDionaHandLeft
-        - id: OrganDionaLegLeft
-        - id: OrganDionaLegRight
-        - id: OrganDionaFootLeft
-        - id: OrganDionaFootRight
-        - id: OrganDionaBrainNymphing
-        - id: OrganDionaEyes
-        - id: OrganDionaLungsNymphing
-        - id: OrganDionaStomachNymphing
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganDionaTorso
+      Head: OrganDionaHead
+      ArmLeft: OrganDionaArmLeft
+      ArmRight: OrganDionaArmRight
+      HandRight: OrganDionaHandRight
+      HandLeft: OrganDionaHandLeft
+      LegLeft: OrganDionaLegLeft
+      LegRight: OrganDionaLegRight
+      FootLeft: OrganDionaFootLeft
+      FootRight: OrganDionaFootRight
+      Brain: OrganDionaBrainNymphing
+      Eyes: OrganDionaEyes
+      Lungs: OrganDionaLungsNymphing
+      Stomach: OrganDionaStomachNymphing
+  - type: HumanoidProfile
     species: Diona
 
 - type: entity
     sprite: Mobs/Species/Diona/organs.rsi
 
 - type: entity
-  parent: OrganDiona
+  id: OrganDionaVisual
+  abstract: true
+  components:
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Species/Diona/parts.rsi
+  - type: VisualOrganMarkings
+    markingData:
+      group: Diona
+
+- type: entity
+  parent: [ OrganDiona, OrganDionaVisual ]
   id: OrganDionaExternal
   abstract: true
   components:
   id: OrganDionaBrain
 
 - type: entity
-  parent: [ OrganBaseEyes, OrganDionaInternal ]
+  parent: [ OrganDionaVisual, OrganBaseEyes, OrganDionaInternal ]
   id: OrganDionaEyes
+  components:
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Customization/eyes.rsi
+      state: diona
 
 - type: entity
   parent: [ OrganBaseLungs, OrganDionaInternal, OrganDionaMetabolizer ]
index cfc9e43a34ed5ef75fc94937338abd11fafc7c7c..addecdacdbabade896f14c38ed2054cf32b9c872 100644 (file)
           32:
             sprite: Mobs/Species/Human/displacement.rsi
             state: jumpsuit-female
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganHumanTorso
-        - id: OrganHumanHead
-        - id: OrganHumanArmLeft
-        - id: OrganHumanArmRight
-        - id: OrganHumanHandRight
-        - id: OrganHumanHandLeft
-        - id: OrganHumanLegLeft
-        - id: OrganHumanLegRight
-        - id: OrganHumanFootLeft
-        - id: OrganHumanFootRight
-        - id: OrganHumanBrain
-        - id: OrganHumanEyes
-        - id: OrganHumanTongue
-        - id: OrganHumanAppendix
-        - id: OrganHumanEars
-        - id: OrganHumanLungs
-        - id: OrganDwarfHeart
-        - id: OrganDwarfStomach
-        - id: OrganDwarfLiver
-        - id: OrganHumanKidneys
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganHumanTorso
+      Head: OrganHumanHead
+      ArmLeft: OrganHumanArmLeft
+      ArmRight: OrganHumanArmRight
+      HandRight: OrganHumanHandRight
+      HandLeft: OrganHumanHandLeft
+      LegLeft: OrganHumanLegLeft
+      LegRight: OrganHumanLegRight
+      FootLeft: OrganHumanFootLeft
+      FootRight: OrganHumanFootRight
+      Brain: OrganHumanBrain
+      Eyes: OrganHumanEyes
+      Tongue: OrganHumanTongue
+      Appendix: OrganHumanAppendix
+      Ears: OrganHumanEars
+      Lungs: OrganHumanLungs
+      Heart: OrganDwarfHeart
+      Stomach: OrganDwarfStomach
+      Liver: OrganDwarfLiver
+      Kidneys: OrganHumanKidneys
+  - type: HumanoidProfile
     species: Dwarf
   - type: ScaleVisuals
     scale: 1, 0.8
index 6b528e9a0caf392f6ca75f6923b952f96973ba70..b32cbac91d95f37b472bef4e14d94d9248d01d3c 100644 (file)
           32:
             sprite: Mobs/Species/Human/displacement.rsi
             state: jumpsuit-female
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganGingerbreadTorso
-        - id: OrganGingerbreadHead
-        - id: OrganGingerbreadArmLeft
-        - id: OrganGingerbreadArmRight
-        - id: OrganGingerbreadHandRight
-        - id: OrganGingerbreadHandLeft
-        - id: OrganGingerbreadLegLeft
-        - id: OrganGingerbreadLegRight
-        - id: OrganGingerbreadFootLeft
-        - id: OrganGingerbreadFootRight
-        - id: OrganHumanBrain
-        - id: OrganHumanEyes
-        - id: OrganHumanTongue
-        - id: OrganHumanAppendix
-        - id: OrganHumanEars
-        - id: OrganHumanLungs
-        - id: OrganHumanHeart
-        - id: OrganHumanStomach
-        - id: OrganHumanLiver
-        - id: OrganHumanKidneys
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganGingerbreadTorso
+      Head: OrganGingerbreadHead
+      ArmLeft: OrganGingerbreadArmLeft
+      ArmRight: OrganGingerbreadArmRight
+      HandRight: OrganGingerbreadHandRight
+      HandLeft: OrganGingerbreadHandLeft
+      LegLeft: OrganGingerbreadLegLeft
+      LegRight: OrganGingerbreadLegRight
+      FootLeft: OrganGingerbreadFootLeft
+      FootRight: OrganGingerbreadFootRight
+      Brain: OrganHumanBrain
+      Eyes: OrganHumanEyes
+      Tongue: OrganHumanTongue
+      Appendix: OrganHumanAppendix
+      Ears: OrganHumanEars
+      Lungs: OrganHumanLungs
+      Heart: OrganHumanHeart
+      Stomach: OrganHumanStomach
+      Liver: OrganHumanLiver
+      Kidneys: OrganHumanKidneys
+  - type: HumanoidProfile
     species: Gingerbread
 
 - type: entity
   components:
   - type: Sprite
     sprite: Mobs/Species/Gingerbread/parts.rsi
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Species/Gingerbread/parts.rsi
+  - type: VisualOrganMarkings
+    markingData:
+      group: None
 
 - type: entity
   parent: [ OrganBaseTorso, OrganGingerbreadExternal ]
index 5f4d65d6a796b5bf9129b73edd90dd409b159f6b..aa67c15ec780826f517253041c316b0052c15bc2 100644 (file)
@@ -1,3 +1,44 @@
+- type: markingsGroup
+  parent: Undergarments
+  id: Human
+  limits:
+    enum.HumanoidVisualLayers.Hair:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.FacialHair:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.Chest:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.Snout:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LHand:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RHand:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LFoot:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RFoot:
+      limit: 1
+      required: false
+
 - type: entity
   parent: BaseSpeciesAppearance
   id: AppearanceHuman
           32:
             sprite: Mobs/Species/Human/displacement.rsi
             state: jumpsuit-female
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganHumanTorso
-        - id: OrganHumanHead
-        - id: OrganHumanArmLeft
-        - id: OrganHumanArmRight
-        - id: OrganHumanHandRight
-        - id: OrganHumanHandLeft
-        - id: OrganHumanLegLeft
-        - id: OrganHumanLegRight
-        - id: OrganHumanFootLeft
-        - id: OrganHumanFootRight
-        - id: OrganHumanBrain
-        - id: OrganHumanEyes
-        - id: OrganHumanTongue
-        - id: OrganHumanAppendix
-        - id: OrganHumanEars
-        - id: OrganHumanLungs
-        - id: OrganHumanHeart
-        - id: OrganHumanStomach
-        - id: OrganHumanLiver
-        - id: OrganHumanKidneys
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganHumanTorso
+      Head: OrganHumanHead
+      ArmLeft: OrganHumanArmLeft
+      ArmRight: OrganHumanArmRight
+      HandRight: OrganHumanHandRight
+      HandLeft: OrganHumanHandLeft
+      LegLeft: OrganHumanLegLeft
+      LegRight: OrganHumanLegRight
+      FootLeft: OrganHumanFootLeft
+      FootRight: OrganHumanFootRight
+      Brain: OrganHumanBrain
+      Eyes: OrganHumanEyes
+      Tongue: OrganHumanTongue
+      Appendix: OrganHumanAppendix
+      Ears: OrganHumanEars
+      Lungs: OrganHumanLungs
+      Heart: OrganHumanHeart
+      Stomach: OrganHumanStomach
+      Liver: OrganHumanLiver
+      Kidneys: OrganHumanKidneys
+  - type: HumanoidProfile
     species: Human
-    hideLayersOnEquip:
-    - Hair
-    - Snout
 
 - type: entity
   parent:
   components:
   - type: Sprite
     sprite: Mobs/Species/Human/parts.rsi
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Species/Human/parts.rsi
+  - type: VisualOrganMarkings
+    markingData:
+      group: Human
 
 - type: entity
   parent: [ OrganBaseTorso, OrganHumanExternal ]
   id: OrganHumanBrain
 
 - type: entity
-  parent: [ OrganBaseEyes, OrganHumanInternal ]
+  parent: [ OrganBaseEyes, OrganHumanInternal, OrganHumanExternal ]
   id: OrganHumanEyes
+  components:
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Customization/eyes.rsi
 
 - type: entity
   parent: [ OrganBaseTongue, OrganHumanInternal ]
index d274a06cd68a0be210c0dd854a1f0e22b69bb517..22c08d540fb7d555d911f1c8a7bfd227a93b1bb2 100644 (file)
@@ -1,3 +1,53 @@
+- type: markingsGroup
+  parent: Undergarments
+  id: Moth
+  onlyGroupWhitelisted: true
+  limits:
+    enum.HumanoidVisualLayers.Hair:
+      limit: 0
+      required: false
+    enum.HumanoidVisualLayers.FacialHair:
+      limit: 0
+      required: false
+    enum.HumanoidVisualLayers.Tail:
+      limit: 1
+      required: true
+      default: [ MothWingsDefault ]
+    enum.HumanoidVisualLayers.Chest:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.HeadTop:
+      limit: 1
+      required: true
+      default: [ MothAntennasDefault ]
+    enum.HumanoidVisualLayers.Snout:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LHand:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RHand:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LFoot:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RFoot:
+      limit: 1
+      required: false
+
 - type: entity
   parent: BaseSpeciesAppearance
   id: AppearanceMoth
           32:
             sprite: Mobs/Species/Moth/displacement.rsi
             state: shoes
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganMothTorso
-        - id: OrganMothHead
-        - id: OrganMothArmLeft
-        - id: OrganMothArmRight
-        - id: OrganMothHandRight
-        - id: OrganMothHandLeft
-        - id: OrganMothLegLeft
-        - id: OrganMothLegRight
-        - id: OrganMothFootLeft
-        - id: OrganMothFootRight
-        - id: OrganMothBrain
-        - id: OrganMothEyes
-        - id: OrganMothTongue
-        - id: OrganMothAppendix
-        - id: OrganMothEars
-        - id: OrganMothLungs
-        - id: OrganMothHeart
-        - id: OrganMothStomach
-        - id: OrganMothLiver
-        - id: OrganMothKidneys
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganMothTorso
+      Head: OrganMothHead
+      ArmLeft: OrganMothArmLeft
+      ArmRight: OrganMothArmRight
+      HandRight: OrganMothHandRight
+      HandLeft: OrganMothHandLeft
+      LegLeft: OrganMothLegLeft
+      LegRight: OrganMothLegRight
+      FootLeft: OrganMothFootLeft
+      FootRight: OrganMothFootRight
+      Brain: OrganMothBrain
+      Eyes: OrganMothEyes
+      Tongue: OrganMothTongue
+      Appendix: OrganMothAppendix
+      Ears: OrganMothEars
+      Lungs: OrganMothLungs
+      Heart: OrganMothHeart
+      Stomach: OrganMothStomach
+      Liver: OrganMothLiver
+      Kidneys: OrganMothKidneys
+  - type: HumanoidProfile
     species: Moth
 
 - type: entity
     sprite: Mobs/Species/Human/organs.rsi
 
 - type: entity
-  parent: OrganMoth
+  id: OrganMothVisual
+  abstract: true
+  components:
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Species/Moth/parts.rsi
+  - type: VisualOrganMarkings
+    markingData:
+      group: Moth
+
+- type: entity
+  parent: [ OrganMoth, OrganMothVisual ]
   id: OrganMothExternal
   abstract: true
   components:
   id: OrganMothBrain
 
 - type: entity
-  parent: [ OrganBaseEyes, OrganMothInternal ]
+  parent: [ OrganMothVisual, OrganBaseEyes, OrganMothInternal ]
   id: OrganMothEyes
 
 - type: entity
index 4a7a4f23f7c2c8ff4c92fc2ab655006a40ae2c96..2c0d18056b1ddc3f3251dd4832d44f9d5949a6f9 100644 (file)
@@ -1,3 +1,56 @@
+- type: markingsGroup
+  parent: Undergarments
+  id: Reptilian
+  onlyGroupWhitelisted: true
+  limits:
+    enum.HumanoidVisualLayers.Hair:
+      limit: 0
+      required: false
+    enum.HumanoidVisualLayers.FacialHair:
+      limit: 0
+      required: false
+    enum.HumanoidVisualLayers.Tail:
+      limit: 1
+      required: true
+      default: [ LizardTailSmooth ]
+    enum.HumanoidVisualLayers.Chest:
+      limit: 3
+      required: false
+    enum.HumanoidVisualLayers.Snout:
+      limit: 1
+      required: true
+      default: [ LizardSnoutRound ]
+    enum.HumanoidVisualLayers.HeadTop:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.HeadSide:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LHand:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RHand:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LFoot:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RFoot:
+      limit: 1
+      required: false
+
 - type: entity
   parent: BaseSpeciesAppearance
   id: AppearanceReptilian
           32:
             sprite: Mobs/Species/Reptilian/displacement.rsi
             state: mask
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganReptilianTorso
-        - id: OrganReptilianHead
-        - id: OrganReptilianArmLeft
-        - id: OrganReptilianArmRight
-        - id: OrganReptilianHandRight
-        - id: OrganReptilianHandLeft
-        - id: OrganReptilianLegLeft
-        - id: OrganReptilianLegRight
-        - id: OrganReptilianFootLeft
-        - id: OrganReptilianFootRight
-        - id: OrganReptilianBrain
-        - id: OrganReptilianEyes
-        - id: OrganReptilianTongue
-        - id: OrganReptilianAppendix
-        - id: OrganReptilianEars
-        - id: OrganReptilianLungs
-        - id: OrganReptilianHeart
-        - id: OrganReptilianStomach
-        - id: OrganReptilianLiver
-        - id: OrganReptilianKidneys
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganReptilianTorso
+      Head: OrganReptilianHead
+      ArmLeft: OrganReptilianArmLeft
+      ArmRight: OrganReptilianArmRight
+      HandRight: OrganReptilianHandRight
+      HandLeft: OrganReptilianHandLeft
+      LegLeft: OrganReptilianLegLeft
+      LegRight: OrganReptilianLegRight
+      FootLeft: OrganReptilianFootLeft
+      FootRight: OrganReptilianFootRight
+      Brain: OrganReptilianBrain
+      Eyes: OrganReptilianEyes
+      Tongue: OrganReptilianTongue
+      Appendix: OrganReptilianAppendix
+      Ears: OrganReptilianEars
+      Lungs: OrganReptilianLungs
+      Heart: OrganReptilianHeart
+      Stomach: OrganReptilianStomach
+      Liver: OrganReptilianLiver
+      Kidneys: OrganReptilianKidneys
+  - type: HumanoidProfile
     species: Reptilian
-    hideLayersOnEquip:
-    - Snout
-    - HeadTop
-    - HeadSide
-    - Tail
-    undergarmentBottom: UndergarmentBottomBoxersReptilian
 
 - type: entity
   parent:
   components:
   - type: Sprite
     sprite: Mobs/Species/Reptilian/parts.rsi
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Species/Reptilian/parts.rsi
+  - type: VisualOrganMarkings
+    markingData:
+      group: Reptilian
 
 - type: entity
   parent: [ OrganBaseTorso, OrganReptilianExternal ]
   id: OrganReptilianBrain
 
 - type: entity
-  parent: [ OrganBaseEyes, OrganReptilianInternal ]
+  parent: [ OrganBaseEyes, OrganReptilianInternal, OrganReptilianExternal ]
   id: OrganReptilianEyes
+  components:
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Customization/eyes.rsi
 
 - type: entity
   parent: [ OrganBaseTongue, OrganReptilianInternal ]
index 5cddd68e1ad5f477b9d1215309e42de25ada0bac..dd2004aebafd6971d22e4f44de16573a000ed610 100644 (file)
@@ -1,3 +1,44 @@
+- type: markingsGroup
+  id: Skeleton
+  onlyGroupWhitelisted: true
+  limits:
+    enum.HumanoidVisualLayers.Hair:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.FacialHair:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.Chest:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.Snout:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LHand:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RHand:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LFoot:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RFoot:
+      limit: 1
+      required: false
+
 - type: entity
   parent: BaseSpeciesAppearance
   id: AppearanceSkeletonPerson
           32:
             sprite: Mobs/Species/Human/displacement.rsi
             state: jumpsuit-female
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganSkeletonPersonTorso
-        - id: OrganSkeletonPersonHead
-        - id: OrganSkeletonPersonArmLeft
-        - id: OrganSkeletonPersonArmRight
-        - id: OrganSkeletonPersonHandRight
-        - id: OrganSkeletonPersonHandLeft
-        - id: OrganSkeletonPersonLegLeft
-        - id: OrganSkeletonPersonLegRight
-        - id: OrganSkeletonPersonFootLeft
-        - id: OrganSkeletonPersonFootRight
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganSkeletonPersonTorso
+      Head: OrganSkeletonPersonHead
+      ArmLeft: OrganSkeletonPersonArmLeft
+      ArmRight: OrganSkeletonPersonArmRight
+      HandRight: OrganSkeletonPersonHandRight
+      HandLeft: OrganSkeletonPersonHandLeft
+      LegLeft: OrganSkeletonPersonLegLeft
+      LegRight: OrganSkeletonPersonLegRight
+      FootLeft: OrganSkeletonPersonFootLeft
+      FootRight: OrganSkeletonPersonFootRight
+  - type: HumanoidProfile
     species: Skeleton
 
 - type: entity
   components:
   - type: Sprite
     sprite: Mobs/Species/Skeleton/parts.rsi
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Species/Skeleton/parts.rsi
+  - type: VisualOrganMarkings
+    markingData:
+      group: Skeleton
 
 - type: entity
   parent: [ OrganBaseTorso, OrganSkeletonPersonExternal ]
index 4d596c05583f3b23ad62ecf4b91ce0b2a410a5b7..fd238c55250bdff485815e0f0cadf1f13035a0df 100644 (file)
@@ -1,3 +1,48 @@
+- type: markingsGroup
+  parent: Undergarments
+  id: Slime
+  limits:
+    enum.HumanoidVisualLayers.Hair:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.FacialHair:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.Chest:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.LArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RArm:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LHand:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RHand:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RLeg:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LFoot:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.RFoot:
+      limit: 1
+      required: false
+  appearances:
+    enum.HumanoidVisualLayers.Hair:
+      layerAlpha: 0.72
+      matchSkin: true
+    enum.HumanoidVisualLayers.FacialHair:
+      layerAlpha: 0.72
+      matchSkin: true
+
 - type: entity
   parent: BaseSpeciesAppearance
   id: AppearanceSlimePerson
           32:
             sprite: Mobs/Species/Human/displacement.rsi
             state: jumpsuit-female
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganSlimePersonTorso
-        - id: OrganSlimePersonHead
-        - id: OrganSlimePersonArmLeft
-        - id: OrganSlimePersonArmRight
-        - id: OrganSlimePersonHandRight
-        - id: OrganSlimePersonHandLeft
-        - id: OrganSlimePersonLegLeft
-        - id: OrganSlimePersonLegRight
-        - id: OrganSlimePersonFootLeft
-        - id: OrganSlimePersonFootRight
-        - id: OrganSlimePersonCore
-        - id: OrganSlimePersonLungs
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganSlimePersonTorso
+      Head: OrganSlimePersonHead
+      ArmLeft: OrganSlimePersonArmLeft
+      ArmRight: OrganSlimePersonArmRight
+      HandRight: OrganSlimePersonHandRight
+      HandLeft: OrganSlimePersonHandLeft
+      LegLeft: OrganSlimePersonLegLeft
+      LegRight: OrganSlimePersonLegRight
+      FootLeft: OrganSlimePersonFootLeft
+      FootRight: OrganSlimePersonFootRight
+      Brain: OrganSlimePersonCore
+      Lungs: OrganSlimePersonLungs
+  - type: HumanoidProfile
     species: SlimePerson
 
 - type: entity
   components:
   - type: Sprite
     sprite: Mobs/Species/Slime/parts.rsi
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Species/Slime/parts.rsi
+  - type: VisualOrganMarkings
+    markingData:
+      group: Slime
 
 - type: entity
   parent: [ OrganBaseTorso, OrganSlimePersonExternal ]
index e58f8c314cbbcea9ea9d28e8f01b1bc18fc0f6a4..15190165e076975eba80f2a9f879dceef297472a 100644 (file)
@@ -1,3 +1,71 @@
+- type: markingsGroup
+  parent: Undergarments
+  id: Vox
+  limits:
+    enum.HumanoidVisualLayers.Hair:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.FacialHair:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.Head:
+      limit: 4
+      required: true
+    enum.HumanoidVisualLayers.Snout:
+      limit: 1
+      required: true
+      default: [ VoxBeak ]
+    enum.HumanoidVisualLayers.SnoutCover:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.LArm:
+      limit: 1
+      required: true
+      default: [ VoxLArmScales ]
+    enum.HumanoidVisualLayers.RArm:
+      limit: 1
+      required: true
+      default: [ VoxRArmScales ]
+    enum.HumanoidVisualLayers.LHand:
+      limit: 1
+      required: true
+      default: [ VoxLHandScales ]
+    enum.HumanoidVisualLayers.RHand:
+      limit: 1
+      required: true
+      default: [ VoxRHandScales ]
+    enum.HumanoidVisualLayers.LLeg:
+      limit: 1
+      required: true
+      default: [ VoxLLegScales ]
+    enum.HumanoidVisualLayers.RLeg:
+      limit: 1
+      required: true
+      default: [ VoxRLegScales ]
+    enum.HumanoidVisualLayers.LFoot:
+      limit: 1
+      required: true
+      default: [ VoxLFootScales ]
+    enum.HumanoidVisualLayers.RFoot:
+      limit: 1
+      required: true
+      default: [ VoxRFootScales ]
+    enum.HumanoidVisualLayers.Chest:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.Tail:
+      limit: 1
+      required: true
+      default: [ VoxTail ]
+    enum.HumanoidVisualLayers.UndergarmentTop:
+      limit: 1
+      required: false
+      nudityDefault: [ UndergarmentTopTanktopVox ]
+    enum.HumanoidVisualLayers.UndergarmentBottom:
+      limit: 1
+      required: false
+      nudityDefault: [ UndergarmentBottomBoxersVox ]
+
 - type: entity
   parent: BaseSpeciesAppearance
   id: AppearanceVox
         32:
           sprite: Mobs/Species/Vox/displacement.rsi
           state: hand_r
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganVoxTorso
-        - id: OrganVoxHead
-        - id: OrganVoxArmLeft
-        - id: OrganVoxArmRight
-        - id: OrganVoxHandRight
-        - id: OrganVoxHandLeft
-        - id: OrganVoxLegLeft
-        - id: OrganVoxLegRight
-        - id: OrganVoxFootLeft
-        - id: OrganVoxFootRight
-        - id: OrganVoxBrain
-        - id: OrganVoxEyes
-        - id: OrganVoxTongue
-        - id: OrganVoxAppendix
-        - id: OrganVoxEars
-        - id: OrganVoxLungs
-        - id: OrganVoxHeart
-        - id: OrganVoxStomach
-        - id: OrganVoxLiver
-        - id: OrganVoxKidneys
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganVoxTorso
+      Head: OrganVoxHead
+      ArmLeft: OrganVoxArmLeft
+      ArmRight: OrganVoxArmRight
+      HandRight: OrganVoxHandRight
+      HandLeft: OrganVoxHandLeft
+      LegLeft: OrganVoxLegLeft
+      LegRight: OrganVoxLegRight
+      FootLeft: OrganVoxFootLeft
+      FootRight: OrganVoxFootRight
+      Brain: OrganVoxBrain
+      Eyes: OrganVoxEyes
+      Tongue: OrganVoxTongue
+      Appendix: OrganVoxAppendix
+      Ears: OrganVoxEars
+      Lungs: OrganVoxLungs
+      Heart: OrganVoxHeart
+      Stomach: OrganVoxStomach
+      Liver: OrganVoxLiver
+      Kidneys: OrganVoxKidneys
+  - type: HumanoidProfile
     species: Vox
-    undergarmentTop: UndergarmentTopTanktopVox
-    undergarmentBottom: UndergarmentBottomBoxersVox
-    markingsDisplacement:
-      Hair:
-        sizeMaps:
-          32:
-            sprite: Mobs/Species/Vox/displacement.rsi
-            state: hair
 
 - type: entity
   parent:
     sprite: Mobs/Species/Vox/organs.rsi
 
 - type: entity
-  parent: OrganVox
+  id: OrganVoxVisual
+  abstract: true
+  components:
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Species/Vox/parts.rsi
+  - type: VisualOrganMarkings
+    markingData:
+      group: Vox
+
+- type: entity
+  parent: [ OrganVox, OrganVoxVisual ]
   id: OrganVoxExternal
   abstract: true
   components:
   components:
   - type: Sprite
     state: torso
+  - type: VisualOrgan
+    data:
+      state: torso
 
 - type: entity
   parent: [ OrganBaseHead, OrganVoxExternal ]
   components:
   - type: Sprite
     state: head
+  - type: VisualOrgan
+    data:
+      state: head
 
 - type: entity
   parent: [ OrganBaseArmLeft, OrganVoxExternal ]
   id: OrganVoxBrain
 
 - type: entity
-  parent: [ OrganBaseEyes, OrganVoxInternal ]
+  parent: [ OrganVoxVisual, OrganBaseEyes, OrganVoxInternal ]
   id: OrganVoxEyes
 
 - type: entity
index 6a3b657bfb523fb35a9b009f78ad12a81a25e418..2e3557c8d7e3267cde7707064d48c25f21f78d6d 100644 (file)
@@ -1,49 +1,94 @@
+- type: markingsGroup
+  parent: Undergarments
+  id: Vulpkanin
+  limits:
+    enum.HumanoidVisualLayers.Hair:
+      limit: 1
+      required: false
+    enum.HumanoidVisualLayers.FacialHair:
+      limit: 1
+      onlyGroupWhitelisted: true
+      required: false
+    enum.HumanoidVisualLayers.Head:
+      limit: 3
+      required: false
+    enum.HumanoidVisualLayers.HeadTop:
+      limit: 1
+      required: true
+      default: [ VulpEar ]
+    enum.HumanoidVisualLayers.Snout:
+      limit: 1
+      required: true
+      default: [ VulpSnout ]
+    enum.HumanoidVisualLayers.SnoutCover:
+      limit: 3
+      required: false
+    enum.HumanoidVisualLayers.Tail:
+      limit: 1
+      required: true
+      default: [ VulpTailVulp ]
+    enum.HumanoidVisualLayers.LArm:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.RArm:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.LHand:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.RHand:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.LLeg:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.RLeg:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.LFoot:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.RFoot:
+      limit: 2
+      required: false
+    enum.HumanoidVisualLayers.UndergarmentTop:
+      limit: 1
+      required: false
+      nudityDefault: [ UndergarmentTopTanktopVulpkanin ]
+    enum.HumanoidVisualLayers.UndergarmentBottom:
+      limit: 1
+      required: false
+      nudityDefault: [ UndergarmentBottomBoxersVulpkanin ]
+
 - type: entity
   parent: BaseSpeciesAppearance
   id: AppearanceVulpkanin
   name: vulpkanin appearance
   components:
-  - type: EntityTableContainerFill
-    containers:
-      body_organs: !type:AllSelector
-        children:
-        - id: OrganVulpkaninTorso
-        - id: OrganVulpkaninHead
-        - id: OrganVulpkaninArmLeft
-        - id: OrganVulpkaninArmRight
-        - id: OrganVulpkaninHandRight
-        - id: OrganVulpkaninHandLeft
-        - id: OrganVulpkaninLegLeft
-        - id: OrganVulpkaninLegRight
-        - id: OrganVulpkaninFootLeft
-        - id: OrganVulpkaninFootRight
-        - id: OrganVulpkaninBrain
-        - id: OrganVulpkaninEyes
-        - id: OrganVulpkaninTongue
-        - id: OrganVulpkaninAppendix
-        - id: OrganVulpkaninEars
-        - id: OrganVulpkaninLungs
-        - id: OrganVulpkaninHeart
-        - id: OrganVulpkaninStomach
-        - id: OrganVulpkaninLiver
-        - id: OrganVulpkaninKidneys
-  - type: HumanoidAppearance
+  - type: InitialBody
+    organs:
+      Torso: OrganVulpkaninTorso
+      Head: OrganVulpkaninHead
+      ArmLeft: OrganVulpkaninArmLeft
+      ArmRight: OrganVulpkaninArmRight
+      HandRight: OrganVulpkaninHandRight
+      HandLeft: OrganVulpkaninHandLeft
+      LegLeft: OrganVulpkaninLegLeft
+      LegRight: OrganVulpkaninLegRight
+      FootLeft: OrganVulpkaninFootLeft
+      FootRight: OrganVulpkaninFootRight
+      Brain: OrganVulpkaninBrain
+      Eyes: OrganVulpkaninEyes
+      Tongue: OrganVulpkaninTongue
+      Appendix: OrganVulpkaninAppendix
+      Ears: OrganVulpkaninEars
+      Lungs: OrganVulpkaninLungs
+      Heart: OrganVulpkaninHeart
+      Stomach: OrganVulpkaninStomach
+      Liver: OrganVulpkaninLiver
+      Kidneys: OrganVulpkaninKidneys
+  - type: HumanoidProfile
     species: Vulpkanin
-    undergarmentTop: UndergarmentTopTanktopVulpkanin
-    undergarmentBottom: UndergarmentBottomBoxersVulpkanin
-    hideLayersOnEquip:
-    - Snout
-    - SnoutCover
-    - HeadTop
-    - HeadSide
-    - FacialHair
-    - Hair
-    markingsDisplacement:
-      Hair:
-        sizeMaps:
-          32:
-            sprite: Mobs/Species/Vulpkanin/displacement.rsi
-            state: hair
   - type: Inventory
     speciesId: vulpkanin
     displacements:
     sprite: Mobs/Species/Human/organs.rsi
 
 - type: entity
-  parent: OrganVulpkanin
+  id: OrganVulpkaninVisual
+  abstract: true
+  components:
+  - type: VisualOrgan
+    data:
+      sprite: Mobs/Species/Vulpkanin/parts.rsi
+  - type: VisualOrganMarkings
+    markingData:
+      group: Vulpkanin
+
+- type: entity
+  parent: [ OrganVulpkanin, OrganVulpkaninVisual ]
   id: OrganVulpkaninExternal
   abstract: true
   components:
   id: OrganVulpkaninBrain
 
 - type: entity
-  parent: [ OrganBaseEyes, OrganVulpkaninInternal ]
+  parent: [ OrganVulpkaninVisual, OrganBaseEyes, OrganVulpkaninInternal ]
   id: OrganVulpkaninEyes
 
 - type: entity
index 2101ceffaba8ca4cb56848e6d79600bcc69e237a..1dca7f2a0ec94117728bbfdd72c710175580b070 100644 (file)
@@ -1,3 +1,19 @@
+- type: markingsGroup
+  id: None
+  onlyGroupWhitelisted: true
+
+- type: markingsGroup
+  id: Undergarments
+  limits:
+    enum.HumanoidVisualLayers.UndergarmentTop:
+      limit: 1
+      required: false
+      nudityDefault: [ UndergarmentTopTanktop ]
+    enum.HumanoidVisualLayers.UndergarmentBottom:
+      limit: 1
+      required: false
+      nudityDefault: [ UndergarmentBottomBoxers ]
+
 - type: entity
   id: OrganBase
   name: organ
     category: Torso
   - type: Sprite
     state: torso_m
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.Chest
+    data:
+      state: torso_m
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - Chest
+      - Tail
+      - Overlay
+      - UndergarmentTop
+      - UndergarmentBottom
 
 - type: entity
   parent: OrganBase
     category: Head
   - type: Sprite
     state: head_m
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.Head
+    data:
+      state: head_m
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - Head
+      - Hair
+      - FacialHair
+      - Snout
+      - SnoutCover
+      - HeadSide
+      - HeadTop
 
 - type: entity
   parent: OrganBase
     category: ArmLeft
   - type: Sprite
     state: l_arm
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.LArm
+    data:
+      state: l_arm
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - LArm
 
 - type: entity
   parent: OrganBase
     category: ArmRight
   - type: Sprite
     state: r_arm
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.RArm
+    data:
+      state: r_arm
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - RArm
 
 - type: entity
   parent: OrganBase
       location: Left
   - type: Sprite
     state: l_hand
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.LHand
+    data:
+      state: l_hand
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - LHand
 
 - type: entity
   parent: OrganBase
       location: Right
   - type: Sprite
     state: r_hand
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.RHand
+    data:
+      state: r_hand
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - RHand
 
 - type: entity
   parent: OrganBase
     category: LegLeft
   - type: Sprite
     state: l_leg
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.LLeg
+    data:
+      state: l_leg
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - LLeg
 
 - type: entity
   parent: OrganBase
     category: LegRight
   - type: Sprite
     state: r_leg
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.RLeg
+    data:
+      state: r_leg
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - RLeg
 
 - type: entity
   parent: OrganBase
     category: FootLeft
   - type: Sprite
     state: l_foot
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.LFoot
+    data:
+      state: l_foot
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - LFoot
 
 - type: entity
   parent: OrganBase
     category: FootRight
   - type: Sprite
     state: r_foot
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.RFoot
+    data:
+      state: r_foot
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - RFoot
 
 - type: entity
   parent: OrganBase
     layers:
     - state: eyeball-l
     - state: eyeball-r
+  - type: VisualOrgan
+    layer: enum.HumanoidVisualLayers.Eyes
+    data:
+      sprite: Mobs/Customization/eyes.rsi
+      state: eyes
+  - type: VisualOrganMarkings
+    markingData:
+      layers:
+      - Eyes
 
 - type: entity
   parent: OrganBase
index 91f7370f657330bb1e3303069d79d093cf16e791..ab5f83dd7c77ee04043896a03044fb0bf0901b77 100644 (file)
@@ -29,6 +29,9 @@
     - map: ["enum.HumanoidVisualLayers.LHand"]
     - map: ["enum.HumanoidVisualLayers.RHand"]
 
+    # Stuff that goes over the body but below equipment
+    - map: ["enum.HumanoidVisualLayers.Overlay"]
+
     # More equipment
     - map: [ "gloves" ]
     - map: [ "shoes" ]
   save: false
   components:
   - type: Body
+  - type: VisualBody
   - type: Hands
   - type: ComplexInteraction
   - type: ContainerContainer
   - type: Appearance
+  - type: HideableHumanoidLayers
+    hideLayersOnEquip:
+    - Snout
+    - SnoutCover
+    - HeadTop
+    - HeadSide
+    - FacialHair
+    - Hair
   - type: UserInterface
     interfaces:
       enum.HumanoidMarkingModifierKey.Key:
index 7caa300bc99dee56a8ce1ad1f40c236d5cecf2a7..bd5902f1349223bfe2bab1d8b60f227c5cec2785 100644 (file)
@@ -36,7 +36,7 @@
   - type: RotationVisuals
     defaultRotation: 90
     horizontalRotation: 90
-  - type: HumanoidAppearance
+  - type: HumanoidProfile
   - type: TypingIndicator
   - type: SlowOnDamage
     speedModifierThresholds:
index 8545dab9a6857a3cd5d5c6c8d246b27e8e10e958..bf39828627049f28729da774512a5840e4ba34ac 100644 (file)
@@ -8,10 +8,6 @@
   - type: CollideOnAnchor
   - type: Transform
     anchored: false
-  - type: AnchoredStorageFilter
-    blacklist:
-      components:
-      - HumanoidAppearance # for forks with felines
   - type: BlockAnchorOn
     blacklist:
       components:
index 316332b7b0f2f75f1db5c25608f85a340b3606b5..a27145b08f4628faa2c1a73db3dbe28dd1c23d65 100644 (file)
@@ -2,8 +2,7 @@
 - type: marking
   id: VulpBellyCrest
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: belly_crest
@@ -11,8 +10,7 @@
 - type: marking
   id: VulpBellyFull
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: belly_full
@@ -20,8 +18,7 @@
 - type: marking
   id: VulpBellyFox
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: belly_fox
\ No newline at end of file
index 12bba0c6c5789ff635909ac2e1b7200e22cfc20b..6fa06adb715bd4741e23a60e8bd942e7ce12ede8 100644 (file)
@@ -3,8 +3,7 @@
 - type: marking
   id: VulpEar
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
       state: vulp
@@ -14,8 +13,7 @@
 - type: marking
   id: VulpEarSharp
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
       state: vulp
@@ -27,8 +25,7 @@
 - type: marking
   id: VulpEarFade
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
       state: vulp
@@ -40,8 +37,7 @@
 - type: marking
   id: VulpEarJackal
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
       state: jackal
@@ -51,8 +47,7 @@
 - type: marking
   id: VulpEarTerrier
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
       state: terrier
@@ -62,8 +57,7 @@
 - type: marking
   id: VulpEarFennec
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
       state: fennec
@@ -73,8 +67,7 @@
 - type: marking
   id: VulpEarFox
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
       state: fox
@@ -84,8 +77,7 @@
 - type: marking
   id: VulpEarOtie
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
       state: otie
@@ -95,8 +87,7 @@
 - type: marking
   id: VulpEarShock
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
       state: shock
 - type: marking
   id: VulpEarCoyote
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
       state: coyote
index 880f737977ae57c89a4bd71288b9b1551bd6eedc..a5958d2e0458290fab85738c7159454ea5ac841a 100644 (file)
@@ -2,8 +2,7 @@
 - type: marking
   id: VulpHairAdhara
   bodyPart: Hair
-  speciesRestriction: [ Vulpkanin ]
-  markingCategory: Hair
+  groupWhitelist: [ Vulpkanin ]
   canBeDisplaced: false
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
@@ -12,8 +11,7 @@
 - type: marking
   id: VulpHairAnita
   bodyPart: Hair
-  speciesRestriction: [ Vulpkanin ]
-  markingCategory: Hair
+  groupWhitelist: [ Vulpkanin ]
   canBeDisplaced: false
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
@@ -22,8 +20,7 @@
 - type: marking
   id: VulpHairApollo
   bodyPart: Hair
-  speciesRestriction: [ Vulpkanin ]
-  markingCategory: Hair
+  groupWhitelist: [ Vulpkanin ]
   canBeDisplaced: false
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
@@ -32,8 +29,7 @@
 - type: marking
   id: VulpHairBelle
   bodyPart: Hair
-  speciesRestriction: [ Vulpkanin ]
-  markingCategory: Hair
+  groupWhitelist: [ Vulpkanin ]
   canBeDisplaced: false
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
@@ -42,9 +38,8 @@
 - type: marking
   id: VulpHairBraided
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: braided
@@ -52,9 +47,8 @@
 - type: marking
   id: VulpHairBun
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: bun
@@ -62,9 +56,8 @@
 - type: marking
   id: VulpHairCleanCut
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: clean_cut
@@ -72,9 +65,8 @@
 - type: marking
   id: VulpHairCurl
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: curl
@@ -82,9 +74,8 @@
 - type: marking
   id: VulpHairHawk
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: hawk
@@ -92,9 +83,8 @@
 - type: marking
   id: VulpHairJagged
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: jagged
 - type: marking
   id: VulpHairJeremy
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: jeremy
 - type: marking
   id: VulpHairKajam
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: kajam
 - type: marking
   id: VulpHairKeid
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: keid
 - type: marking
   id: VulpHairKleeia
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: kleeia
 - type: marking
   id: VulpHairMizar
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: mizar
 - type: marking
   id: VulpHairPunkBraided
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: punkbraided
 - type: marking
   id: VulpHairRaine
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: raine
 - type: marking
   id: VulpHairRough
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: rough
 - type: marking
   id: VulpHairShort
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: short
 - type: marking
   id: VulpHairShort2
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: short2
 - type: marking
   id: VulpHairSpike
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/hair.rsi
       state: spike
 - type: marking
   id: VulpFacialHairRuff
   bodyPart: FacialHair
-  markingCategory: FacialHair
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   canBeDisplaced: false
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/facial_hair.rsi
 - type: marking
   id: VulpFacialHairElder
   bodyPart: FacialHair
-  markingCategory: FacialHair
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   canBeDisplaced: false
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/facial_hair.rsi
 - type: marking
   id: VulpFacialHairElderChin
   bodyPart: FacialHair
-  markingCategory: FacialHair
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   canBeDisplaced: false
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/facial_hair.rsi
 - type: marking
   id: VulpFacialHairKita
   bodyPart: FacialHair
-  markingCategory: FacialHair
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   canBeDisplaced: false
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/facial_hair.rsi
 - type: marking
   id: VulpFacialHairGoatee
   bodyPart: FacialHair
-  markingCategory: FacialHair
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   canBeDisplaced: false
   sprites:
   - sprite: Mobs/Customization/Vulpkanin/facial_hair.rsi
index 6fefbe3e4cc05c5695b28972f56daff8063eb290..e22255463dc43e18f58636a46385f350aae461b3 100644 (file)
@@ -2,8 +2,7 @@
 - type: marking
   id: VulpHeadBlaze
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
       state: blaze
@@ -11,8 +10,7 @@
 - type: marking
   id: VulpHeadMask
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
       state: mask
@@ -20,8 +18,7 @@
 - type: marking
   id: VulpPatch
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
       state: patch
@@ -29,8 +26,7 @@
 - type: marking
   id: VulpSlash
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
       state: slash
@@ -38,8 +34,7 @@
 - type: marking
   id: VulpStripes1
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
       state: stripes_1
@@ -47,8 +42,7 @@
 - type: marking
   id: VulpStripes2
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
       state: stripes_2
@@ -56,8 +50,7 @@
 - type: marking
   id: VulpVulpine
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
       state: vulpine
index 23d7b0242f7db4c6d6ab3374ad8dd8718b9221d5..7635114536933d57d420656d0d143f7aa50a8c14 100644 (file)
@@ -1,8 +1,7 @@
 - type: marking
   id: VulpClawsHandLeft
   bodyPart: LHand
-  markingCategory: Arms
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: claws_l_hand
@@ -15,8 +14,7 @@
 - type: marking
   id: VulpClawsHandRight
   bodyPart: RHand
-  markingCategory: Arms
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: claws_r_hand
@@ -29,8 +27,7 @@
 - type: marking
   id: VulpClawsFootLeft
   bodyPart: LFoot
-  markingCategory: Legs
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: claws_l_foot
@@ -43,8 +40,7 @@
 - type: marking
   id: VulpClawsFootRight
   bodyPart: RFoot
-  markingCategory: Legs
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: claws_r_foot
@@ -59,9 +55,8 @@
 ## Left Side
 - type: marking
   id: VulpPointsCrestLegLeft
-  markingCategory: Legs
   bodyPart: LLeg
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: crest-leg-l
@@ -73,9 +68,8 @@
 
 - type: marking
   id: VulpPointsCrestArmLeft
-  markingCategory: Arms
   bodyPart: LArm
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: crest-arm-l
@@ -87,9 +81,8 @@
 
 - type: marking
   id: VulpPointsCrestFootLeft
-  markingCategory: Legs
   bodyPart: LFoot
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: crest-foot-l
 
 - type: marking
   id: VulpPointsCrestHandLeft
-  markingCategory: Arms
   bodyPart: LHand
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: crest-hand-l
 
 - type: marking
   id: VulpPointsCrestLegRight
-  markingCategory: Legs
   bodyPart: RLeg
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: crest-leg-r
 
 - type: marking
   id: VulpPointsCrestArmRight
-  markingCategory: Arms
   bodyPart: RArm
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: crest-arm-r
 
 - type: marking
   id: VulpPointsCrestFootRight
-  markingCategory: Legs
   bodyPart: RFoot
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: crest-foot-r
 
 - type: marking
   id: VulpPointsCrestHandRight
-  markingCategory: Arms
   bodyPart: RHand
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: crest-hand-r
 
 - type: marking
   id: VulpPointsFadeLegLeft
-  markingCategory: Legs
   bodyPart: LLeg
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_fade-leg-l
 
 - type: marking
   id: VulpPointsFadeArmLeft
-  markingCategory: Arms
   bodyPart: LArm
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_fade-arm-l
 
 - type: marking
   id: VulpPointsFadeFootLeft
-  markingCategory: Legs
   bodyPart: LFoot
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_fade-foot-l
 
 - type: marking
   id: VulpPointsFadeHandLeft
-  markingCategory: Arms
   bodyPart: LHand
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_fade-hand-l
 
 - type: marking
   id: VulpPointsFadeLegRight
-  markingCategory: Legs
   bodyPart: RLeg
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_fade-leg-r
 
 - type: marking
   id: VulpPointsFadeArmRight
-  markingCategory: Arms
   bodyPart: RArm
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_fade-arm-r
 
 - type: marking
   id: VulpPointsFadeFootRight
-  markingCategory: Legs
   bodyPart: RFoot
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_fade-foot-r
 
 - type: marking
   id: VulpPointsFadeHandRight
-  markingCategory: Arms
   bodyPart: RHand
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_fade-hand-r
 
 - type: marking
   id: VulpPointsSharpLegLeft
-  markingCategory: Legs
   bodyPart: LLeg
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_sharp-leg-l
 
 - type: marking
   id: VulpPointsSharpArmLeft
-  markingCategory: Arms
   bodyPart: LArm
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_sharp-arm-l
 
 - type: marking
   id: VulpPointsSharpLongArmLeft
-  markingCategory: Arms
   bodyPart: LArm
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_sharp-arm-long-l
 
 - type: marking
   id: VulpPointsSharpFootLeft
-  markingCategory: Legs
   bodyPart: LFoot
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_sharp-foot-l
 
 - type: marking
   id: VulpPointsSharpHandLeft
-  markingCategory: Arms
   bodyPart: LHand
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_sharp-hand-l
 
 - type: marking
   id: VulpPointsSharpLegRight
-  markingCategory: Legs
   bodyPart: RLeg
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_sharp-leg-r
 
 - type: marking
   id: VulpPointsSharpArmRight
-  markingCategory: Arms
   bodyPart: RArm
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_sharp-arm-r
 
 - type: marking
   id: VulpPointsSharpLongArmRight
-  markingCategory: Arms
   bodyPart: RArm
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_sharp-arm-long-r
 
 - type: marking
   id: VulpPointsSharpFootRight
-  markingCategory: Legs
   bodyPart: RFoot
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_sharp-foot-r
 
 - type: marking
   id: VulpPointsSharpHandRight
-  markingCategory: Arms
   bodyPart: RHand
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
       state: points_sharp-hand-r
index 44c494d52d2c550d0e6f88baa8ccfb265efee717..7ffca898d0ff1285e323734f0f9a17a74b1d0be1 100644 (file)
@@ -3,71 +3,63 @@
 - type: marking
   id: VulpSnout
   bodyPart: Snout
-  markingCategory: Snout
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/snout_markings.rsi
       state: snout
 
 - type: marking
   id: VulpSnoutNose
-  bodyPart: Snout
-  markingCategory: SnoutCover
-  speciesRestriction: [ Vulpkanin ]
+  bodyPart: SnoutCover
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/snout_markings.rsi
       state: snout-nose
 
 - type: marking
   id: VulpSnoutVulpine
-  bodyPart: Snout
-  markingCategory: SnoutCover
-  speciesRestriction: [ Vulpkanin ]
+  bodyPart: SnoutCover
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/snout_markings.rsi
       state: vulpine
 
 - type: marking
   id: VulpSnoutVulpineLines
-  bodyPart: Snout
-  markingCategory: SnoutCover
-  speciesRestriction: [ Vulpkanin ]
+  bodyPart: SnoutCover
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/snout_markings.rsi
       state: vulpine-lines
 
 - type: marking
   id: VulpSnoutBlaze
-  bodyPart: Snout
-  markingCategory: SnoutCover
-  speciesRestriction: [ Vulpkanin ]
+  bodyPart: SnoutCover
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/snout_markings.rsi
       state: blaze
 
 - type: marking
   id: VulpSnoutMask
-  bodyPart: Snout
-  markingCategory: SnoutCover
-  speciesRestriction: [ Vulpkanin ]
+  bodyPart: SnoutCover
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/snout_markings.rsi
       state: mask
 
 - type: marking
   id: VulpSnoutTop
-  bodyPart: Snout
-  markingCategory: SnoutCover
-  speciesRestriction: [ Vulpkanin ]
+  bodyPart: SnoutCover
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/snout_markings.rsi
       state: snout-top
 
 - type: marking
   id: VulpSnoutPatch
-  bodyPart: Snout
-  markingCategory: SnoutCover
-  speciesRestriction: [ Vulpkanin ]
+  bodyPart: SnoutCover
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/snout_markings.rsi
       state: patch
index 238bf88134f36de351e65936e0b39d385343e678..120000656a5ae014ef80e34c81d7c323131b36fa 100644 (file)
@@ -2,8 +2,7 @@
 - type: marking
   id: VulpTailFennec
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
       state: fennec
@@ -13,8 +12,7 @@
 - type: marking
   id: VulpTailFluffy
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
       state: fluffy
@@ -24,8 +22,7 @@
 - type: marking
   id: VulpTailHusky
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
       state: husky
@@ -37,8 +34,7 @@
 - type: marking
   id: VulpTailLong
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
       state: long
@@ -48,8 +44,7 @@
 - type: marking
   id: VulpTailVulp
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
       state: vulp
@@ -59,8 +54,7 @@
 - type: marking
   id: VulpTailVulpFade
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [ Vulpkanin ]
+  groupWhitelist: [ Vulpkanin ]
   sprites:
     - sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
       state: vulp
index f4c446df5b7a6af702a10c36ffe4f0373a9dd179..17e8f34c1dea1ef1acb3bc5e7f43b8349f179c9d 100644 (file)
@@ -2,8 +2,7 @@
 - type: marking
   id: ArachnidCheliceraeDownwards
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chelicerae.rsi
     state: downwards
@@ -11,8 +10,7 @@
 - type: marking
   id: ArachnidCheliceraeInwards
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chelicerae.rsi
     state: inwards
@@ -21,8 +19,7 @@
 - type: marking
   id: ArachnidAppendagesDefault
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/appendages.rsi
     state: long_primary
@@ -32,8 +29,7 @@
 - type: marking
   id: ArachnidAppendagesSharp
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/appendages.rsi
     state: sharp_primary
@@ -43,8 +39,7 @@
 - type: marking
   id: ArachnidAppendagesStingers
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/appendages.rsi
     state: stingers_primary
@@ -54,8 +49,7 @@
 - type: marking
   id: ArachnidAppendagesZigZag
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/appendages.rsi
     state: zigzag_primary
@@ -65,8 +59,7 @@
 - type: marking
   id: ArachnidAppendagesCurled
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/appendages.rsi
     state: curled_primary
@@ -76,8 +69,7 @@
 - type: marking
   id: ArachnidAppendagesChipped
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/appendages.rsi
     state: chipped_primary
@@ -87,8 +79,7 @@
 - type: marking
   id: ArachnidAppendagesHarvest
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/appendages.rsi
     state: harvest_primary
@@ -98,8 +89,7 @@
 - type: marking
   id: ArachnidAppendagesShort
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/appendages.rsi
     state: short_primary
 - type: marking
   id: ArachnidAppendagesFreaky
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/appendages.rsi
     state: freaky_primary
 - type: marking
   id: ArachnidTorsoStripes
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: stripes
 - type: marking
   id: ArachnidTorsoSlashes
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: slashes
 - type: marking
   id: ArachnidTorsoX
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: x
 - type: marking
   id: ArachnidTorsoCross
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: cross
 - type: marking
   id: ArachnidTorsoHeart
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: heart
 - type: marking
   id: ArachnidTorsoHourglass
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: hourglass
 - type: marking
   id: ArachnidTorsoNailAndHammer
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: nail-and-hammer
 - type: marking
   id: ArachnidTorsoStar
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: star
 - type: marking
   id: ArachnidTorsoArrows
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: arrows
 - type: marking
   id: ArachnidTorsoCore
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: core
 - type: marking
   id: ArachnidTorsoFiddleback
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: fiddleback
 - type: marking
   id: ArachnidTorsoSkull
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: skull
 - type: marking
   id: ArachnidTorsoTarget
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/chest.rsi
     state: target
 - type: marking
   id: ArachnidRArmStripes
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/arms.rsi
     state: stripes_right
 - type: marking
   id: ArachnidLArmStripes
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/arms.rsi
     state: stripes_left
 - type: marking
   id: ArachnidRLegStripes
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/legs.rsi
     state: stripes_right
 - type: marking
   id: ArachnidLLegStripes
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/legs.rsi
     state: stripes_left
 - type: marking
   id: ArachnidOverlayFuzzy
   bodyPart: Chest
-  markingCategory: Overlay
   forcedColoring: true
-  speciesRestriction: [Arachnid]
+  groupWhitelist: [Arachnid]
   sprites:
   - sprite: Mobs/Customization/Arachnid/overlay.rsi
     state: fuzzy
index 32ac26abc1aa28edbd50e1e1bb6c9511c09f78c5..68d94ebdcf212e4a809151dd2cea09114bf1c7f9 100644 (file)
@@ -1,8 +1,7 @@
 - type: marking
   id: CatEars
   bodyPart: Special
-  markingCategory: Special
-  speciesRestriction: [Human]
+  groupWhitelist: [Human]
   coloring:
     default:
       type:
@@ -24,8 +23,7 @@
 - type: marking
   id: CatTail
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Human]
+  groupWhitelist: [Human]
   coloring:
     default:
       type:
index 569fb5f2b8c295eedf46fd9b0433865de6d05b6c..7d37edb795643d4c11e2ff5470c5f9c56ce214d4 100644 (file)
@@ -1,8 +1,7 @@
 - type: marking
   id: DionaThornsHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
@@ -15,8 +14,7 @@
 - type: marking
   id: DionaThornsBody
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
@@ -29,8 +27,7 @@
 - type: marking
   id: DionaFlowersHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
@@ -43,8 +40,7 @@
 - type: marking
   id: DionaFlowersBody
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
@@ -57,8 +53,7 @@
 - type: marking
   id: DionaLeafCover
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
@@ -70,8 +65,7 @@
 - type: marking
   id: DionaBloomHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
@@ -84,8 +78,7 @@
 - type: marking
   id: DionaBracketHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
@@ -98,8 +91,7 @@
 - type: marking
   id: DionaBrushHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaCornflowerHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaFicusHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaGarlandHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaKingHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaLaurelHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaLeafyHeadTop
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaLotusHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaMeadowHeadTop
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaOakHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaPalmHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaRootHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaRoseHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaRoseyHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaShrubHeadTop
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaSpinnerHeadSide
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaSproutHeadSide
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaVineHeadTop
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaVinelHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaVinesHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 - type: marking
   id: DionaWildflowerHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Diona]
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
 
 - type: marking
   id: DionaVineOverlay
-  bodyPart: LLeg
-  markingCategory: Overlay
-  speciesRestriction: [Diona]
+  bodyPart: Overlay
+  groupWhitelist: [Diona]
   coloring:
     default:
       type:
index c32348b273db48473fa82d519f8be40d12f0097d..553018cf75df716c38c4d33d3e9a4e380ed1fa97 100644 (file)
@@ -1,10 +1,8 @@
 - type: marking
   id: HumanLongEars
   bodyPart: HeadTop
-  markingCategory: HeadTop
   forcedColoring: true
-  followSkinColor: true
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/ears.rsi
     state: long_ears_standard
 - type: marking
   id: LongEarsWide
   bodyPart: HeadTop
-  markingCategory: HeadTop
   forcedColoring: true
-  followSkinColor: true
-  speciesRestriction: [Human]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/ears.rsi
     state: long_ears_wide
 - type: marking
   id: LongEarsSmall
   bodyPart: HeadTop
-  markingCategory: HeadTop
   forcedColoring: true
-  followSkinColor: true
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/ears.rsi
     state: long_ears_small
 - type: marking
   id: LongEarsUpwards
   bodyPart: HeadTop
-  markingCategory: HeadTop
   forcedColoring: true
-  followSkinColor: true
-  speciesRestriction: [Human]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/ears.rsi
     state: long_ears_upwards
 - type: marking
   id: LongEarsTall
   bodyPart: HeadTop
-  markingCategory: HeadTop
   forcedColoring: true
-  followSkinColor: true
-  speciesRestriction: [Human]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/ears.rsi
     state: long_ears_tall
 - type: marking
   id: LongEarsThin
   bodyPart: HeadTop
-  markingCategory: HeadTop
   forcedColoring: true
-  followSkinColor: true
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/ears.rsi
     state: long_ears_thin
index 870a7cf7d7545bc45d552c5bab5eb39334f599eb..d2509475e971f0a2f4a40664b6a9bb7d37e3390f 100644 (file)
@@ -1,8 +1,7 @@
 - type: marking
   id: GauzeLefteyePatch
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Arachnid]
+  groupWhitelist: [Human, Arachnid]
   coloring:
     default:
       type:
@@ -15,8 +14,7 @@
 - type: marking
   id: GauzeLefteyePad
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
@@ -29,8 +27,7 @@
 - type: marking
   id: GauzeRighteyePatch
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Arachnid]
+  groupWhitelist: [Human, Arachnid]
   coloring:
     default:
       type:
@@ -43,8 +40,7 @@
 - type: marking
   id: GauzeRighteyePad
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
@@ -57,8 +53,7 @@
 - type: marking
   id: GauzeBlindfold
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Arachnid]
+  groupWhitelist: [Human, Arachnid]
   coloring:
     default:
       type:
@@ -71,8 +66,7 @@
 - type: marking
   id: GauzeShoulder
   bodyPart: Chest
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
@@ -85,8 +79,7 @@
 - type: marking
   id: GauzeStomach
   bodyPart: Chest
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
@@ -99,8 +92,7 @@
 - type: marking
   id: GauzeUpperArmRight
   bodyPart: RArm
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
 
 - type: marking
   id: GauzeLowerArmRight
-  bodyPart: RArm, RHand
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  bodyPart: RHand
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
 
 - type: marking
   id: GauzeLeftArm
-  bodyPart: LArm, LHand
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  bodyPart: LArm
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeLowerLegLeft
   bodyPart: LFoot
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Arachnid]
+  groupWhitelist: [Human, Arachnid]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeUpperLegLeft
   bodyPart: LLeg
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeUpperLegRight
   bodyPart: RLeg
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeLowerLegRight
   bodyPart: RFoot
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Arachnid]
+  groupWhitelist: [Human, Arachnid]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeBoxerWrapRight
   bodyPart: RHand
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeBoxerWrapLeft
   bodyPart: LHand
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+  groupWhitelist: [Human, Reptilian, Arachnid]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeHead
   bodyPart: Head
-  markingCategory: Overlay
-  speciesRestriction: [Dwarf, Human, Reptilian, Arachnid, Moth]
+  groupWhitelist: [Human, Reptilian, Arachnid, Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeLizardLefteyePatch
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeLizardRighteyePatch
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeLizardFootRight
   bodyPart: RFoot
-  markingCategory: Overlay
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeLizardFootLeft
   bodyPart: LFoot
-  markingCategory: Overlay
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeLizardBlindfold
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothBlindfold
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothShoulder
   bodyPart: Chest
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothStomach
   bodyPart: Chest
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothLeftEyePatch
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothLeftEyePad
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothRightEyePatch
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothRightEyePad
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothUpperArmRight
   bodyPart: RArm
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothUpperArmLeft
   bodyPart: LArm
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothUpperLegRight
   bodyPart: RLeg
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothUpperLegLeft
   bodyPart: LLeg
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothLowerLegRight
   bodyPart: RFoot
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: GauzeMothLowerLegLeft
   bodyPart: LFoot
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
index 1b4175ed540d3ad3c2b5aa22e63677fad20d2279..2f3b2eb9f91ca31620f9b389c947e92fbfb6bbb2 100644 (file)
 - type: marking
   id: HumanFacialHairAbe
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: abe
 - type: marking
   id: HumanFacialHairBrokenman
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: brokenman
 - type: marking
   id: HumanFacialHairChin
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: chin
 - type: marking
   id: HumanFacialHairDwarf
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: dwarf
 - type: marking
   id: HumanFacialHairFullbeard
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: fullbeard
 - type: marking
   id: HumanFacialHairCroppedfullbeard
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: croppedfullbeard
 - type: marking
   id: HumanFacialHairGt
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: gt
 - type: marking
   id: HumanFacialHairHip
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: hip
 - type: marking
   id: HumanFacialHairJensen
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: jensen
 - type: marking
   id: HumanFacialHairNeckbeard
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: neckbeard
 - type: marking
   id: HumanFacialHairWise
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: wise
 - type: marking
   id: HumanFacialHairMuttonmus
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: muttonmus
 - type: marking
   id: HumanFacialHairMartialartist
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: martialartist
 - type: marking
   id: HumanFacialHairChinlessbeard
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: chinlessbeard
 - type: marking
   id: HumanFacialHairMoonshiner
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: moonshiner
 - type: marking
   id: HumanFacialHairLongbeard
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: longbeard
 - type: marking
   id: HumanFacialHairVolaju
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: volaju
 - type: marking
   id: HumanFacialHair3oclock
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: 3oclock
 - type: marking
   id: HumanFacialHairFiveoclock
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: fiveoclock
 - type: marking
   id: HumanFacialHair5oclockmoustache
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: 5oclockmoustache
 - type: marking
   id: HumanFacialHair7oclock
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: 7oclock
 - type: marking
   id: HumanFacialHair7oclockmoustache
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: 7oclockmoustache
 - type: marking
   id: HumanFacialHairMoustache
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: moustache
 - type: marking
   id: HumanFacialHairPencilstache
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: pencilstache
 - type: marking
   id: HumanFacialHairSmallstache
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: smallstache
 - type: marking
   id: HumanFacialHairWalrus
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: walrus
 - type: marking
   id: HumanFacialHairFumanchu
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: fumanchu
 - type: marking
   id: HumanFacialHairHogan
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: hogan
 - type: marking
   id: HumanFacialHairSelleck
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: selleck
 - type: marking
   id: HumanFacialHairChaplin
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: chaplin
 - type: marking
   id: HumanFacialHairVandyke
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: vandyke
 - type: marking
   id: HumanFacialHairWatson
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: watson
 - type: marking
   id: HumanFacialHairElvis
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: elvis
 - type: marking
   id: HumanFacialHairMutton
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: mutton
 - type: marking
   id: HumanFacialHairSideburn
   bodyPart: FacialHair
-  markingCategory: FacialHair
   sprites:
     - sprite: Mobs/Customization/human_facial_hair.rsi
       state: sideburn
index a7748c4566373fe991424bddec27c2b0dc302306..adf2bf0bb702ab553830e671a040fb130d4e18db 100644 (file)
 - type: marking
   id: HumanHairAfro
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: afro
 - type: marking
   id: HumanHairAfro2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: afro2
 - type: marking
   id: HumanHairBaby
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: baby
 - type: marking
   id: HumanHairBigafro
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bigafro
 - type: marking
   id: HumanHairAntenna
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: antenna
 - type: marking
   id: HumanHairBalding
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: e
 - type: marking
   id: HumanHairBedhead
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bedhead
 - type: marking
   id: HumanHairBedheadv2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bedheadv2
 - type: marking
   id: HumanHairBedheadv3
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bedheadv3
 - type: marking
   id: HumanHairCube
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: cube
 - type: marking
   id: HumanHairPulato
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: pulato
 - type: marking
   id: HumanHairLongBedhead
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: long_bedhead
 - type: marking
   id: HumanHairLongBedhead2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: long_bedhead2
 - type: marking
   id: HumanHairFloorlengthBedhead
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: floorlength_bedhead
 - type: marking
   id: HumanHairBeehive
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: beehive
 - type: marking
   id: HumanHairBeehivev2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: beehivev2
 - type: marking
   id: HumanHairBob
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bob
 - type: marking
   id: HumanHairBob2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bob2
 - type: marking
   id: HumanHairBobcut
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bobcut
 - type: marking
   id: HumanHairBob4
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bob4
 - type: marking
   id: HumanHairBob5
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bob5
 - type: marking
   id: HumanHairBobcurl
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bobcurl
 - type: marking
   id: HumanHairBoddicker
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: boddicker
 - type: marking
   id: HumanHairBowlcut
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bowlcut
 - type: marking
   id: HumanHairBowlcut2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bowlcut2
 - type: marking
   id: HumanHairBraid
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: braid
 - type: marking
   id: HumanHairBraided
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: braided
 - type: marking
   id: HumanHairBraidfront
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: braidfront
 - type: marking
   id: HumanHairBraid2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: braid2
 - type: marking
   id: HumanHairHbraid
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: hbraid
 - type: marking
   id: HumanHairShortbraid
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: shortbraid
 - type: marking
   id: HumanHairBraidtail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: braidtail
 - type: marking
   id: HumanHairBun
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bun
 - type: marking
   id: HumanHairBunhead2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bunhead2
 - type: marking
   id: HumanHairBun3
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bun3
 - type: marking
   id: HumanHairLargebun
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: largebun
 - type: marking
   id: HumanHairManbun
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: manbun
 - type: marking
   id: HumanHairTightbun
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: tightbun
 - type: marking
   id: HumanHairBusiness
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: business
 - type: marking
   id: HumanHairBusiness2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: business2
 - type: marking
   id: HumanHairBusiness3
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: business3
 - type: marking
   id: HumanHairBusiness4
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: business4
 - type: marking
   id: HumanHairBuzzcut
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: buzzcut
 - type: marking
   id: HumanHairCia
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: cia
 - type: marking
   id: HumanHairClassicAfro
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classicafro
 - type: marking
   id: HumanHairClassicBigAfro
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classicbigafro
 - type: marking
   id: HumanHairClassicBusiness
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classicbusiness
 - type: marking
   id: HumanHairClassicCia
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classiccia
 - type: marking
   id: HumanHairClassicCornrows2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classiccornrows2
 - type: marking
   id: HumanHairClassicFloorlengthBedhead
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classicfloorlength_bedhead
 - type: marking
   id: HumanHairClassicModern
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classicmodern
 - type: marking
   id: HumanHairClassicMulder
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classicmulder
 - type: marking
   id: HumanHairClassicWisp
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classicwisp
 - type: marking
   id: HumanHairCoffeehouse
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: coffeehouse
 - type: marking
   id: HumanHairCombover
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: combover
 - type: marking
   id: HumanHairCornrows
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: cornrows
 - type: marking
   id: HumanHairCornrows2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: cornrows2
 - type: marking
   id: HumanHairCornrowbun
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: cornrowbun
 - type: marking
   id: HumanHairCornrowbraid
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: cornrowbraid
 - type: marking
   id: HumanHairCornrowtail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: cornrowtail
 - type: marking
   id: HumanHairCrewcut
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: crewcut
 - type: marking
   id: HumanHairCrewcut2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: crewcut2
 - type: marking
   id: HumanHairCurls
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: curls
 - type: marking
   id: HumanHairC
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: c
 - type: marking
   id: HumanHairDandypompadour
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: dandypompadour
 - type: marking
   id: HumanHairDevilock
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: devilock
 - type: marking
   id: HumanHairDoublebun
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: doublebun
 - type: marking
   id: HumanHairDoublebunLong
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
   - sprite: Mobs/Customization/human_hair.rsi
     state: doublebun_long
 - type: marking
   id: HumanHairDreads
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: dreads
 - type: marking
   id: HumanHairDrillruru
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: drillruru
 - type: marking
   id: HumanHairDrillhairextended
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: drillhairextended
 - type: marking
   id: HumanHairEmo
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: emo
 - type: marking
   id: HumanHairEmofringe
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: emofringe
 - type: marking
   id: HumanHairNofade
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: nofade
 - type: marking
   id: HumanHairHighfade
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: highfade
 - type: marking
   id: HumanHairMedfade
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: medfade
 - type: marking
   id: HumanHairLowfade
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: lowfade
 - type: marking
   id: HumanHairBaldfade
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: baldfade
 - type: marking
   id: HumanHairFeather
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: feather
 - type: marking
   id: HumanHairFather
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: father
 - type: marking
   id: HumanHairSargeant
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: sargeant
 - type: marking
   id: HumanHairFlair
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: flair
 - type: marking
   id: HumanHairBigflattop
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bigflattop
 - type: marking
   id: HumanHairFlow
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: f
 - type: marking
   id: HumanHairGelled
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: gelled
 - type: marking
   id: HumanHairGentle
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: gentle
 - type: marking
   id: HumanHairHalfbang
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: halfbang
 - type: marking
   id: HumanHairHalfbang2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: halfbang2
 - type: marking
   id: HumanHairHalfshaved
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: halfshaved
 - type: marking
   id: HumanHairHedgehog
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: hedgehog
 - type: marking
   id: HumanHairHimecut
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: himecut
 - type: marking
   id: HumanHairHimecut2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: himecut2
 - type: marking
   id: HumanHairShorthime
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: shorthime
 - type: marking
   id: HumanHairHimeup
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: himeup
 - type: marking
   id: HumanHairHitop
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: hitop
 - type: marking
   id: HumanHairJade
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: jade
 - type: marking
   id: HumanHairJensen
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: jensen
 - type: marking
   id: HumanHairJoestar
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: joestar
 - type: marking
   id: HumanHairKeanu
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: keanu
 - type: marking
   id: HumanHairKusanagi
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: kusanagi
 - type: marking
   id: HumanHairLong
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: long
 - type: marking
   id: HumanHairLong2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: long2
 - type: marking
   id: HumanHairLong3
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: long3
 - type: marking
   id: HumanHairLongWithBundles
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: longbundled
 - type: marking
   id: HumanHairLongovereye
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: longovereye
 - type: marking
   id: HumanHairLbangs
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: lbangs
 - type: marking
   id: HumanHairLongemo
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: longemo
 - type: marking
   id: HumanHairLongfringe
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: longfringe
 - type: marking
   id: HumanHairLongsidepart
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: longsidepart
 - type: marking
   id: HumanHairMegaeyebrows
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: megaeyebrows
 - type: marking
   id: HumanHairMessy
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: messy
 - type: marking
   id: HumanHairModern
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: modern
 - type: marking
   id: HumanHairMohawk
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: d
 - type: marking
   id: HumanHairNitori
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: nitori
 - type: marking
   id: HumanHairReversemohawk
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: reversemohawk
 - type: marking
   id: HumanHairUnshavenMohawk
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: unshaven_mohawk
 - type: marking
   id: HumanHairMulder
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: mulder
 - type: marking
   id: HumanHairOdango
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: odango
 - type: marking
   id: HumanHairOmbre
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: ombre
 - type: marking
   id: HumanHairOneshoulder
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: oneshoulder
 - type: marking
   id: HumanHairShortovereye
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: shortovereye
 - type: marking
   id: HumanHairOxton
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: oxton
 - type: marking
   id: HumanHairParted
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: parted
 - type: marking
   id: HumanHairPart
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: part
 - type: marking
   id: HumanHairKagami
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: kagami
 - type: marking
   id: HumanHairPigtails
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: pigtails
 - type: marking
   id: HumanHairPigtails2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: pigtails2
 - type: marking
   id: HumanHairPixie
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: pixie
 - type: marking
   id: HumanHairPompadour
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: pompadour
 - type: marking
   id: HumanHairBigpompadour
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: bigpompadour
 - type: marking
   id: HumanHairPonytail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: ponytail
 - type: marking
   id: HumanHairPonytail2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: ponytail2
 - type: marking
   id: HumanHairPonytail3
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: ponytail3
 - type: marking
   id: HumanHairPonytail4
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: ponytail4
 - type: marking
   id: HumanHairPonytail5
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: ponytail5
 - type: marking
   id: HumanHairPonytail6
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: ponytail6
 - type: marking
   id: HumanHairPonytail7
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: ponytail7
 - type: marking
   id: HumanHairHighponytail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: highponytail
 - type: marking
   id: HumanHairStail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: stail
 - type: marking
   id: HumanHairLongstraightponytail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: longstraightponytail
 - type: marking
   id: HumanHairCountry
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: country
 - type: marking
   id: HumanHairFringetail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: fringetail
 - type: marking
   id: HumanHairSidetail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: sidetail
 - type: marking
   id: HumanHairSidetail2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: sidetail2
 - type: marking
   id: HumanHairSidetail3
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: sidetail3
 - type: marking
   id: HumanHairSidetail4
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: sidetail4
 - type: marking
   id: HumanHairSpikyponytail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: spikyponytail
 - type: marking
   id: HumanHairPoofy
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: poofy
 - type: marking
   id: HumanHairQuiff
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: quiff
 - type: marking
   id: HumanHairRonin
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: ronin
 - type: marking
   id: HumanHairShaved
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: shaved
 - type: marking
   id: HumanHairShavedpart
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: shavedpart
 - type: marking
   id: HumanHairShortbangs
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: shortbangs
 - type: marking
   id: HumanHairA
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: a
 - type: marking
   id: HumanHairShorthair2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: shorthair2
 - type: marking
   id: HumanHairShorthair3
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: shorthair3
 - type: marking
   id: HumanHairD
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: d
 - type: marking
   id: HumanHairE
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: e
 - type: marking
   id: HumanHairF
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: f
 - type: marking
   id: HumanHairShorthairg
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: shorthairg
 - type: marking
   id: HumanHair80s
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: 80s
 - type: marking
   id: HumanHairRosa
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: rosa
 - type: marking
   id: HumanHairB
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: b
 - type: marking
   id: HumanHairSidecut
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: sidecut
 - type: marking
   id: HumanHairSkinhead
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: skinhead
 - type: marking
   id: HumanHairProtagonist
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: protagonist
 - type: marking
   id: HumanHairSpikey
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: spikey
 - type: marking
   id: HumanHairSpiky
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: spiky
 - type: marking
   id: HumanHairSpiky2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: spiky2
 - type: marking
   id: HumanHairSpookyLong
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: spookylong
 - type: marking
   id: HumanHairSwept
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: swept
 - type: marking
   id: HumanHairSwept2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: swept2
 - type: marking
   id: HumanHairBAlt
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: b_alt
 - type: marking
   id: HumanHairThinning
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: thinning
 - type: marking
   id: HumanHairThinningfront
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: thinningfront
 - type: marking
   id: HumanHairThinningrear
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: thinningrear
 - type: marking
   id: HumanHairTopknot
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: topknot
 - type: marking
   id: HumanHairTressshoulder
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: tressshoulder
 - type: marking
   id: HumanHairTrimmed
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: trimmed
 - type: marking
   id: HumanHairTrimflat
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: trimflat
 - type: marking
   id: HumanHairTwintail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: twintail
 - type: marking
   id: HumanHairTwoStrands
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: twostrands
 - type: marking
   id: HumanHairUndercut
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: undercut
 - type: marking
   id: HumanHairUndercutleft
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: undercutleft
 - type: marking
   id: HumanHairUndercutright
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: undercutright
 - type: marking
   id: HumanHairUnkept
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: unkept
 - type: marking
   id: HumanHairUpdo
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: updo
 - type: marking
   id: HumanHairVlong
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: vlong
 - type: marking
   id: HumanHairLongest
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: longest
 - type: marking
   id: HumanHairLongest2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: longest2
 - type: marking
   id: HumanHairVeryshortovereyealternate
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: veryshortovereyealternate
 - type: marking
   id: HumanHairVlongfringe
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: vlongfringe
 - type: marking
   id: HumanHairVolaju
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: volaju
 - type: marking
   id: HumanHairWisp
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: wisp
 - type: marking
   id: HumanHairUneven
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: uneven
 - type: marking
   id: HumanHairTailed
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: tailed
 - type: marking
   id: HumanHairClassicLong2
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classiclong2
 - type: marking
   id: HumanHairClassicLong3
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: classiclong3
 - type: marking
   id: HumanHairShaped
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: shaped
 - type: marking
   id: HumanHairLongBow
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
     - sprite: Mobs/Customization/human_hair.rsi
       state: longbow
 - type: marking
   id: HumanHairLongWithBangs
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
   - sprite: Mobs/Customization/human_hair.rsi
     state: longwithbangs
 - type: marking
   id: HumanHairOverEyePigtail
   bodyPart: Hair
-  markingCategory: Hair
   sprites:
   - sprite: Mobs/Customization/human_hair.rsi
     state: overeyepigtail
index 51fc2fd15a2f515d6d55f7ad7a05649f43e7d993..d4009a0b030ebd3974dbe5a6fe46cb9f7555120f 100644 (file)
@@ -1,10 +1,8 @@
 - type: marking
   id: HumanNoseSchnozz
   bodyPart: Snout
-  markingCategory: Snout
-  followSkinColor: true
   forcedColoring: true
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/human_noses.rsi
     state: schnozz
 - type: marking
   id: HumanNoseNubby
   bodyPart: Snout
-  markingCategory: Snout
-  followSkinColor: true
   forcedColoring: true
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/human_noses.rsi
     state: nubby
 - type: marking
   id: HumanNoseDroop
   bodyPart: Snout
-  markingCategory: Snout
-  followSkinColor: true
   forcedColoring: true
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/human_noses.rsi
     state: droop
 - type: marking
   id: HumanNoseBlob
   bodyPart: Snout
-  markingCategory: Snout
-  followSkinColor: true
   forcedColoring: true
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/human_noses.rsi
     state: blob
 - type: marking
   id: HumanNoseUppie
   bodyPart: Snout
-  markingCategory: Snout
-  followSkinColor: true
   forcedColoring: true
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/human_noses.rsi
     state: uppie
index ee4c4f67fb831b6d8c8dff4ba7bc688af06c0527..03cd4ed6e5d5f3aa66964d28f4bd3d235d8235eb 100644 (file)
@@ -2,8 +2,7 @@
 - type: marking
   id: MothAntennasDefault
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: default
@@ -11,8 +10,7 @@
 - type: marking
   id: MothAntennasCharred
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: charred
@@ -20,8 +18,7 @@
 - type: marking
   id: MothAntennasDbushy
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: dbushy
@@ -29,8 +26,7 @@
 - type: marking
   id: MothAntennasDcurvy
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: dcurvy
@@ -38,8 +34,7 @@
 - type: marking
   id: MothAntennasDfan
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: dfan
@@ -47,8 +42,7 @@
 - type: marking
   id: MothAntennasDpointy
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: dpointy
@@ -56,8 +50,7 @@
 - type: marking
   id: MothAntennasFeathery
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: feathery
@@ -65,8 +58,7 @@
 - type: marking
   id: MothAntennasFirewatch
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: firewatch
@@ -74,8 +66,7 @@
 - type: marking
   id: MothAntennasGray
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: gray
@@ -83,8 +74,7 @@
 - type: marking
   id: MothAntennasJungle
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: jungle
@@ -92,8 +82,7 @@
 - type: marking
   id: MothAntennasMoffra
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: moffra
 - type: marking
   id: MothAntennasOakworm
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: oakworm
 - type: marking
   id: MothAntennasPlasmafire
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: plasmafire
 - type: marking
   id: MothAntennasMaple
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: maple
 - type: marking
   id: MothAntennasRoyal
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: royal
 - type: marking
   id: MothAntennasStriped
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: striped
 - type: marking
   id: MothAntennasWhitefly
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: whitefly
 - type: marking
   id: MothAntennasWitchwing
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: witchwing
 - type: marking
   id: MothAntennasUnderwing
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_antennas.rsi
     state: underwing_primary
 - type: marking
   id: MothWingsDefault
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: default
 - type: marking
   id: MothWingsCharred
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: charred
 - type: marking
   id: MothWingsDbushy
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: dbushy_primary
 - type: marking
   id: MothWingsDeathhead
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: deathhead_primary
 - type: marking
   id: MothWingsFan
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: fan
 - type: marking
   id: MothWingsDfan
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: dfan
 - type: marking
   id: MothWingsFeathery
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: feathery
 - type: marking
   id: MothWingsFirewatch
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: firewatch_primary
 - type: marking
   id: MothWingsGothic
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: gothic
 - type: marking
   id: MothWingsJungle
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: jungle
 - type: marking
   id: MothWingsLadybug
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: ladybug
 - type: marking
   id: MothWingsMaple
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: maple_primary
 - type: marking
   id: MothWingsMoffra
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: moffra_primary
 - type: marking
   id: MothWingsOakworm
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: oakworm
 - type: marking
   id: MothWingsPlasmafire
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: plasmafire_primary
 - type: marking
   id: MothWingsPointy
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: pointy
 - type: marking
   id: MothWingsRoyal
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: royal_primary
 - type: marking
   id: MothWingsStellar
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: stellar
 - type: marking
   id: MothWingsStriped
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: striped
 - type: marking
   id: MothWingsSwirly
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: swirly
 - type: marking
   id: MothWingsWhitefly
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: whitefly
 - type: marking
   id: MothWingsWitchwing
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: witchwing
 - type: marking
   id: MothWingsUnderwing
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_wings.rsi
     state: underwing_primary
 - type: marking
   id: MothChestCharred
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: charred_chest
 - type: marking
   id: MothHeadCharred
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: charred_head
 - type: marking
   id: MothLLegCharred
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: charred_l_leg
 - type: marking
   id: MothRLegCharred
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: charred_r_leg
 - type: marking
   id: MothLArmCharred
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: charred_l_arm
 - type: marking
   id: MothRArmCharred
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: charred_r_arm
 - type: marking
   id: MothChestDeathhead
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: deathhead_chest
 - type: marking
   id: MothHeadDeathhead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: deathhead_head
 - type: marking
   id: MothLLegDeathhead
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: deathhead_l_leg
 - type: marking
   id: MothRLegDeathhead
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: deathhead_r_leg
 - type: marking
   id: MothLArmDeathhead
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: deathhead_l_arm
 - type: marking
   id: MothRArmDeathhead
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: deathhead_r_arm
 - type: marking
   id: MothChestFan
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: fan_chest
 - type: marking
   id: MothHeadFan
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: fan_head
 - type: marking
   id: MothLLegFan
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: fan_l_leg
 - type: marking
   id: MothRLegFan
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: fan_r_leg
 - type: marking
   id: MothLArmFan
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: fan_l_arm
 - type: marking
   id: MothRArmFan
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: fan_r_arm
 - type: marking
   id: MothChestFirewatch
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: firewatch_chest
 - type: marking
   id: MothHeadFirewatch
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: firewatch_head
 - type: marking
   id: MothLLegFirewatch
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: firewatch_l_leg
 - type: marking
   id: MothRLegFirewatch
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: firewatch_r_leg
 - type: marking
   id: MothLArmFirewatch
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: firewatch_l_arm
 - type: marking
   id: MothRArmFirewatch
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: firewatch_r_arm
 - type: marking
   id: MothChestGothic
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: gothic_chest
 - type: marking
   id: MothHeadGothic
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: gothic_head
 - type: marking
   id: MothLLegGothic
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: gothic_l_leg
 - type: marking
   id: MothRLegGothic
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: gothic_r_leg
 - type: marking
   id: MothLArmGothic
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: gothic_l_arm
 - type: marking
   id: MothRArmGothic
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: gothic_r_arm
 - type: marking
   id: MothChestJungle
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: jungle_chest
 - type: marking
   id: MothHeadJungle
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: jungle_head
 - type: marking
   id: MothLLegJungle
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: jungle_l_leg
 - type: marking
   id: MothRLegJungle
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: jungle_r_leg
 - type: marking
   id: MothLArmJungle
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: jungle_l_arm
 - type: marking
   id: MothRArmJungle
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: jungle_r_arm
 - type: marking
   id: MothChestMoonfly
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: moonfly_chest
 - type: marking
   id: MothHeadMoonfly
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: moonfly_head
 - type: marking
   id: MothLLegMoonfly
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: moonfly_l_leg
 - type: marking
   id: MothRLegMoonfly
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: moonfly_r_leg
 - type: marking
   id: MothLArmMoonfly
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: moonfly_l_arm
 - type: marking
   id: MothRArmMoonfly
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: moonfly_r_arm
 - type: marking
   id: MothChestOakworm
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: oakworm_chest
 - type: marking
   id: MothHeadOakworm
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: oakworm_head
 - type: marking
   id: MothLLegOakworm
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: oakworm_l_leg
 - type: marking
   id: MothRLegOakworm
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: oakworm_r_leg
 - type: marking
   id: MothLArmOakworm
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: oakworm_l_arm
 - type: marking
   id: MothRArmOakworm
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: oakworm_r_arm
 - type: marking
   id: MothChestPointy
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: pointy_chest
 - type: marking
   id: MothHeadPointy
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: pointy_head
 - type: marking
   id: MothLLegPointy
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: pointy_l_leg
 - type: marking
   id: MothRLegPointy
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: pointy_r_leg
 - type: marking
   id: MothLArmPointy
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: pointy_l_arm
 - type: marking
   id: MothRArmPointy
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: pointy_r_arm
 - type: marking
   id: MothChestRagged
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: ragged_chest
 - type: marking
   id: MothHeadRagged
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: ragged_head
 - type: marking
   id: MothLLegRagged
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: ragged_l_leg
 - type: marking
   id: MothRLegRagged
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: ragged_r_leg
 - type: marking
   id: MothLArmRagged
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: ragged_l_arm
 - type: marking
   id: MothRArmRagged
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: ragged_r_arm
 - type: marking
   id: MothChestRoyal
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: royal_chest
 - type: marking
   id: MothHeadRoyal
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: royal_head
 - type: marking
   id: MothLLegRoyal
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: royal_l_leg
 - type: marking
   id: MothRLegRoyal
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: royal_r_leg
 - type: marking
   id: MothLArmRoyal
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: royal_l_arm
 - type: marking
   id: MothRArmRoyal
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: royal_r_arm
 - type: marking
   id: MothChestWhitefly
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: whitefly_chest
 - type: marking
   id: MothHeadWhitefly
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: whitefly_head
 - type: marking
   id: MothLLegWhitefly
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: whitefly_l_leg
 - type: marking
   id: MothRLegWhitefly
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: whitefly_r_leg
 - type: marking
   id: MothLArmWhitefly
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: whitefly_l_arm
 - type: marking
   id: MothRArmWhitefly
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: whitefly_r_arm
 - type: marking
   id: MothChestWitchwing
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: witchwing_chest
 - type: marking
   id: MothHeadWitchwing
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: witchwing_head
 - type: marking
   id: MothLLegWitchwing
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: witchwing_l_leg
 - type: marking
   id: MothRLegWitchwing
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: witchwing_r_leg
 - type: marking
   id: MothLArmWitchwing
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: witchwing_l_arm
 - type: marking
   id: MothRArmWitchwing
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   sprites:
   - sprite: Mobs/Customization/Moth/moth_parts.rsi
     state: witchwing_r_arm
index f9bae9ef1ad7fd3bcc232081a2d181697a7f95a3..dae42db94e2125231ebc64b014574f058233c0f1 100644 (file)
@@ -1,8 +1,7 @@
 - type: marking
   id: LizardFrillsAquatic
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: frills_aquatic
@@ -10,8 +9,7 @@
 - type: marking
   id: LizardFrillsShort
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: frills_short
@@ -19,8 +17,7 @@
 - type: marking
   id: LizardFrillsSimple
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: frills_simple
@@ -28,8 +25,7 @@
 - type: marking
   id: LizardFrillsDivinity
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: frills_divinity
@@ -37,8 +33,7 @@
 - type: marking
   id: LizardFrillsBig
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: frills_big
@@ -46,8 +41,7 @@
 - type: marking
   id: LizardFrillsAxolotl
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: frills_axolotl
@@ -55,8 +49,7 @@
 - type: marking
   id: LizardFrillsHood
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: frills_hood_primary
@@ -66,8 +59,7 @@
 - type: marking
   id: LizardFrillsNeckfull
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: frills_neckfull
@@ -75,8 +67,7 @@
 - type: marking
   id: LizardHornsAngler
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_angler
@@ -84,8 +75,7 @@
 - type: marking
   id: LizardHornsCurled
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_curled
@@ -93,8 +83,7 @@
 - type: marking
   id: LizardHornsRam
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_ram
 - type: marking
   id: LizardHornsShort
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_short
 - type: marking
   id: LizardHornsSimple
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_simple
 - type: marking
   id: LizardHornsDouble
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_double
 - type: marking
   id: LizardTailSmooth
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: tail_smooth_primary
 - type: marking
   id: LizardTailLarge
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: tail_large
 - type: marking
   id: LizardTailSpikes
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: tail_spikes
 - type: marking
   id: LizardTailLTiger
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: tail_ltiger
 - type: marking
   id: LizardTailDTiger
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: tail_dtiger
 - type: marking
   id: LizardTailAquatic
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: tail_aquatic
 - type: marking
   id: LizardSnoutRound
   bodyPart: Snout
-  markingCategory: Snout
   forcedColoring: true
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: snout_round
 - type: marking
   id: LizardSnoutSharp
   bodyPart: Snout
-  markingCategory: Snout
   forcedColoring: true
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: snout_sharp
 - type: marking
   id: LizardSnoutSplotch
   bodyPart: Snout
-  markingCategory: Snout
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: snout_splotch_primary
 - type: marking
   id: LizardSnoutVisageRound
   bodyPart: Snout
-  markingCategory: Snout
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: visage_round
 - type: marking
   id: LizardSnoutVisageSharp
   bodyPart: Snout
-  markingCategory: Snout
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: visage_sharp
 - type: marking
   id: LizardChestTiger
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: body_tiger
 - type: marking
   id: LizardHeadTiger
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: head_tiger
 - type: marking
   id: LizardLArmTiger
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: l_arm_tiger
 - type: marking
   id: LizardLLegTiger
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: l_leg_tiger
 - type: marking
   id: LizardRArmTiger
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: r_arm_tiger
 - type: marking
   id: LizardRLegTiger
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: r_leg_tiger
 - type: marking
   id: LizardHornsArgali
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_argali
 - type: marking
   id: LizardHornsAyrshire
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_ayrshire
 - type: marking
   id: LizardHornsMyrsore
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_myrsore
 - type: marking
   id: LizardHornsBighorn
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_bighorn
 - type: marking
   id: LizardHornsDemonic
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_demonic
 - type: marking
   id: LizardHornsKoboldEars
   bodyPart: HeadTop
-  markingCategory: HeadTop
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_kobold_ears
 - type: marking
   id: LizardHornsFloppyKoboldEars
   bodyPart: HeadSide
-  markingCategory: HeadSide
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: horns_floppy_kobold_ears
 - type: marking
   id: LizardChestUnderbelly
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: body_underbelly
 - type: marking
   id: LizardChestBackspikes
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: body_backspikes
 - type: marking
   id: LizardChestFin
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: body_fin
 - type: marking
   id: LizardTailSmoothAnimated
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: []
+  groupWhitelist: []
   sprites:
     - sprite: Mobs/Customization/reptilian_parts.rsi
       state: tail_smooth_wagging_primary
 - type: marking
   id: LizardTailLargeAnimated
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: []
+  groupWhitelist: []
   sprites:
   - sprite: Mobs/Customization/reptilian_parts.rsi
     state: tail_large_wagging
 - type: marking
   id: LizardTailSpikesAnimated
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: []
+  groupWhitelist: []
   sprites:
     - sprite: Mobs/Customization/reptilian_parts.rsi
       state: tail_spikes_wagging
 - type: marking
   id: LizardTailLTigerAnimated
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: []
+  groupWhitelist: []
   sprites:
     - sprite: Mobs/Customization/reptilian_parts.rsi
       state: tail_ltiger_wagging
 - type: marking
   id: LizardTailDTigerAnimated
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: []
+  groupWhitelist: []
   sprites:
     - sprite: Mobs/Customization/reptilian_parts.rsi
       state: tail_dtiger_wagging
 - type: marking
   id: LizardTailAquaticAnimated
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: []
+  groupWhitelist: []
   sprites:
     - sprite: Mobs/Customization/reptilian_parts.rsi
       state: tail_aquatic_wagging
index 5dd1cd560ea327e1bb45cfdf8a6d1e2bd839fd8e..12430935ddb561ec72756d32ae28cb0a00b7403b 100644 (file)
@@ -1,9 +1,7 @@
 - type: marking
   id: ScarEyeRight
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Human, Dwarf, SlimePerson]
-  followSkinColor: true
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_eye_right
@@ -11,9 +9,7 @@
 - type: marking
   id: ScarEyeLeft
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Human, Dwarf, SlimePerson]
-  followSkinColor: true
+  groupWhitelist: [Human]
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_eye_left
 - type: marking
   id: ScarTopSurgeryShort
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Human, Dwarf, Reptilian, SlimePerson, Moth, Arachnid, Diona]
+  groupWhitelist: [Human, Reptilian, Moth, Arachnid, Diona]
   sexRestriction: [Male]
-  followSkinColor: true
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_top_surgery_short
 - type: marking
   id: ScarTopSurgeryLong
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Human, Dwarf, Reptilian, SlimePerson, Moth, Arachnid, Diona]
+  groupWhitelist: [Human, Reptilian, Moth, Arachnid, Diona]
   sexRestriction: [Male]
-  followSkinColor: true
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_top_surgery_long
@@ -43,9 +35,7 @@
 - type: marking
   id: ScarChest
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Human, Dwarf, Reptilian, SlimePerson, Moth, Arachnid, Diona]
-  followSkinColor: true
+  groupWhitelist: [Human, Reptilian, Moth, Arachnid, Diona]
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_chest
@@ -53,9 +43,7 @@
 - type: marking
   id: ScarNeck
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Human, Dwarf, Reptilian, SlimePerson]
-  followSkinColor: true
+  groupWhitelist: [Human, Reptilian]
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_neck
@@ -63,9 +51,7 @@
 - type: marking
   id: ScarChestBullets
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Human, Dwarf, Reptilian, SlimePerson, Moth, Arachnid]
-  followSkinColor: true
+  groupWhitelist: [Human, Reptilian, Moth, Arachnid]
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_chest_bullets
@@ -73,9 +59,7 @@
 - type: marking
   id: ScarStomachBullets
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Human, Dwarf, Reptilian, SlimePerson, Moth, Arachnid]
-  followSkinColor: true
+  groupWhitelist: [Human, Reptilian, Moth, Arachnid]
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_stomach_bullets
@@ -83,9 +67,7 @@
 - type: marking
   id: ScarFace1
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Human, Dwarf, Reptilian, SlimePerson, Moth]
-  followSkinColor: true
+  groupWhitelist: [Human, Reptilian, Moth]
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_face_1
@@ -93,9 +75,7 @@
 - type: marking
   id: ScarFace2
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Human, Dwarf, Reptilian, SlimePerson, Moth]
-  followSkinColor: true
+  groupWhitelist: [Human, Reptilian, Moth]
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_face_2
 - type: marking
   id: ScarEyeRightSmall
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Human, Dwarf, Reptilian, SlimePerson]
-  followSkinColor: true
+  groupWhitelist: [Human, Reptilian]
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_eye_right_small
 - type: marking
   id: ScarEyeLeftSmall
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Human, Dwarf, Reptilian, SlimePerson]
-  followSkinColor: true
+  groupWhitelist: [Human, Reptilian]
   sprites:
   - sprite: Mobs/Customization/scars.rsi
     state: scar_eye_left_small
index 092ebf2fddf1b50c71a62f1810a0bef156422ccd..af3c1762643df41a4f13eb41e317d970b228969e 100644 (file)
@@ -1,8 +1,7 @@
 - type: marking
   id: SlimeGradientLeftArm
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [SlimePerson]
+  groupWhitelist: [Slime]
   sprites:
   - sprite: Mobs/Customization/slime_parts.rsi
     state: gradient_l_arm
@@ -10,8 +9,7 @@
 - type: marking
   id: SlimeGradientRightArm
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [SlimePerson]
+  groupWhitelist: [Slime]
   sprites:
   - sprite: Mobs/Customization/slime_parts.rsi
     state: gradient_r_arm
@@ -19,8 +17,7 @@
 - type: marking
   id: SlimeGradientLeftLeg
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [SlimePerson]
+  groupWhitelist: [Slime]
   sprites:
   - sprite: Mobs/Customization/slime_parts.rsi
     state: gradient_l_leg
@@ -28,8 +25,7 @@
 - type: marking
   id: SlimeGradientRightLeg
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [SlimePerson]
+  groupWhitelist: [Slime]
   sprites:
   - sprite: Mobs/Customization/slime_parts.rsi
     state: gradient_r_leg
@@ -37,8 +33,7 @@
 - type: marking
   id: SlimeGradientLeftFoot
   bodyPart: LFoot
-  markingCategory: Legs
-  speciesRestriction: [SlimePerson]
+  groupWhitelist: [Slime]
   sprites:
   - sprite: Mobs/Customization/slime_parts.rsi
     state: gradient_l_foot
@@ -46,8 +41,7 @@
 - type: marking
   id: SlimeGradientRightFoot
   bodyPart: RFoot
-  markingCategory: Legs
-  speciesRestriction: [SlimePerson]
+  groupWhitelist: [Slime]
   sprites:
   - sprite: Mobs/Customization/slime_parts.rsi
     state: gradient_r_foot
@@ -55,8 +49,7 @@
 - type: marking
   id: SlimeGradientLeftHand
   bodyPart: LHand
-  markingCategory: Arms
-  speciesRestriction: [SlimePerson]
+  groupWhitelist: [Slime]
   sprites:
   - sprite: Mobs/Customization/slime_parts.rsi
     state: gradient_l_hand
@@ -64,8 +57,7 @@
 - type: marking
   id: SlimeGradientRightHand
   bodyPart: RHand
-  markingCategory: Arms
-  speciesRestriction: [SlimePerson]
+  groupWhitelist: [Slime]
   sprites:
   - sprite: Mobs/Customization/slime_parts.rsi
     state: gradient_r_hand
index b38d954cc8d33f984d3e2be31e718506d997d9b5..a53fa1367431786a50a6137640309d37c25c5527 100644 (file)
@@ -1,8 +1,7 @@
 - type: marking
   id: TattooHiveChest
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   coloring:
     default:
       type:
@@ -15,8 +14,7 @@
 - type: marking
   id: TattooNightlingChest
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   coloring:
     default:
       type:
@@ -29,8 +27,7 @@
 - type: marking
   id: TattooSilverburghLeftLeg
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   coloring:
     default:
       type:
@@ -43,8 +40,7 @@
 - type: marking
   id: TattooSilverburghRightLeg
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   coloring:
     default:
       type:
@@ -57,8 +53,7 @@
 - type: marking
   id: TattooCampbellLeftArm
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   coloring:
     default:
       type:
@@ -71,8 +66,7 @@
 - type: marking
   id: TattooCampbellRightArm
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   coloring:
     default:
       type:
@@ -85,8 +79,7 @@
 - type: marking
   id: TattooCampbellLeftLeg
   bodyPart: LLeg
-  markingCategory: Legs
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   coloring:
     default:
       type:
@@ -99,8 +92,7 @@
 - type: marking
   id: TattooCampbellRightLeg
   bodyPart: RLeg
-  markingCategory: Legs
-  speciesRestriction: [Human, Dwarf]
+  groupWhitelist: [Human]
   coloring:
     default:
       type:
 - type: marking
   id: TattooEyeRight
   bodyPart: Eyes
-  markingCategory: [Head]
-  speciesRestriction: [Human, SlimePerson, Reptilian, Dwarf]
+  groupWhitelist: [Human, Slime, Reptilian]
   coloring:
     default:
       type:
 - type: marking
   id: TattooEyeLeft
   bodyPart: Eyes
-  markingCategory: Head
-  speciesRestriction: [Human, SlimePerson, Reptilian, Dwarf]
+  groupWhitelist: [Human, Slime, Reptilian]
   coloring:
     default:
       type:
 - type: marking
   id: TattooEyeMothRight
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: TattooEyeMothLeft
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Moth]
+  groupWhitelist: [Moth]
   coloring:
     default:
       type:
 - type: marking
   id: TattooEyeVulpkaninRight
   bodyPart: Eyes
-  markingCategory: [Head]
-  speciesRestriction: [Vulpkanin]
+  groupWhitelist: [Vulpkanin]
   coloring:
     default:
       type:
 - type: marking
   id: TattooEyeVulpkaninLeft
   bodyPart: Eyes
-  markingCategory: Head
-  speciesRestriction: [Vulpkanin]
+  groupWhitelist: [Vulpkanin]
   coloring:
     default:
       type:
   # this is not a very big issue on humans, but is much more pronounced on vox & other nonhuman species, where the skin being more colorful can make for some truly dreadful meshing with the sprite thats definitely not desired.
   # some of these limitations could possibly be removed with better control over how the marking can be customized - possibly removing stacking, allowing recoloring & clamping higher-end colors for the eyeshadow, etc.
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Human, SlimePerson, Reptilian, Dwarf]
+  groupWhitelist: [Human, Slime, Reptilian]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/tattoos.rsi
 - type: marking
   id: TattooEyeshadowLower
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Human, SlimePerson, Reptilian, Dwarf]
+  groupWhitelist: [Human, Slime, Reptilian]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/tattoos.rsi
index f711264bcff10f6c0d76144489583c7b5c06f4d9..e1de3807c1307e557fe13594a6521ac9d673dc93 100644 (file)
@@ -4,8 +4,7 @@
 - type: marking
   id: UndergarmentBottomBoxers
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Arachnid, Diona, Human, Dwarf, Moth, SlimePerson]
+  groupWhitelist: [Arachnid, Diona, Human, Moth, Slime]
   coloring:
     default:
       type: null
@@ -17,8 +16,7 @@
 - type: marking
   id: UndergarmentBottomBriefs
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Arachnid, Diona, Human, Dwarf, Moth, SlimePerson]
+  groupWhitelist: [Arachnid, Diona, Human, Moth, Slime]
   coloring:
     default:
       type: null
@@ -30,8 +28,7 @@
 - type: marking
   id: UndergarmentBottomSatin
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Arachnid, Diona, Human, Dwarf, Moth, SlimePerson]
+  groupWhitelist: [Arachnid, Diona, Human, Moth, Slime]
   coloring:
     default:
       type: null
@@ -43,8 +40,7 @@
 - type: marking
   id: UndergarmentTopBra
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Arachnid, Diona, Human, Dwarf, Moth, Reptilian, SlimePerson]
+  groupWhitelist: [Arachnid, Diona, Human, Moth, Reptilian, Slime]
   coloring:
     default:
       type: null
@@ -56,8 +52,7 @@
 - type: marking
   id: UndergarmentTopSportsbra
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Arachnid, Diona, Human, Dwarf, Moth, Reptilian, SlimePerson]
+  groupWhitelist: [Arachnid, Diona, Human, Moth, Reptilian, Slime]
   coloring:
     default:
       type: null
@@ -69,8 +64,7 @@
 - type: marking
   id: UndergarmentTopBinder
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Arachnid, Diona, Human, Dwarf, Moth, Reptilian, SlimePerson]
+  groupWhitelist: [Arachnid, Diona, Human, Moth, Reptilian, Slime]
   coloring:
     default:
       type: null
@@ -82,8 +76,7 @@
 - type: marking
   id: UndergarmentTopTanktop
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Arachnid, Diona, Human, Dwarf, Moth, Reptilian, SlimePerson]
+  groupWhitelist: [Arachnid, Diona, Human, Moth, Reptilian, Slime]
   coloring:
     default:
       type: null
@@ -95,8 +88,7 @@
 - type: marking
   id: UndergarmentBottomBoxersVox # Voxers.
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentBottomBriefsVox
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentBottomSatinVox
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentTopBraVox
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentTopSportsbraVox
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentTopBinderVox
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentTopTanktopVox
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentBottomBoxersReptilian
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentBottomBriefsReptilian
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentBottomSatinReptilian
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Reptilian]
+  groupWhitelist: [Reptilian]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentBottomBoxersVulpkanin
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Vulpkanin]
+  groupWhitelist: [Vulpkanin]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentBottomBriefsVulpkanin
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Vulpkanin]
+  groupWhitelist: [Vulpkanin]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentBottomSatinVulpkanin
   bodyPart: UndergarmentBottom
-  markingCategory: UndergarmentBottom
-  speciesRestriction: [Vulpkanin]
+  groupWhitelist: [Vulpkanin]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentTopBraVulpkanin
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Vulpkanin]
+  groupWhitelist: [Vulpkanin]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentTopSportsbraVulpkanin
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Vulpkanin]
+  groupWhitelist: [Vulpkanin]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentTopBinderVulpkanin
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Vulpkanin]
+  groupWhitelist: [Vulpkanin]
   coloring:
     default:
       type: null
 - type: marking
   id: UndergarmentTopTanktopVulpkanin
   bodyPart: UndergarmentTop
-  markingCategory: UndergarmentTop
-  speciesRestriction: [Vulpkanin]
+  groupWhitelist: [Vulpkanin]
   coloring:
     default:
       type: null
index 67e686999a4787d3b536ed5c56bb94e0dedc8daa..6d6737628789077991977b76db4dd3aea7be5f0f 100644 (file)
@@ -1,9 +1,8 @@
 - type: marking
   id: VoxFacialHairBeard
   bodyPart: FacialHair
-  markingCategory: FacialHair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
       state: beard_s
@@ -11,9 +10,8 @@
 - type: marking
   id: VoxFacialHairColonel
   bodyPart: FacialHair
-  markingCategory: FacialHair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
       state: colonel_s
@@ -21,9 +19,8 @@
 - type: marking
   id: VoxFacialHairFu
   bodyPart: FacialHair
-  markingCategory: FacialHair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
       state: fu_s
@@ -31,9 +28,8 @@
 - type: marking
   id: VoxFacialHairMane
   bodyPart: FacialHair
-  markingCategory: FacialHair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
       state: mane_s
@@ -41,9 +37,8 @@
 - type: marking
   id: VoxFacialHairManeSmall
   bodyPart: FacialHair
-  markingCategory: FacialHair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
       state: manesmall_s
@@ -51,9 +46,8 @@
 - type: marking
   id: VoxFacialHairNeck
   bodyPart: FacialHair
-  markingCategory: FacialHair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
       state: neck_s
@@ -61,9 +55,8 @@
 - type: marking
   id: VoxFacialHairTufts
   bodyPart: FacialHair
-  markingCategory: FacialHair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_facial_hair.rsi
       state: tuft_s
index e6e5026fc08675ce7b6b7706ab9d73eb552450bc..1e0ed131e7f4b3a4af1b9a8ea994a008875e8e39 100644 (file)
@@ -1,9 +1,8 @@
 - type: marking
   id: VoxHairAfro
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: afro_s
@@ -11,9 +10,8 @@
 - type: marking
   id: VoxHairBraids
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: braid_s
@@ -21,9 +19,8 @@
 - type: marking
   id: VoxHairBushy
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: bushy_s
@@ -31,9 +28,8 @@
 - type: marking
   id: VoxHairCrestedQuills
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: crestedquills_s
@@ -41,9 +37,8 @@
 - type: marking
   id: VoxHairEmperorQuills
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: emperorquills_s
@@ -51,9 +46,8 @@
 - type: marking
   id: VoxHairFlowing
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: flowing_s
@@ -61,9 +55,8 @@
 - type: marking
   id: VoxHairHawk
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: hawk_s
@@ -71,9 +64,8 @@
 - type: marking
   id: VoxHairHedgehog
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: hedgehog_s
@@ -81,9 +73,8 @@
 - type: marking
   id: VoxHairHorns
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: horns_s
@@ -91,9 +82,8 @@
 - type: marking
   id: VoxHairKeelQuills
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: keelquills_s
 - type: marking
   id: VoxHairKeetQuills
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: keetquills_s
 - type: marking
   id: VoxHairKingly
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: kingly_s
 - type: marking
   id: VoxHairLongBraid
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: long_braid_s
 - type: marking
   id: VoxHairMadScientist
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: mad_scientist_s
 - type: marking
   id: VoxHairMange
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: mange_s
 - type: marking
   id: VoxHairMohawk
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: mohawk_s
 - type: marking
   id: VoxHairNights
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: nights_s
 - type: marking
   id: VoxHairPony
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: ponytail_s
 - type: marking
   id: VoxHairRazorClipped
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: razor_clipped_s
 - type: marking
   id: VoxHairRazor
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: razor_s
 - type: marking
   id: VoxHairSortBraid
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: short_braid_s
 - type: marking
   id: VoxHairShortQuills
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: shortquills_s
 - type: marking
   id: VoxHairSlick
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: slick_s
 - type: marking
   id: VoxHairSpotty
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: spotty_s
 - type: marking
   id: VoxHairSurf
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: surf_s
 - type: marking
   id: VoxHairTielQuills
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: tielquills_s
 - type: marking
   id: VoxHairWiseBraid
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: wise_braid_s
 - type: marking
   id: VoxHairYasu
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: yasu_s
 - type: marking
   id: VoxHairCatfish
   bodyPart: Hair
-  markingCategory: Hair
   canBeDisplaced: false
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
     - sprite: Mobs/Customization/vox_hair.rsi
       state: catfish_s
index ccf1a687b8776481951509fd46e43b8af892a445..2aaf87b5c673a18d1047f64ec6f0c19b8b5a90a2 100644 (file)
@@ -1,9 +1,8 @@
 - type: marking
   id: VoxBeak
   bodyPart: Snout
-  markingCategory: Snout
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: beak
@@ -17,9 +16,8 @@
   # The cere is the base of the top part of the beak, the cere on this beak, is a square.
   id: VoxBeakSquareCere
   bodyPart: Snout
-  markingCategory: Snout
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: beak_squarecere
@@ -32,9 +30,8 @@
 - type: marking
   id: VoxBeakShaved
   bodyPart: Snout
-  markingCategory: Snout
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: beak_shaved
@@ -47,9 +44,8 @@
 - type: marking
   id: VoxBeakHooked
   bodyPart: Snout
-  markingCategory: Snout
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: beak_hooked
@@ -62,9 +58,8 @@
 - type: marking
   id: VoxLArmScales
   bodyPart: LArm
-  markingCategory: Arms
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: l_arm
@@ -77,9 +72,8 @@
 - type: marking
   id: VoxLLegScales
   bodyPart: LLeg
-  markingCategory: Legs
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: l_leg
@@ -92,9 +86,8 @@
 - type: marking
   id: VoxRArmScales
   bodyPart: RArm
-  markingCategory: Arms
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: r_arm
 - type: marking
   id: VoxRLegScales
   bodyPart: RLeg
-  markingCategory: Legs
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: r_leg
 - type: marking
   id: VoxRHandScales
   bodyPart: RHand
-  markingCategory: Arms
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: r_hand
 - type: marking
   id: VoxLHandScales
   bodyPart: LHand
-  markingCategory: Arms
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: l_hand
 - type: marking
   id: VoxLFootScales
   bodyPart: LFoot
-  markingCategory: Legs
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: l_foot
 - type: marking
   id: VoxRFootScales
   bodyPart: RFoot
-  markingCategory: Legs
   forcedColoring: true
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
     state: r_foot
 - type: marking
   id: VoxTail
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
 - type: marking
   id: VoxTailShort
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
 - type: marking
   id: VoxTailBig
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
 - type: marking
   id: VoxTailSpikes
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
 - type: marking
   id: VoxTailDocked
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
 - type: marking
   id: VoxTailSplit
   bodyPart: Tail
-  markingCategory: Tail
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/vox_parts.rsi
index 5998ddc237602aa60807ff10f68878d0c65563c4..ce48e869bbee33228dbe42978c6a859922daf163 100644 (file)
@@ -1,8 +1,7 @@
 - type: marking
   id: VoxScarEyeRight
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_scar_eye_right
@@ -10,9 +9,7 @@
 - type: marking
   id: VoxScarEyeLeft
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_scar_eye_left
@@ -20,9 +17,7 @@
 - type: marking
   id: VoxScarTopSurgeryShort
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_top_surgery_short
@@ -30,9 +25,7 @@
 - type: marking
   id: VoxScarTopSurgeryLong
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_top_surgery_long
@@ -40,9 +33,7 @@
 - type: marking
   id: VoxScarChest
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_scar_chest
@@ -50,9 +41,7 @@
 - type: marking
   id: VoxScarNeck
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_scar_neck
@@ -60,9 +49,7 @@
 - type: marking
   id: VoxScarChestBullets
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_scar_chest_bullets
@@ -70,9 +57,7 @@
 - type: marking
   id: VoxScarStomachBullets
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_scar_stomach_bullets
@@ -80,9 +65,7 @@
 - type: marking
   id: VoxScarFace1
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_scar_face_1
@@ -90,9 +73,7 @@
 - type: marking
   id: VoxScarFace2
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_scar_face_2
 - type: marking
   id: VoxScarEyeRightSmall
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_scar_eye_right_small
 - type: marking
   id: VoxScarEyeLeftSmall
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
-  followSkinColor: true
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_scars.rsi
     state: vox_scar_eye_left_small
\ No newline at end of file
index 75d2503528399c33a1f8473270236cebaf1cc6a2..c10880d281a1e22fc8d702b77af58c0f2940f103 100644 (file)
@@ -1,8 +1,7 @@
 - type: marking
   id: TattooVoxHeartLeftArm
   bodyPart: LArm
-  markingCategory: Arms
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
@@ -15,8 +14,7 @@
 - type: marking
   id: TattooVoxHeartRightArm
   bodyPart: RArm
-  markingCategory: Arms
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
@@ -29,8 +27,7 @@
 - type: marking
   id: TattooVoxHiveChest
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
@@ -43,8 +40,7 @@
 - type: marking
   id: TattooVoxNightlingChest
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
@@ -57,8 +53,7 @@
 - type: marking
   id: TattooVoxNightbelt
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
@@ -71,8 +66,7 @@
 - type: marking
   id: TattooVoxChestV
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
@@ -87,8 +81,7 @@
 - type: marking
   id: TattooVoxUnderbelly
   bodyPart: Chest
-  markingCategory: Chest
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
   id: TattooVoxTailRing
   # TODO // Looks off on some tails (i.e docked/amputated), if conditionals for markings ever get implemented this needs to be updated to account for those.
   bodyPart: Tail
-  markingCategory: Overlay
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
 - type: marking
   id: TattooEyeVoxRight
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
 - type: marking
   id: TattooEyeVoxLeft
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
   # this is not a very big issue on humans, but is much more pronounced on vox & other nonhuman species, where the skin being more colorful can make for some truly dreadful meshing with the sprite thats definitely not desired.
   # some of these limitations could possibly be removed with better control over how the marking can be customized - possibly removing stacking, allowing recoloring & clamping higher-end colors for the eyeshadow, etc.
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/vox_tattoos.rsi
 - type: marking
   id: TattooEyeshadowVoxMedium
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/vox_tattoos.rsi
 - type: marking
   id: TattooEyeshadowVoxLarge
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   forcedColoring: true
   sprites:
   - sprite: Mobs/Customization/vox_tattoos.rsi
 - type: marking
   id: VoxTattooEyeliner
   bodyPart: Eyes
-  markingCategory: Overlay
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_tattoos.rsi
     state: eyeliner
 - type: marking
   id: VoxBeakCoverStripe
   bodyPart: Snout
-  markingCategory: SnoutCover
   coloring:
     default:
       type:
         !type:TattooColoring
       fallbackColor: "#666666"
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_tattoos.rsi
     state: beakcover_stripe
 - type: marking
   id: VoxBeakCoverTip
   bodyPart: Snout
-  markingCategory: SnoutCover
   coloring:
     default:
       type:
         !type:TattooColoring
       fallbackColor: "#666666"
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   sprites:
   - sprite: Mobs/Customization/vox_tattoos.rsi
     state: beakcover_tip
 - type: marking
   id: TattooVoxArrowHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
 - type: marking
   id: TattooVoxNightlingHead
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
 - type: marking
   id: VoxVisage
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
 - type: marking
   id: VoxVisageL
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
 - type: marking
   id: VoxVisageR
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
 - type: marking
   id: VoxCheek
   bodyPart: Head
-  markingCategory: Head
-  speciesRestriction: [Vox]
+  groupWhitelist: [Vox]
   coloring:
     default:
       type:
index d6727c7d88b33ff14a4897bd3cd03023c36ddbe4..e49e9fe3220887e72786873d9004a32263d2f4cf 100644 (file)
   - type: NPCImprintingOnSpawnBehaviour
     whitelist:
       components:
-      - HumanoidAppearance
+      - HumanoidProfile
   - type: Sprite
     sprite: Mobs/Demons/tomatokiller.rsi
     noRot: true
index 419bf233256dce0d89bbee7341accd5ea0f927a4..6e22f479d90fac90fdbd9a163651c974fe663362 100644 (file)
@@ -81,7 +81,7 @@
   blacklist:
     components:
     - AttachedClothing # helmets, which are part of the suit
-    - HumanoidAppearance # will cause problems for downstream felinids getting cloned as Urists
+    - HumanoidProfile # will cause problems for downstream felinids getting cloned as Urists
     - Implanter # they will spawn full again, but you already get the implant. And we can't do item slot copying yet
     - VirtualItem
 
index 5aa7f05f0ea64fbc1cf02adeb86ec13f7ba03312..28c18d6a69454f4b5acdd4cd891606ad57546f08 100644 (file)
   - type: Devourer
     foodPreferenceWhitelist:
       components:
-      - HumanoidAppearance
+      - HumanoidProfile
     stomachStorageWhitelist:
       components:
       - MobState
index e99d7b75247774fbe28ee13379e96037acecf420..87849b8d0b14ca9c81ad17de7761bf1fa27c8745 100644 (file)
@@ -7,7 +7,6 @@
   components:
   - type: RandomHumanoidAppearance
     randomizeName: false
-    hair: HairBald
   - type: Loadout
     prototypes: [SyndicateOperativeGearExtremelyBasic]
     roleLoadout: [ RoleSurvivalSyndicate ]
index c6f545f21fc33074076248fa5544d170b6b94903..c40e710d80e4d6d7f9d0cb748d2e8954bd8290f6 100644 (file)
     implantAction: ActionActivateDnaScramblerImplant
     whitelist:
       components:
-      - HumanoidAppearance # syndies cant turn hamlet into a human
+      - HumanoidProfile # syndies cant turn hamlet into a human
   - type: TriggerOnActivateImplant
   - type: DnaScrambleOnTrigger
     targetUser: true
index bda59013787d02f296ed20233f8692f6b3560bff..c9362ae1595ed4fd222c81e0f2f0d76a3ab07faa 100644 (file)
     mechToPilotDamageMultiplier: 0.75
     pilotWhitelist:
       components:
-        - HumanoidAppearance
+        - HumanoidProfile
   - type: MeleeWeapon
     hidden: true
     attackRate: 1
     mechToPilotDamageMultiplier: 0.5
     pilotWhitelist:
       components:
-      - HumanoidAppearance
+      - HumanoidProfile
 
 - type: entity
   parent: MechHonker
index ccd42776e3f3aaede769eba2e1f5741f562b4fed..70b99499488a83355de4b4390d85b5b398bc3d87 100644 (file)
       state: storage
       sprite: Objects/Tools/scissors.rsi
   - type: MagicMirror
+    organs:
+    - Head
+    layers:
+    - Hair
+    - FacialHair
   - type: ActivatableUI
     key: enum.MagicMirrorUiKey.Key
     inHandsOnly: true
index 9a9bbd9a7dd4db5ea5c4b23179f804b4046d173c..79fc6f4ad9cb941347a1d390bf540a3c30bb4b01 100644 (file)
   - type: Strap
     whitelist:
       components:
-      - HumanoidAppearance
+      - HumanoidProfile
   - type: DisposalUnit
     blacklist:
       components:
-      - HumanoidAppearance
+      - HumanoidProfile
       - Plunger
       - SolutionTransfer
       - EntityStorage
index c32a2731cdb9e6105d17b4ec5e33bfcfc606b7ab..a07661c4a069ad4183bd14be8d66b020b147b651 100644 (file)
@@ -9,12 +9,14 @@
   - type: Sprite
     sprite: Structures/Wallmounts/mirror.rsi
     state: mirror
-  - type: MagicMirror #instant and silent
+  - type: MagicMirror
+    organs:
+    - Head
+    layers:
+    - Hair
+    - FacialHair
     changeHairSound: null
-    addSlotTime: 0
-    removeSlotTime: 0
-    selectSlotTime: 0
-    changeSlotTime: 0
+    modifyTime: 0
   - type: ActivatableUI
     key: enum.MagicMirrorUiKey.Key
     singleUser: true
index 1a56b22b9213ec56b5298a5b5bf8ff5f6e9878f3..409b2b03e40ea5de6aefbfaee02a832ab0c0a995 100644 (file)
   query:
   - !type:ComponentQuery
     components:
-    - type: HumanoidAppearance
+    - type: HumanoidProfile
       species: Human # This specific value isn't actually used, so don't worry about it being just `Human`.
   - !type:ComponentFilter
     retainWithComp: false
index ad576b8b60b84cfd4b3715254d9a0b2d9cb2c1d7..10d838d01df7ebfa1e7282d4e472ee27087355bd 100644 (file)
@@ -3,9 +3,7 @@
   name: species-name-arachnid
   roundStart: true
   prototype: MobArachnid
-  sprites: MobArachnidSprites
   defaultSkinTone: "#385878"
-  markingLimits: MobArachnidMarkingLimits
   dollPrototype: AppearanceArachnid
   skinColoration: Hues
   maleFirstNames: NamesArachnidFirst
   lastNames: NamesArachnidLast
   sexes:
   - Unsexed
-
-- type: speciesBaseSprites
-  id: MobArachnidSprites
-  sprites:
-    Head: MobArachnidHead
-    Snout: MobHumanoidAnyMarking
-    UndergarmentTop: MobHumanoidAnyMarking
-    UndergarmentBottom: MobHumanoidAnyMarking
-    Chest: MobArachnidTorso
-    HeadTop: MobHumanoidAnyMarking
-    HeadSide: MobHumanoidAnyMarking
-    Tail: MobHumanoidAnyMarking
-    Eyes: MobArachnidEyes
-    LArm: MobArachnidLArm
-    RArm: MobArachnidRArm
-    LHand: MobArachnidLHand
-    RHand: MobArachnidRHand
-    LLeg: MobArachnidLLeg
-    RLeg: MobArachnidRLeg
-    LFoot: MobArachnidLFoot
-    RFoot: MobArachnidRFoot
-
-- type: humanoidBaseSprite
-  id: MobArachnidEyes
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: eyes
-
-- type: markingPoints
-  id: MobArachnidMarkingLimits
-  onlyWhitelisted: true
-  points:
-    Hair:
-      points: 0
-      required: false
-    FacialHair:
-      points: 0
-      required: false
-    Tail:
-      points: 1
-      required: true
-      defaultMarkings: [ ArachnidAppendagesDefault ]
-    HeadTop:
-      points: 1
-      required: false
-    HeadSide:
-      points: 1
-      required: true
-      defaultMarkings: [ ArachnidCheliceraeDownwards ]
-    Chest:
-      points: 2
-      required: false
-    Legs:
-      points: 2
-      required: false
-    Arms:
-      points: 2
-      required: false
-
-- type: humanoidBaseSprite
-  id: MobArachnidHead
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobArachnidHeadMale
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobArachnidHeadFemale
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: head_f
-
-- type: humanoidBaseSprite
-  id: MobArachnidTorso
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobArachnidTorsoMale
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobArachnidTorsoFemale
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: torso_f
-
-- type: humanoidBaseSprite
-  id: MobArachnidLLeg
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: l_leg
-
-- type: humanoidBaseSprite
-  id: MobArachnidLHand
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: l_hand
-
-- type: humanoidBaseSprite
-  id: MobArachnidLArm
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: l_arm
-
-- type: humanoidBaseSprite
-  id: MobArachnidLFoot
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: l_foot
-
-- type: humanoidBaseSprite
-  id: MobArachnidRLeg
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: r_leg
-
-- type: humanoidBaseSprite
-  id: MobArachnidRHand
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: r_hand
-
-- type: humanoidBaseSprite
-  id: MobArachnidRArm
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: r_arm
-
-- type: humanoidBaseSprite
-  id: MobArachnidRFoot
-  baseSprite:
-    sprite: Mobs/Species/Arachnid/parts.rsi
-    state: r_foot
index e08d476ee384777f9f65bfbe805a8df978e85e65..6dfd358720687d4c8cf063ef0c9d82d317104a05 100644 (file)
   name: species-name-diona
   roundStart: true
   prototype: MobDiona
-  sprites: MobDionaSprites
   defaultSkinTone: "#cdb369"
-  markingLimits: MobDionaMarkingLimits
   dollPrototype: AppearanceDiona
   skinColoration: Hues
   maleFirstNames: NamesDionaFirst
   femaleFirstNames: NamesDionaFirst
   lastNames: NamesDionaLast
   naming: TheFirstofLast
-
-- type: speciesBaseSprites
-  id: MobDionaSprites
-  sprites:
-    Head: MobDionaHead
-    HeadTop: MobHumanoidAnyMarking
-    HeadSide: MobHumanoidAnyMarking
-    UndergarmentTop: MobHumanoidAnyMarking
-    UndergarmentBottom: MobHumanoidAnyMarking
-    Chest: MobDionaTorso
-    Eyes: MobDionaEyes
-    LArm: MobDionaLArm
-    RArm: MobDionaRArm
-    LHand: MobDionaLHand
-    RHand: MobDionaRHand
-    LLeg: MobDionaLLeg
-    RLeg: MobDionaRLeg
-    LFoot: MobDionaLFoot
-    RFoot: MobDionaRFoot
-
-- type: markingPoints
-  id: MobDionaMarkingLimits
-  onlyWhitelisted: true
-  points:
-    Head:
-      points: 2
-      required: false
-    HeadTop:
-      points: 1
-      required: false
-    HeadSide:
-      points: 1
-      required: false
-    UndergarmentTop:
-      points: 1
-      required: false
-    UndergarmentBottom:
-      points: 1
-      required: false
-    Chest:
-      points: 2
-      required: false
-    Legs:
-      points: 2
-      required: false
-    Arms:
-      points: 2
-      required: false
-    Overlay:
-      points: 1
-      required: true
-      defaultMarkings: [ DionaVineOverlay ]
-
-- type: humanoidBaseSprite
-  id: MobDionaEyes
-  baseSprite:
-    sprite: Mobs/Customization/eyes.rsi
-    state: diona
-
-- type: humanoidBaseSprite
-  id: MobDionaHead
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobDionaHeadMale
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobDionaHeadFemale
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: head_f
-
-- type: humanoidBaseSprite
-  id: MobDionaTorso
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobDionaTorsoMale
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobDionaTorsoFemale
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: torso_f
-
-- type: humanoidBaseSprite
-  id: MobDionaLLeg
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: l_leg
-
-- type: humanoidBaseSprite
-  id: MobDionaLHand
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: l_hand
-
-- type: humanoidBaseSprite
-  id: MobDionaLArm
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: l_arm
-
-- type: humanoidBaseSprite
-  id: MobDionaLFoot
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: l_foot
-
-- type: humanoidBaseSprite
-  id: MobDionaRLeg
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: r_leg
-
-- type: humanoidBaseSprite
-  id: MobDionaRHand
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: r_hand
-
-- type: humanoidBaseSprite
-  id: MobDionaRArm
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: r_arm
-
-- type: humanoidBaseSprite
-  id: MobDionaRFoot
-  baseSprite:
-    sprite: Mobs/Species/Diona/parts.rsi
-    state: r_foot
index 8d1d41d89211d8bcf8e01743f228be6c1b37ac2c..0c2fbaf8b63c4cf7d8f140d30fced1cc149fa9c5 100644 (file)
@@ -3,7 +3,5 @@
   name: species-name-dwarf
   roundStart: true
   prototype: MobDwarf
-  sprites: MobHumanSprites
-  markingLimits: MobHumanMarkingLimits
   dollPrototype: AppearanceDwarf
   skinColoration: HumanToned
index e8f9fc0754614912d1e3c85ea2aa51f759892bf0..1823d5c9de53dcc3ca10f90b515b24b45512f00f 100644 (file)
@@ -3,115 +3,6 @@
   name: species-name-gingerbread
   roundStart: false
   prototype: MobGingerbread
-  sprites: MobGingerbreadSprites
-  markingLimits: MobHumanMarkingLimits
   dollPrototype: AppearanceGingerbread
   skinColoration: HumanToned
   defaultSkinTone: "#9a7c5a"
-
-- type: speciesBaseSprites
-  id: MobGingerbreadSprites
-  sprites:
-    Head: MobGingerbreadHead
-    HeadTop: MobHumanoidAnyMarking
-    HeadSide: MobHumanoidAnyMarking
-    Chest: MobGingerbreadTorso
-    Eyes: MobGingerbreadEyes
-    LArm: MobGingerbreadLArm
-    RArm: MobGingerbreadRArm
-    LHand: MobGingerbreadLHand
-    RHand: MobGingerbreadRHand
-    LLeg: MobGingerbreadLLeg
-    RLeg: MobGingerbreadRLeg
-    LFoot: MobGingerbreadLFoot
-    RFoot: MobGingerbreadRFoot
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadEyes
-  baseSprite:
-    sprite: Mobs/Customization/eyes.rsi
-    state: no_eyes
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadHead
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadHeadMale
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadHeadFemale
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: head_f
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadTorso
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadTorsoMale
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadTorsoFemale
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: torso_f
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadLLeg
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: l_leg
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadLHand
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: l_hand
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadLArm
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: l_arm
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadLFoot
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: l_foot
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadRLeg
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: r_leg
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadRHand
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: r_hand
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadRArm
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: r_arm
-
-- type: humanoidBaseSprite
-  id: MobGingerbreadRFoot
-  baseSprite:
-    sprite: Mobs/Species/Gingerbread/parts.rsi
-    state: r_foot
index 249e5d5a7413e8b2309f7a4fc95a2f981aaebe3d..24c18c00deabab49cb88c55ad816067cc77f0e5f 100644 (file)
@@ -3,169 +3,5 @@
   name: species-name-human
   roundStart: true
   prototype: MobHuman
-  sprites: MobHumanSprites
-  markingLimits: MobHumanMarkingLimits
   dollPrototype: AppearanceHuman
   skinColoration: HumanToned
-
-# The lack of a layer means that
-# this person cannot have round-start anything
-# applied to that layer. It has to instead
-# be defined as a 'custom base layer'
-# in either the mob's starting marking prototype,
-# or it has to be added in C#.
-- type: speciesBaseSprites
-  id: MobHumanSprites
-  sprites:
-    Special: MobHumanoidAnyMarking
-    Head: MobHumanHead
-    Hair: MobHumanoidAnyMarking
-    FacialHair: MobHumanoidAnyMarking
-    Snout: MobHumanoidAnyMarking
-    UndergarmentTop: MobHumanoidAnyMarking
-    UndergarmentBottom: MobHumanoidAnyMarking
-    Chest: MobHumanTorso
-    Eyes: MobHumanoidEyes
-    HeadTop: MobHumanoidAnyMarking
-    LArm: MobHumanLArm
-    RArm: MobHumanRArm
-    LHand: MobHumanLHand
-    RHand: MobHumanRHand
-    LLeg: MobHumanLLeg
-    RLeg: MobHumanRLeg
-    LFoot: MobHumanLFoot
-    RFoot: MobHumanRFoot
-
-- type: markingPoints
-  id: MobHumanMarkingLimits
-  points:
-    Special: # the cat ear joke
-      points: 0
-      required: false
-    Hair:
-      points: 1
-      required: false
-    FacialHair:
-      points: 1
-      required: false
-    Snout:
-      points: 1
-      required: false
-    Tail: # the cat tail joke
-      points: 0
-      required: false
-    HeadTop:
-      points: 1
-      required: false
-    UndergarmentTop:
-      points: 1
-      required: false
-    UndergarmentBottom:
-      points: 1
-      required: false
-    Chest:
-      points: 2
-      required: false
-    Legs:
-      points: 2
-      required: false
-    Arms:
-      points: 2
-      required: false
-
-- type: humanoidBaseSprite
-  id: MobHumanoidEyes
-  baseSprite:
-    sprite: Mobs/Customization/eyes.rsi
-    state: eyes
-
-- type: humanoidBaseSprite
-  id: MobHumanoidAnyMarking
-
-- type: humanoidBaseSprite
-  id: MobHumanoidMarkingMatchSkin
-  markingsMatchSkin: true
-
-- type: humanoidBaseSprite
-  id: MobHumanHead
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobHumanHeadMale
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobHumanHeadFemale
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: head_f
-
-- type: humanoidBaseSprite
-  id: MobHumanTorso
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobHumanTorsoMale
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobHumanTorsoFemale
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: torso_f
-
-- type: humanoidBaseSprite
-  id: MobHumanLLeg
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: l_leg
-
-- type: humanoidBaseSprite
-  id: MobHumanLArm
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: l_arm
-
-- type: humanoidBaseSprite
-  id: MobHumanLHand
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: l_hand
-
-- type: humanoidBaseSprite
-  id: MobHumanLFoot
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: l_foot
-
-- type: humanoidBaseSprite
-  id: MobHumanRLeg
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: r_leg
-
-- type: humanoidBaseSprite
-  id: MobHumanRArm
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: r_arm
-
-- type: humanoidBaseSprite
-  id: MobHumanRHand
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: r_hand
-
-- type: humanoidBaseSprite
-  id: MobHumanRFoot
-  baseSprite:
-    sprite: Mobs/Species/Human/parts.rsi
-    state: r_foot
index aacb6fe57129329ff7813222dd5880785c329c9d..0e6fcd244a0ac8b392de0ed5211e8ab7faec726e 100644 (file)
@@ -3,165 +3,9 @@
   name: species-name-moth
   roundStart: true
   prototype: MobMoth
-  sprites: MobMothSprites
   defaultSkinTone: "#ffda93"
-  markingLimits: MobMothMarkingLimits
   dollPrototype: AppearanceMoth
   skinColoration: Hues
   maleFirstNames: NamesMothFirstMale
   femaleFirstNames: NamesMothFirstFemale
   lastNames: NamesMothLast
-
-- type: speciesBaseSprites
-  id: MobMothSprites
-  sprites:
-    Head: MobMothHead
-    Snout: MobHumanoidAnyMarking
-    UndergarmentTop: MobHumanoidAnyMarking
-    UndergarmentBottom: MobHumanoidAnyMarking
-    Chest: MobMothTorso
-    HeadTop: MobHumanoidAnyMarking
-    HeadSide: MobHumanoidAnyMarking
-    Tail: MobHumanoidAnyMarking
-    Eyes: MobMothEyes
-    LArm: MobMothLArm
-    RArm: MobMothRArm
-    LHand: MobMothLHand
-    RHand: MobMothRHand
-    LLeg: MobMothLLeg
-    RLeg: MobMothRLeg
-    LFoot: MobMothLFoot
-    RFoot: MobMothRFoot
-
-- type: humanoidBaseSprite
-  id: MobMothEyes
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: eyes
-
-- type: markingPoints
-  id: MobMothMarkingLimits
-  onlyWhitelisted: true
-  points:
-    Hair:
-      points: 0
-      required: false
-    FacialHair:
-      points: 0
-      required: false
-    Tail:
-      points: 1
-      required: true
-      defaultMarkings: [ MothWingsDefault ]
-    Snout:
-      points: 1
-      required: false
-    HeadTop:
-      points: 1
-      required: true
-      defaultMarkings: [ MothAntennasDefault ]
-    HeadSide:
-      points: 1
-      required: false
-    Head:
-      points: 1
-      required: false
-    UndergarmentTop:
-      points: 1
-      required: false
-    UndergarmentBottom:
-      points: 1
-      required: false
-    Chest:
-      points: 2
-      required: false
-    Legs:
-      points: 2
-      required: false
-    Arms:
-      points: 2
-      required: false
-
-- type: humanoidBaseSprite
-  id: MobMothHead
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobMothHeadMale
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobMothHeadFemale
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: head_f
-
-- type: humanoidBaseSprite
-  id: MobMothTorso
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobMothTorsoMale
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobMothTorsoFemale
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: torso_f
-
-- type: humanoidBaseSprite
-  id: MobMothLLeg
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: l_leg
-
-- type: humanoidBaseSprite
-  id: MobMothLHand
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: l_hand
-
-- type: humanoidBaseSprite
-  id: MobMothLArm
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: l_arm
-
-- type: humanoidBaseSprite
-  id: MobMothLFoot
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: l_foot
-
-- type: humanoidBaseSprite
-  id: MobMothRLeg
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: r_leg
-
-- type: humanoidBaseSprite
-  id: MobMothRHand
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: r_hand
-
-- type: humanoidBaseSprite
-  id: MobMothRArm
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: r_arm
-
-- type: humanoidBaseSprite
-  id: MobMothRFoot
-  baseSprite:
-    sprite: Mobs/Species/Moth/parts.rsi
-    state: r_foot
index cbcc2715a4fddaedb632b1cdaa8de91742c133a4..d8491a6aec9deb71f13676268bbc9903430a63f6 100644 (file)
@@ -3,156 +3,9 @@
   name: species-name-reptilian
   roundStart: true
   prototype: MobReptilian
-  sprites: MobReptilianSprites
   defaultSkinTone: "#34a223"
-  markingLimits: MobReptilianMarkingLimits
   dollPrototype: AppearanceReptilian
   skinColoration: Hues
   maleFirstNames: NamesReptilianMale
   femaleFirstNames: NamesReptilianFemale
   naming: FirstDashFirst
-
-- type: speciesBaseSprites
-  id: MobReptilianSprites
-  sprites:
-    Head: MobReptilianHead
-    Snout: MobHumanoidAnyMarking
-    UndergarmentTop: MobHumanoidAnyMarking
-    UndergarmentBottom: MobHumanoidAnyMarking
-    Chest: MobReptilianTorso
-    HeadTop: MobHumanoidAnyMarking
-    HeadSide: MobHumanoidAnyMarking
-    Tail: MobHumanoidAnyMarking
-    Eyes: MobHumanoidEyes
-    LArm: MobReptilianLArm
-    RArm: MobReptilianRArm
-    LHand: MobReptilianLHand
-    RHand: MobReptilianRHand
-    LLeg: MobReptilianLLeg
-    RLeg: MobReptilianRLeg
-    LFoot: MobReptilianLFoot
-    RFoot: MobReptilianRFoot
-
-- type: markingPoints
-  id: MobReptilianMarkingLimits
-  onlyWhitelisted: true
-  points:
-    Hair:
-      points: 0
-      required: false
-    FacialHair:
-      points: 0
-      required: false
-    Tail:
-      points: 1
-      required: true
-      defaultMarkings: [ LizardTailSmooth ]
-    Snout:
-      points: 1
-      required: true
-      defaultMarkings: [ LizardSnoutRound ]
-    HeadTop:
-      points: 2
-      required: false
-    HeadSide:
-      points: 1
-      required: false
-    UndergarmentTop:
-      points: 1
-      required: false
-    UndergarmentBottom:
-      points: 1
-      required: false
-    Chest:
-      points: 3
-      required: false
-    Legs:
-      points: 2
-      required: false
-    Arms:
-      points: 2
-      required: false
-
-- type: humanoidBaseSprite
-  id: MobReptilianHead
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobReptilianHeadMale
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobReptilianHeadFemale
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: head_f
-
-- type: humanoidBaseSprite
-  id: MobReptilianTorso
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobReptilianTorsoMale
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobReptilianTorsoFemale
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: torso_f
-
-- type: humanoidBaseSprite
-  id: MobReptilianLLeg
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: l_leg
-
-- type: humanoidBaseSprite
-  id: MobReptilianLHand
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: l_hand
-
-- type: humanoidBaseSprite
-  id: MobReptilianLArm
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: l_arm
-
-- type: humanoidBaseSprite
-  id: MobReptilianLFoot
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: l_foot
-
-- type: humanoidBaseSprite
-  id: MobReptilianRLeg
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: r_leg
-
-- type: humanoidBaseSprite
-  id: MobReptilianRHand
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: r_hand
-
-- type: humanoidBaseSprite
-  id: MobReptilianRArm
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: r_arm
-
-- type: humanoidBaseSprite
-  id: MobReptilianRFoot
-  baseSprite:
-    sprite: Mobs/Species/Reptilian/parts.rsi
-    state: r_foot
index e04f44da5ec2185cccabd27b421f1baf0cdce1c4..b018d33d7ee7b37da7a5950e7086c09717508af5 100644 (file)
@@ -3,108 +3,8 @@
   name: species-name-skeleton
   roundStart: false
   prototype: MobSkeletonPerson
-  sprites: MobSkeletonSprites
   defaultSkinTone: "#fff9e2"
-  markingLimits: MobHumanMarkingLimits
   maleFirstNames: NamesSkeletonFirst
   femaleFirstNames: NamesSkeletonFirst
   dollPrototype: AppearanceSkeletonPerson
   skinColoration: TintedHues
-
-- type: speciesBaseSprites
-  id: MobSkeletonSprites
-  sprites:
-    Head: MobSkeletonHead
-    Chest: MobSkeletonTorso
-    LArm: MobSkeletonLArm
-    RArm: MobSkeletonRArm
-    LHand: MobSkeletonLHand
-    RHand: MobSkeletonRHand
-    LLeg: MobSkeletonLLeg
-    RLeg: MobSkeletonRLeg
-    LFoot: MobSkeletonLFoot
-    RFoot: MobSkeletonRFoot
-
-- type: humanoidBaseSprite
-  id: MobSkeletonHead
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobSkeletonHeadMale
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobSkeletonHeadFemale
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: head_f
-
-- type: humanoidBaseSprite
-  id: MobSkeletonTorso
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobSkeletonTorsoMale
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobSkeletonTorsoFemale
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: torso_f
-
-- type: humanoidBaseSprite
-  id: MobSkeletonLLeg
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: l_leg
-
-- type: humanoidBaseSprite
-  id: MobSkeletonLArm
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: l_arm
-
-- type: humanoidBaseSprite
-  id: MobSkeletonLHand
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: l_hand
-
-- type: humanoidBaseSprite
-  id: MobSkeletonLFoot
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: l_foot
-
-- type: humanoidBaseSprite
-  id: MobSkeletonRLeg
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: r_leg
-
-- type: humanoidBaseSprite
-  id: MobSkeletonRArm
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: r_arm
-
-- type: humanoidBaseSprite
-  id: MobSkeletonRHand
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: r_hand
-
-- type: humanoidBaseSprite
-  id: MobSkeletonRFoot
-  baseSprite:
-    sprite: Mobs/Species/Skeleton/parts.rsi
-    state: r_foot
index 50961b619c9044670855f9f798d635d33ce4e729..5c3624731ebad548f1e2041a5db9cd20bc24ecc4 100644 (file)
@@ -3,135 +3,6 @@
   name: species-name-slime
   roundStart: true
   prototype: MobSlimePerson
-  sprites: MobSlimeSprites
   defaultSkinTone: "#b8b8b8"
-  markingLimits: MobSlimeMarkingLimits
   dollPrototype: AppearanceSlimePerson
   skinColoration: Hues
-
-- type: speciesBaseSprites
-  id: MobSlimeSprites
-  sprites:
-    Head: MobSlimeHead
-    Hair: MobSlimeMarkingFollowSkin
-    FacialHair: MobSlimeMarkingFollowSkin
-    UndergarmentTop: MobHumanoidAnyMarking
-    UndergarmentBottom: MobHumanoidAnyMarking
-    Chest: MobSlimeTorso
-    Eyes: MobHumanoidEyes
-    LArm: MobSlimeLArm
-    RArm: MobSlimeRArm
-    LHand: MobSlimeLHand
-    RHand: MobSlimeRHand
-    LLeg: MobSlimeLLeg
-    RLeg: MobSlimeRLeg
-    LFoot: MobSlimeLFoot
-    RFoot: MobSlimeRFoot
-
-- type: markingPoints
-  id: MobSlimeMarkingLimits
-  points:
-    Hair:
-      points: 1
-      required: false
-    FacialHair:
-      points: 1
-      required: false
-    Chest:
-      points: 2
-      required: false
-    Legs:
-      points: 4
-      required: false
-    Arms:
-      points: 4
-      required: false
-
-- type: humanoidBaseSprite
-  id: MobSlimeMarkingFollowSkin
-  markingsMatchSkin: true
-  layerAlpha: 0.72
-
-- type: humanoidBaseSprite
-  id: MobSlimeHead
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobSlimeHeadMale
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobSlimeHeadFemale
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: head_f
-
-- type: humanoidBaseSprite
-  id: MobSlimeTorso
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobSlimeTorsoMale
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobSlimeTorsoFemale
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: torso_f
-
-- type: humanoidBaseSprite
-  id: MobSlimeLLeg
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: l_leg
-
-- type: humanoidBaseSprite
-  id: MobSlimeLArm
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: l_arm
-
-- type: humanoidBaseSprite
-  id: MobSlimeLHand
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: l_hand
-
-- type: humanoidBaseSprite
-  id: MobSlimeLFoot
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: l_foot
-
-- type: humanoidBaseSprite
-  id: MobSlimeRLeg
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: r_leg
-
-- type: humanoidBaseSprite
-  id: MobSlimeRArm
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: r_arm
-
-- type: humanoidBaseSprite
-  id: MobSlimeRHand
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: r_hand
-
-- type: humanoidBaseSprite
-  id: MobSlimeRFoot
-  baseSprite:
-    sprite: Mobs/Species/Slime/parts.rsi
-    state: r_foot
index 22ed5c0461eb903d1f86e272d6fafd7bbc28fc96..3a543caca15fc15d1be30b0e1253636f85683466 100644 (file)
@@ -3,8 +3,6 @@
   name: species-name-vox
   roundStart: true
   prototype: MobVox
-  sprites: MobVoxSprites
-  markingLimits: MobVoxMarkingLimits
   dollPrototype: AppearanceVox
   skinColoration: VoxFeathers
   defaultSkinTone: "#6c741d"
   naming: First
   sexes:
   - Unsexed
-
-- type: speciesBaseSprites
-  id: MobVoxSprites
-  sprites:
-    Head: MobVoxHead
-    Snout: MobHumanoidAnyMarking
-    Hair: MobHumanoidAnyMarking
-    FacialHair: MobHumanoidAnyMarking
-    UndergarmentTop: MobHumanoidAnyMarking
-    UndergarmentBottom: MobHumanoidAnyMarking
-    Chest: MobVoxTorso
-    Eyes: MobVoxEyes
-    LArm: MobVoxLArm
-    RArm: MobVoxRArm
-    LHand: MobVoxLHand
-    RHand: MobVoxRHand
-    LLeg: MobVoxLLeg
-    RLeg: MobVoxRLeg
-    LFoot: MobVoxLFoot
-    RFoot: MobVoxRFoot
-    Tail: MobHumanoidAnyMarking
-
-- type: markingPoints
-  id: MobVoxMarkingLimits
-  onlyWhitelisted: true
-  points:
-    Hair:
-      points: 1
-      required: false
-    FacialHair:
-      points: 1
-      required: false
-    Head:
-      points: 4
-      required: true
-    Snout:
-      points: 1
-      required: true
-      defaultMarkings: [ VoxBeak ]
-    SnoutCover:
-      points: 1
-      required: false
-    Arms:
-      points: 4
-      required: true
-      defaultMarkings: [ VoxLArmScales, VoxRArmScales, VoxRHandScales, VoxLHandScales ]
-    Legs:
-      points: 4
-      required: true
-      defaultMarkings: [ VoxLLegScales, VoxRLegScales, VoxRFootScales, VoxLFootScales ]
-    UndergarmentTop:
-      points: 1
-      required: false
-    UndergarmentBottom:
-      points: 1
-      required: false
-    Chest:
-      points: 2
-      required: false
-    Tail:
-      points: 1
-      required: true
-      defaultMarkings: [ VoxTail ]
-
-- type: humanoidBaseSprite
-  id: MobVoxEyes
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: eyes
-
-- type: humanoidBaseSprite
-  id: MobVoxHead
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: head
-
-- type: humanoidBaseSprite
-  id: MobVoxHeadMale
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: head
-
-- type: humanoidBaseSprite
-  id: MobVoxHeadFemale
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: head
-
-- type: humanoidBaseSprite
-  id: MobVoxTorso
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: torso
-
-- type: humanoidBaseSprite
-  id: MobVoxTorsoMale
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: torso
-
-- type: humanoidBaseSprite
-  id: MobVoxTorsoFemale
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: torso
-
-- type: humanoidBaseSprite
-  id: MobVoxLLeg
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: l_leg
-
-- type: humanoidBaseSprite
-  id: MobVoxLArm
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: l_arm
-
-- type: humanoidBaseSprite
-  id: MobVoxLHand
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: l_hand
-
-- type: humanoidBaseSprite
-  id: MobVoxLFoot
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: l_foot
-
-- type: humanoidBaseSprite
-  id: MobVoxRLeg
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: r_leg
-
-- type: humanoidBaseSprite
-  id: MobVoxRArm
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: r_arm
-
-- type: humanoidBaseSprite
-  id: MobVoxRHand
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: r_hand
-
-- type: humanoidBaseSprite
-  id: MobVoxRFoot
-  baseSprite:
-    sprite: Mobs/Species/Vox/parts.rsi
-    state: r_foot
index f57278ab5ae5c27151aaed8d4226c5fcc2bdef52..5e660bea46fec72ecfd89264d25254c5f6a24059 100644 (file)
@@ -3,166 +3,9 @@
   name: species-name-vulpkanin
   roundStart: true
   prototype: MobVulpkanin
-  sprites: MobVulpkaninSprites
   defaultSkinTone: "#5a3f2d"
-  markingLimits: MobVulpkaninMarkingLimits
   dollPrototype: AppearanceVulpkanin
   skinColoration: VulpkaninColors
   maleFirstNames: names_vulpkanin_male
   femaleFirstNames: names_vulpkanin_female
   lastNames: names_vulpkanin_last
-
-- type: speciesBaseSprites
-  id: MobVulpkaninSprites
-  sprites:
-    Head: MobVulpkaninHead
-    Hair: MobHumanoidAnyMarking
-    FacialHair: MobHumanoidAnyMarking
-    Snout: MobHumanoidAnyMarking
-    SnoutCover: MobHumanoidAnyMarking
-    UndergarmentTop: MobHumanoidAnyMarking
-    UndergarmentBottom: MobHumanoidAnyMarking
-    Chest: MobVulpkaninTorso
-    HeadTop: MobHumanoidAnyMarking
-    HeadSide: MobHumanoidAnyMarking
-    Tail: MobHumanoidAnyMarking
-    Eyes: MobVulpkaninEyes
-    LArm: MobVulpkaninLArm
-    RArm: MobVulpkaninRArm
-    LHand: MobVulpkaninLHand
-    RHand: MobVulpkaninRHand
-    LLeg: MobVulpkaninLLeg
-    RLeg: MobVulpkaninRLeg
-    LFoot: MobVulpkaninLFoot
-    RFoot: MobVulpkaninRFoot
-
-- type: markingPoints # 6 points on arms and legs due to the "expected" marking usage. Two for hands, two for arms and 2 for claws. Can be lower once we have a distinction between LeftArm and RightArm instead of just Arms.
-  id: MobVulpkaninMarkingLimits
-  points:
-    Hair:
-      points: 1
-      required: false
-    FacialHair:
-      points: 1
-      onlyWhitelisted: true # Beards lack displacement maps and are impossible to displace onto a snout.
-      required: false
-    Snout:
-      points: 1
-      required: true
-      defaultMarkings: [ VulpSnout ]
-    SnoutCover:
-      points: 3
-      required: false
-    Tail:
-      points: 1
-      required: true
-      defaultMarkings: [ VulpTailVulp ]
-    Head:
-      points: 3
-      required: false
-    HeadTop:
-      points: 1
-      required: true
-      defaultMarkings: [ VulpEar ]
-    UndergarmentTop:
-      points: 1
-      required: false
-    UndergarmentBottom:
-      points: 1
-      required: false
-    Arms:
-      points: 6
-      required: false
-    Legs:
-      points: 6
-      required: false
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninEyes
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: eyes
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninHead
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninHeadMale
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: head_m
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninHeadFemale
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: head_f
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninTorso
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninTorsoMale
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: torso_m
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninTorsoFemale
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: torso_f
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninLLeg
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: l_leg
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninLHand
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: l_hand
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninLArm
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: l_arm
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninLFoot
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: l_foot
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninRLeg
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: r_leg
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninRHand
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: r_hand
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninRArm
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: r_arm
-
-- type: humanoidBaseSprite
-  id: MobVulpkaninRFoot
-  baseSprite:
-    sprite: Mobs/Species/Vulpkanin/parts.rsi
-    state: r_foot
diff --git a/Resources/Textures/Interface/palette.svg b/Resources/Textures/Interface/palette.svg
new file mode 100644 (file)
index 0000000..d0617ff
--- /dev/null
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   width="24"
+   height="24"
+   viewBox="0 0 24 24"
+   fill="none"
+   stroke="currentColor"
+   stroke-width="2"
+   stroke-linecap="round"
+   stroke-linejoin="round"
+   class="lucide lucide-palette-icon lucide-palette"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="palette.svg"
+   inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="namedview4"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:zoom="29.208333"
+     inkscape:cx="4.2796006"
+     inkscape:cy="12.736091"
+     inkscape:window-width="1920"
+     inkscape:window-height="1021"
+     inkscape:window-x="0"
+     inkscape:window-y="31"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4" />
+  <path
+     d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z"
+     id="path1"
+     style="stroke:#ffffff;stroke-opacity:1" />
+  <circle
+     cx="13.5"
+     cy="6.5"
+     r=".5"
+     fill="currentColor"
+     id="circle1"
+     style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-opacity:1" />
+  <circle
+     cx="17.5"
+     cy="10.5"
+     r=".5"
+     fill="currentColor"
+     id="circle2"
+     style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-opacity:1" />
+  <circle
+     cx="6.5"
+     cy="12.5"
+     r=".5"
+     fill="currentColor"
+     id="circle3"
+     style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-opacity:1" />
+  <circle
+     cx="8.5"
+     cy="7.5"
+     r=".5"
+     fill="currentColor"
+     id="circle4"
+     style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-opacity:1" />
+</svg>
diff --git a/Resources/Textures/Interface/palette.svg.png b/Resources/Textures/Interface/palette.svg.png
new file mode 100644 (file)
index 0000000..bd32280
Binary files /dev/null and b/Resources/Textures/Interface/palette.svg.png differ