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);
}
--- /dev/null
+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);
+ }
+ }
+ }
+ }
+}
// 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)
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
--- /dev/null
+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);
+ }
+}
+++ /dev/null
-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));
- }
- }
- }
-}
using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Markings;
using Robust.Client.UserInterface;
namespace Content.Client.Humanoid;
[ViewVariables]
private HumanoidMarkingModifierWindow? _window;
+ private readonly MarkingsViewModel _markingsModel = new();
+
public HumanoidMarkingModifierBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
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)
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));
}
}
<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>
[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;
- }
}
}
--- /dev/null
+<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>
--- /dev/null
+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);
+ }
+}
--- /dev/null
+<BoxContainer xmlns="https://spacestation14.io" Orientation="Vertical">
+ <BoxContainer Name="Items" Margin="4" Orientation="Vertical" />
+</BoxContainer>
--- /dev/null
+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);
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+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);
+ }
+}
<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>
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;
}
}
--- /dev/null
+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,
+}
--- /dev/null
+<Control xmlns="https://spacestation14.io">
+ <TabContainer Name="LayerTabs" />
+</Control>
--- /dev/null
+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;
+ }
+}
+++ /dev/null
-<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>
+++ /dev/null
-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}");
- }
-}
_onBeginDrag = onBeginDrag;
_onEndDrag = onEndDrag;
_onContinueDrag = onContinueDrag;
- _cfg.OnValueChanged(CCVars.DragDropDeadZone, SetDeadZone, true);
}
/// <summary>
Dragged = target;
_state = DragState.MouseDown;
_mouseDownScreenPos = _inputManager.MouseScreenPosition;
+ _deadzone = _cfg.GetCVar(CCVars.DragDropDeadZone);
}
/// <summary>
/// </summary>
public void EndDrag()
{
- Dragged = default;
_state = DragState.NotDragging;
_onEndDrag.Invoke();
+ Dragged = default;
}
private void StartDragging()
}
}
}
-
- private void SetDeadZone(float value)
- {
- _deadzone = value;
- }
}
/// <summary>
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;
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;
[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!;
}
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);
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
{
<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>
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;
private ISawmill _sawmill;
+ private MarkingsViewModel _markingsModel = new();
+
public HumanoidProfileEditor(
IClientPreferencesManager preferencesManager,
IConfigurationManager configurationManager,
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);
_allowFlavorText = _cfgManager.GetCVar(CCVars.FlavorText);
+ Markings.SetModel(_markingsModel);
+
ImportButton.OnPressed += args =>
{
ImportProfile();
{
SpeciesButton.SelectId(args.Id);
SetSpecies(_species[args.Id].ID);
- UpdateHairPickers();
OnSkinColorOnValueChanged();
};
#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>())
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithEyeColor(newColor));
- Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
+ _markingsModel.SetOrganEyeColor(Profile.Appearance.EyeColor);
ReloadProfilePreview();
};
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
{
if (!speciesIds.Contains(Profile.Species))
{
- SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies);
+ SetSpecies(HumanoidCharacterProfile.DefaultSpecies);
}
}
}
UpdateEyePickers();
UpdateSaveButton();
UpdateMarkings();
- UpdateHairPickers();
- UpdateCMarkingsHair();
- UpdateCMarkingsFacialHair();
RefreshAntags();
RefreshJobs();
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();
// 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.
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()
var color = strategy.FromUnary(Skin.Value);
- Markings.CurrentSkinColor = color;
+ _markingsModel.SetOrganSkinColor(color);
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
break;
var color = strategy.ClosestSkinColor(_rgbSkinColorSelector.Color);
- Markings.CurrentSkinColor = color;
+ _markingsModel.SetOrganSkinColor(color);
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
break;
}
UpdateGenderControls();
- Markings.SetSex(newSex);
+ _markingsModel.SetOrganSexes(newSex);
ReloadPreview();
}
{
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
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()
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)
return;
}
- Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
+ _markingsModel.SetOrganEyeColor(Profile.Appearance.EyeColor);
EyeColorPicker.SetData(Profile.Appearance.EyeColor);
}
try
{
- var profile = _entManager.System<HumanoidAppearanceSystem>().FromStream(file, _playerManager.LocalSession!);
+ var profile = HumanoidCharacterProfile.FromStream(file, _playerManager.LocalSession!);
var oldProfile = Profile;
SetProfile(profile, CharacterSlot);
try
{
- var dataNode = _entManager.System<HumanoidAppearanceSystem>().ToDataNode(Profile);
+ var dataNode = Profile.ToDataNode();
await using var writer = new StreamWriter(file.Value.fileStream);
dataNode.Write(writer);
}
-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;
[ViewVariables]
private MagicMirrorWindow? _window;
+ private readonly MarkingsViewModel _markingsModel = new();
+
public MagicMirrorBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
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;
}
}
+++ /dev/null
-using Content.Shared.MagicMirror;
-
-namespace Content.Client.MagicMirror;
-
-public sealed class MagicMirrorSystem : SharedMagicMirrorSystem
-{
-
-}
<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>
-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;
[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") });
- }
}
}
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),
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";
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;
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))
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));
--- /dev/null
+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();
+ }
+}
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
Species = "Human",
Age = 21,
Appearance = new(
- "Afro",
- Color.Aqua,
- "Shaved",
- Color.Aquamarine,
Color.Azure,
Color.Beige,
new ())
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]
--- /dev/null
+// <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
+ }
+ }
+}
--- /dev/null
+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");
+ }
+ }
+}
// <auto-generated />
using System;
+using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using Content.Server.Database;
{
#pragma warning disable 612, 618
modelBuilder
- .HasAnnotation("ProductVersion", "9.0.1")
+ .HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
.HasColumnType("text")
.HasColumnName("admin_ooc_color");
- b.PrimitiveCollection<string[]>("ConstructionFavorites")
+ b.PrimitiveCollection<List<string>>("ConstructionFavorites")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("construction_favorites");
.HasColumnType("jsonb")
.HasColumnName("markings");
+ b.Property<JsonDocument>("OrganMarkings")
+ .HasColumnType("jsonb")
+ .HasColumnName("organ_markings");
+
b.Property<int>("PreferenceId")
.HasColumnType("integer")
.HasColumnName("preference_id");
--- /dev/null
+// <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
+ }
+ }
+}
--- /dev/null
+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");
+ }
+ }
+}
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 =>
{
.HasColumnType("jsonb")
.HasColumnName("markings");
+ b.Property<byte[]>("OrganMarkings")
+ .HasColumnType("jsonb")
+ .HasColumnName("organ_markings");
+
b.Property<int>("PreferenceId")
.HasColumnType("INTEGER")
.HasColumnName("preference_id");
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!;
.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)
};
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);
}
}
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)
--- /dev/null
+using Content.Shared.Body;
+
+namespace Content.Server.Body;
+
+public sealed partial class VisualBodySystem : SharedVisualBodySystem;
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;
/// </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!;
[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.
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))
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);
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);
--- /dev/null
+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))
+ };
+ }
+}
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;
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
{
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;
}
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);
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));
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>();
gender,
new HumanoidCharacterAppearance
(
- profile.HairName,
- Color.FromHex(profile.HairColor),
- profile.FacialHairName,
- Color.FromHex(profile.FacialHairColor),
Color.FromHex(profile.EyeColor),
Color.FromHex(profile.SkinColor),
markings
);
}
- 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;
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;
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;
void Shutdown();
+ Task<bool> HasPendingModelChanges();
+
#region Preferences
Task<PlayerPreferences> InitPrefsAsync(
NetUserId userId,
[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!;
{
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}.");
}
}
+ 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).
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
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);
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
bool inMemory,
IConfigurationManager cfg,
bool synchronous,
- ISawmill opsLog)
- : base(opsLog)
+ ISawmill opsLog,
+ ITaskManager taskManager,
+ ISerializationManager serialization)
+ : base(opsLog, taskManager, serialization)
{
_options = options;
}));
// 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)
}
speciesId = roundStart.Count == 0
- ? SharedHumanoidAppearanceSystem.DefaultSpecies
+ ? HumanoidCharacterProfile.DefaultSpecies
: _robustRandom.Pick(roundStart);
}
else
}
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)
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;
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()
{
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
}
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);
+ }
}
}
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))
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");
{
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)
}
}
- 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))
{
-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;
}
--- /dev/null
+using Content.Shared.Humanoid;
+
+namespace Content.Server.Humanoid;
+
+public sealed class HideableHumanoidLayersSystem : SharedHideableHumanoidLayersSystem;
+++ /dev/null
-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
- ));
- }
-
- }
-}
+++ /dev/null
-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);
- }
-}
-using Content.Server.CharacterAppearance.Components;
+using Content.Server.Humanoid.Components;
+using Content.Shared.Body;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
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()
{
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);
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;
/// </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()
_metaData.SetEntityName(humanoid, prototype.RandomizeName ? profile.Name : name);
- _humanoid.LoadProfile(humanoid, profile);
-
if (prototype.Components != null)
{
foreach (var entry in prototype.Components.Values)
EntityManager.InitializeAndStartEntity(humanoid);
+ _visualBody.ApplyProfileTo(humanoid, profile);
+ _humanoidProfile.ApplyProfileTo(humanoid, profile);
+
return humanoid;
}
}
+++ /dev/null
-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;
- }
-}
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);
// 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 _))
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>();
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;
}
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;
[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!;
if (configuration.TransferHumanoidAppearance)
{
- _humanoid.CloneAppearance(uid, child);
+ _visualBody.CopyAppearanceFrom(uid, child);
}
if (_mindSystem.TryGetMind(uid, out var mindId, out var mind))
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;
}
// 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))
{
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;
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;
[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!;
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}");
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))
{
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))
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;
}
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;
[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);
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;
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()
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;
}
}
[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)
+using System.Linq;
using Content.Server.Administration.Managers;
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
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;
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;
[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!;
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
_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;
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;
using Content.Shared.Body.Events;
using Content.Shared.Gibbing;
+using Content.Shared.Humanoid;
using Content.Shared.Medical;
namespace Content.Shared.Body;
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
--- /dev/null
+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;
+}
--- /dev/null
+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);
+ }
+ }
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+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);
+
--- /dev/null
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Body;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedVisualBodySystem))]
+public sealed partial class VisualBodyComponent : Component;
--- /dev/null
+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;
+}
+
--- /dev/null
+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!;
+}
Components =
[
"MobState",
- "HumanoidAppearance",
+ "HumanoidProfile",
],
};
using Content.Shared.Administration.Logs;
+using Content.Shared.Body;
using Content.Shared.Changeling.Components;
using Content.Shared.Cloning;
using Content.Shared.Database;
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!;
[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()
{
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)
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)
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))
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))
$"{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);
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");
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;
[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";
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)
using System.Numerics;
+using Content.Shared.Body;
using Content.Shared.Changeling.Components;
using Content.Shared.Cloning;
using Content.Shared.Humanoid;
[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;
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;
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);
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()
private void SetLayerVisibility(
Entity<HideLayerClothingComponent?, ClothingComponent?> clothing,
- Entity<HumanoidAppearanceComponent?> user,
+ Entity<HideableHumanoidLayersComponent?> user,
bool hideLayers)
{
if (_timing.ApplyingState)
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)
// 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
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)
using System.Linq;
+using Content.Shared.Body;
using Content.Shared.Clothing.Components;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
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)
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();
}
}
--- /dev/null
+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);
+++ /dev/null
-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; }
-}
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;
[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;
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);
_ => 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>
{
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
_ => 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)
{
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;
}
{
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)
public override int GetHashCode()
{
- return HashCode.Combine(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings);
+ return HashCode.Combine(EyeColor, SkinColor, Markings);
}
public HumanoidCharacterAppearance Clone()
--- /dev/null
+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;
+}
--- /dev/null
+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));
+ }
+}
/// 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!;
--- /dev/null
+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");
+ }
+}
LLeg,
RFoot,
LFoot,
+ Overlay,
Handcuffs,
StencilMask,
Ensnare,
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)
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;
-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;
}
-namespace Content.Shared.Humanoid.Markings;
+namespace Content.Shared.Humanoid.Markings.ColoringTypes;
/// <summary>
/// Colors layer in a specified color
[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;
}
-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;
}
-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)
{
{
MarkingId = other.MarkingId;
_markingColors = new(other.MarkingColors);
- Visible = other.Visible;
Forced = other.Forced;
}
[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>
}
return MarkingId.Equals(other.MarkingId)
&& _markingColors.SequenceEqual(other._markingColors)
- && Visible.Equals(other.Visible)
&& Forced.Equals(other.Forced);
}
+++ /dev/null
-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
- };
- }
- }
-}
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)
{
// 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
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
[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;
}
}
/// </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)
{
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;
}
}
+++ /dev/null
-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!;
-}
[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;
+++ /dev/null
-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();
- }
-
-
-}
--- /dev/null
+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;
+}
+++ /dev/null
-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];
- }
-}
[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();
}
+++ /dev/null
-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; }
-}
+using Content.Shared.Body;
using Content.Shared.Dataset;
using Content.Shared.Humanoid.Markings;
using Robust.Shared.Prototypes;
[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>
[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>
--- /dev/null
+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);
+ }
+}
+++ /dev/null
-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");
- }
-}
+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;
[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; }
}
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;
[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
/// 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))
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);
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,
{
_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,
+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;
/// 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.
[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
--- /dev/null
+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;
+}
+++ /dev/null
-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>();
-}
/// </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 +
+using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Content.Shared.CCVar;
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
{
[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*$)");
/// 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;
/// <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()
/// <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),
};
}
public static HumanoidCharacterProfile RandomWithSpecies(string? species = null)
{
- species ??= SharedHumanoidAppearanceSystem.DefaultSpecies;
+ species ??= HumanoidCharacterProfile.DefaultSpecies;
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var random = IoCManager.Resolve<IRobustRandom>();
if (!prototypeManager.TryIndex(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false)
{
- Species = SharedHumanoidAppearanceSystem.DefaultSpecies;
+ Species = HumanoidCharacterProfile.DefaultSpecies;
speciesPrototype = prototypeManager.Index(Species);
}
{
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;
+ }
}
}
+using Content.Shared.Body;
using Content.Shared.DetailExaminable;
using Content.Shared.Forensics.Systems;
using Content.Shared.Humanoid;
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!;
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;
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.
-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;
[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>
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)
+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;
[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>
[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";
-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
+- 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
+- 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 ]
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
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 ]
+- 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 ]
+- 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
+- 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 ]
+- 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 ]
+- 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 ]
+- 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
+- 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
+- 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
- 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:
- type: RotationVisuals
defaultRotation: 90
horizontalRotation: 90
- - type: HumanoidAppearance
+ - type: HumanoidProfile
- type: TypingIndicator
- type: SlowOnDamage
speedModifierThresholds:
- type: CollideOnAnchor
- type: Transform
anchored: false
- - type: AnchoredStorageFilter
- blacklist:
- components:
- - HumanoidAppearance # for forks with felines
- type: BlockAnchorOn
blacklist:
components:
- type: marking
id: VulpBellyCrest
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
state: belly_crest
- type: marking
id: VulpBellyFull
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
state: belly_full
- 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
- type: marking
id: VulpEar
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
state: vulp
- type: marking
id: VulpEarSharp
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
state: vulp
- type: marking
id: VulpEarFade
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
state: vulp
- type: marking
id: VulpEarJackal
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
state: jackal
- type: marking
id: VulpEarTerrier
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
state: terrier
- type: marking
id: VulpEarFennec
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
state: fennec
- type: marking
id: VulpEarFox
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
state: fox
- type: marking
id: VulpEarOtie
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/ear_markings.rsi
state: otie
- 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
- type: marking
id: VulpHairAdhara
bodyPart: Hair
- speciesRestriction: [ Vulpkanin ]
- markingCategory: Hair
+ groupWhitelist: [ Vulpkanin ]
canBeDisplaced: false
sprites:
- sprite: Mobs/Customization/Vulpkanin/hair.rsi
- type: marking
id: VulpHairAnita
bodyPart: Hair
- speciesRestriction: [ Vulpkanin ]
- markingCategory: Hair
+ groupWhitelist: [ Vulpkanin ]
canBeDisplaced: false
sprites:
- sprite: Mobs/Customization/Vulpkanin/hair.rsi
- type: marking
id: VulpHairApollo
bodyPart: Hair
- speciesRestriction: [ Vulpkanin ]
- markingCategory: Hair
+ groupWhitelist: [ Vulpkanin ]
canBeDisplaced: false
sprites:
- sprite: Mobs/Customization/Vulpkanin/hair.rsi
- type: marking
id: VulpHairBelle
bodyPart: Hair
- speciesRestriction: [ Vulpkanin ]
- markingCategory: Hair
+ groupWhitelist: [ Vulpkanin ]
canBeDisplaced: false
sprites:
- sprite: Mobs/Customization/Vulpkanin/hair.rsi
- type: marking
id: VulpHairBraided
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/hair.rsi
state: braided
- type: marking
id: VulpHairBun
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/hair.rsi
state: bun
- type: marking
id: VulpHairCleanCut
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/hair.rsi
state: clean_cut
- type: marking
id: VulpHairCurl
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/hair.rsi
state: curl
- type: marking
id: VulpHairHawk
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/hair.rsi
state: hawk
- 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
- type: marking
id: VulpHeadBlaze
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
state: blaze
- type: marking
id: VulpHeadMask
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
state: mask
- type: marking
id: VulpPatch
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
state: patch
- type: marking
id: VulpSlash
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
state: slash
- type: marking
id: VulpStripes1
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
state: stripes_1
- type: marking
id: VulpStripes2
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
state: stripes_2
- type: marking
id: VulpVulpine
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/head_markings.rsi
state: vulpine
- type: marking
id: VulpClawsHandLeft
bodyPart: LHand
- markingCategory: Arms
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
state: claws_l_hand
- type: marking
id: VulpClawsHandRight
bodyPart: RHand
- markingCategory: Arms
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
state: claws_r_hand
- type: marking
id: VulpClawsFootLeft
bodyPart: LFoot
- markingCategory: Legs
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
state: claws_l_foot
- type: marking
id: VulpClawsFootRight
bodyPart: RFoot
- markingCategory: Legs
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
state: claws_r_foot
## 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
- type: marking
id: VulpPointsCrestArmLeft
- markingCategory: Arms
bodyPart: LArm
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/body_markings.rsi
state: crest-arm-l
- 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
- 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
- type: marking
id: VulpTailFennec
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
state: fennec
- type: marking
id: VulpTailFluffy
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
state: fluffy
- type: marking
id: VulpTailHusky
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
state: husky
- type: marking
id: VulpTailLong
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
state: long
- type: marking
id: VulpTailVulp
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
state: vulp
- type: marking
id: VulpTailVulpFade
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [ Vulpkanin ]
+ groupWhitelist: [ Vulpkanin ]
sprites:
- sprite: Mobs/Customization/Vulpkanin/tail_markings.rsi
state: vulp
- type: marking
id: ArachnidCheliceraeDownwards
bodyPart: HeadSide
- markingCategory: HeadSide
- speciesRestriction: [Arachnid]
+ groupWhitelist: [Arachnid]
sprites:
- sprite: Mobs/Customization/Arachnid/chelicerae.rsi
state: downwards
- type: marking
id: ArachnidCheliceraeInwards
bodyPart: HeadSide
- markingCategory: HeadSide
- speciesRestriction: [Arachnid]
+ groupWhitelist: [Arachnid]
sprites:
- sprite: Mobs/Customization/Arachnid/chelicerae.rsi
state: inwards
- type: marking
id: ArachnidAppendagesDefault
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [Arachnid]
+ groupWhitelist: [Arachnid]
sprites:
- sprite: Mobs/Customization/Arachnid/appendages.rsi
state: long_primary
- type: marking
id: ArachnidAppendagesSharp
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [Arachnid]
+ groupWhitelist: [Arachnid]
sprites:
- sprite: Mobs/Customization/Arachnid/appendages.rsi
state: sharp_primary
- type: marking
id: ArachnidAppendagesStingers
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [Arachnid]
+ groupWhitelist: [Arachnid]
sprites:
- sprite: Mobs/Customization/Arachnid/appendages.rsi
state: stingers_primary
- type: marking
id: ArachnidAppendagesZigZag
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [Arachnid]
+ groupWhitelist: [Arachnid]
sprites:
- sprite: Mobs/Customization/Arachnid/appendages.rsi
state: zigzag_primary
- type: marking
id: ArachnidAppendagesCurled
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [Arachnid]
+ groupWhitelist: [Arachnid]
sprites:
- sprite: Mobs/Customization/Arachnid/appendages.rsi
state: curled_primary
- type: marking
id: ArachnidAppendagesChipped
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [Arachnid]
+ groupWhitelist: [Arachnid]
sprites:
- sprite: Mobs/Customization/Arachnid/appendages.rsi
state: chipped_primary
- type: marking
id: ArachnidAppendagesHarvest
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [Arachnid]
+ groupWhitelist: [Arachnid]
sprites:
- sprite: Mobs/Customization/Arachnid/appendages.rsi
state: harvest_primary
- 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
- type: marking
id: CatEars
bodyPart: Special
- markingCategory: Special
- speciesRestriction: [Human]
+ groupWhitelist: [Human]
coloring:
default:
type:
- type: marking
id: CatTail
bodyPart: Tail
- markingCategory: Tail
- speciesRestriction: [Human]
+ groupWhitelist: [Human]
coloring:
default:
type:
- type: marking
id: DionaThornsHead
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [Diona]
+ groupWhitelist: [Diona]
coloring:
default:
type:
- type: marking
id: DionaThornsBody
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [Diona]
+ groupWhitelist: [Diona]
coloring:
default:
type:
- type: marking
id: DionaFlowersHead
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [Diona]
+ groupWhitelist: [Diona]
coloring:
default:
type:
- type: marking
id: DionaFlowersBody
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [Diona]
+ groupWhitelist: [Diona]
coloring:
default:
type:
- type: marking
id: DionaLeafCover
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [Diona]
+ groupWhitelist: [Diona]
coloring:
default:
type:
- type: marking
id: DionaBloomHead
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [Diona]
+ groupWhitelist: [Diona]
coloring:
default:
type:
- type: marking
id: DionaBracketHead
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [Diona]
+ groupWhitelist: [Diona]
coloring:
default:
type:
- 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:
- 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
- type: marking
id: GauzeLefteyePatch
bodyPart: Eyes
- markingCategory: Overlay
- speciesRestriction: [Dwarf, Human, Arachnid]
+ groupWhitelist: [Human, Arachnid]
coloring:
default:
type:
- type: marking
id: GauzeLefteyePad
bodyPart: Eyes
- markingCategory: Overlay
- speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+ groupWhitelist: [Human, Reptilian, Arachnid]
coloring:
default:
type:
- type: marking
id: GauzeRighteyePatch
bodyPart: Eyes
- markingCategory: Overlay
- speciesRestriction: [Dwarf, Human, Arachnid]
+ groupWhitelist: [Human, Arachnid]
coloring:
default:
type:
- type: marking
id: GauzeRighteyePad
bodyPart: Eyes
- markingCategory: Overlay
- speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+ groupWhitelist: [Human, Reptilian, Arachnid]
coloring:
default:
type:
- type: marking
id: GauzeBlindfold
bodyPart: Eyes
- markingCategory: Overlay
- speciesRestriction: [Dwarf, Human, Arachnid]
+ groupWhitelist: [Human, Arachnid]
coloring:
default:
type:
- type: marking
id: GauzeShoulder
bodyPart: Chest
- markingCategory: Overlay
- speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+ groupWhitelist: [Human, Reptilian, Arachnid]
coloring:
default:
type:
- type: marking
id: GauzeStomach
bodyPart: Chest
- markingCategory: Overlay
- speciesRestriction: [Dwarf, Human, Reptilian, Arachnid]
+ groupWhitelist: [Human, Reptilian, Arachnid]
coloring:
default:
type:
- 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:
- 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
- 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
- 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
- type: marking
id: MothAntennasDefault
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Moth]
+ groupWhitelist: [Moth]
sprites:
- sprite: Mobs/Customization/Moth/moth_antennas.rsi
state: default
- type: marking
id: MothAntennasCharred
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Moth]
+ groupWhitelist: [Moth]
sprites:
- sprite: Mobs/Customization/Moth/moth_antennas.rsi
state: charred
- type: marking
id: MothAntennasDbushy
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Moth]
+ groupWhitelist: [Moth]
sprites:
- sprite: Mobs/Customization/Moth/moth_antennas.rsi
state: dbushy
- type: marking
id: MothAntennasDcurvy
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Moth]
+ groupWhitelist: [Moth]
sprites:
- sprite: Mobs/Customization/Moth/moth_antennas.rsi
state: dcurvy
- type: marking
id: MothAntennasDfan
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Moth]
+ groupWhitelist: [Moth]
sprites:
- sprite: Mobs/Customization/Moth/moth_antennas.rsi
state: dfan
- type: marking
id: MothAntennasDpointy
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Moth]
+ groupWhitelist: [Moth]
sprites:
- sprite: Mobs/Customization/Moth/moth_antennas.rsi
state: dpointy
- type: marking
id: MothAntennasFeathery
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Moth]
+ groupWhitelist: [Moth]
sprites:
- sprite: Mobs/Customization/Moth/moth_antennas.rsi
state: feathery
- type: marking
id: MothAntennasFirewatch
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Moth]
+ groupWhitelist: [Moth]
sprites:
- sprite: Mobs/Customization/Moth/moth_antennas.rsi
state: firewatch
- type: marking
id: MothAntennasGray
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Moth]
+ groupWhitelist: [Moth]
sprites:
- sprite: Mobs/Customization/Moth/moth_antennas.rsi
state: gray
- type: marking
id: MothAntennasJungle
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Moth]
+ groupWhitelist: [Moth]
sprites:
- sprite: Mobs/Customization/Moth/moth_antennas.rsi
state: jungle
- 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
- type: marking
id: LizardFrillsAquatic
bodyPart: HeadSide
- markingCategory: HeadSide
- speciesRestriction: [Reptilian]
+ groupWhitelist: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: frills_aquatic
- type: marking
id: LizardFrillsShort
bodyPart: HeadSide
- markingCategory: HeadSide
- speciesRestriction: [Reptilian]
+ groupWhitelist: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: frills_short
- type: marking
id: LizardFrillsSimple
bodyPart: HeadSide
- markingCategory: HeadSide
- speciesRestriction: [Reptilian]
+ groupWhitelist: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: frills_simple
- type: marking
id: LizardFrillsDivinity
bodyPart: HeadSide
- markingCategory: HeadSide
- speciesRestriction: [Reptilian]
+ groupWhitelist: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: frills_divinity
- type: marking
id: LizardFrillsBig
bodyPart: HeadSide
- markingCategory: HeadSide
- speciesRestriction: [Reptilian]
+ groupWhitelist: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: frills_big
- type: marking
id: LizardFrillsAxolotl
bodyPart: HeadSide
- markingCategory: HeadSide
- speciesRestriction: [Reptilian]
+ groupWhitelist: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: frills_axolotl
- type: marking
id: LizardFrillsHood
bodyPart: HeadSide
- markingCategory: HeadSide
- speciesRestriction: [Reptilian]
+ groupWhitelist: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: frills_hood_primary
- type: marking
id: LizardFrillsNeckfull
bodyPart: HeadSide
- markingCategory: HeadSide
- speciesRestriction: [Reptilian]
+ groupWhitelist: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: frills_neckfull
- type: marking
id: LizardHornsAngler
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Reptilian]
+ groupWhitelist: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: horns_angler
- type: marking
id: LizardHornsCurled
bodyPart: HeadTop
- markingCategory: HeadTop
- speciesRestriction: [Reptilian]
+ groupWhitelist: [Reptilian]
sprites:
- sprite: Mobs/Customization/reptilian_parts.rsi
state: horns_curled
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- type: marking
id: SlimeGradientLeftArm
bodyPart: LArm
- markingCategory: Arms
- speciesRestriction: [SlimePerson]
+ groupWhitelist: [Slime]
sprites:
- sprite: Mobs/Customization/slime_parts.rsi
state: gradient_l_arm
- type: marking
id: SlimeGradientRightArm
bodyPart: RArm
- markingCategory: Arms
- speciesRestriction: [SlimePerson]
+ groupWhitelist: [Slime]
sprites:
- sprite: Mobs/Customization/slime_parts.rsi
state: gradient_r_arm
- type: marking
id: SlimeGradientLeftLeg
bodyPart: LLeg
- markingCategory: Legs
- speciesRestriction: [SlimePerson]
+ groupWhitelist: [Slime]
sprites:
- sprite: Mobs/Customization/slime_parts.rsi
state: gradient_l_leg
- type: marking
id: SlimeGradientRightLeg
bodyPart: RLeg
- markingCategory: Legs
- speciesRestriction: [SlimePerson]
+ groupWhitelist: [Slime]
sprites:
- sprite: Mobs/Customization/slime_parts.rsi
state: gradient_r_leg
- type: marking
id: SlimeGradientLeftFoot
bodyPart: LFoot
- markingCategory: Legs
- speciesRestriction: [SlimePerson]
+ groupWhitelist: [Slime]
sprites:
- sprite: Mobs/Customization/slime_parts.rsi
state: gradient_l_foot
- type: marking
id: SlimeGradientRightFoot
bodyPart: RFoot
- markingCategory: Legs
- speciesRestriction: [SlimePerson]
+ groupWhitelist: [Slime]
sprites:
- sprite: Mobs/Customization/slime_parts.rsi
state: gradient_r_foot
- type: marking
id: SlimeGradientLeftHand
bodyPart: LHand
- markingCategory: Arms
- speciesRestriction: [SlimePerson]
+ groupWhitelist: [Slime]
sprites:
- sprite: Mobs/Customization/slime_parts.rsi
state: gradient_l_hand
- type: marking
id: SlimeGradientRightHand
bodyPart: RHand
- markingCategory: Arms
- speciesRestriction: [SlimePerson]
+ groupWhitelist: [Slime]
sprites:
- sprite: Mobs/Customization/slime_parts.rsi
state: gradient_r_hand
- type: marking
id: TattooHiveChest
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [Human, Dwarf]
+ groupWhitelist: [Human]
coloring:
default:
type:
- type: marking
id: TattooNightlingChest
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [Human, Dwarf]
+ groupWhitelist: [Human]
coloring:
default:
type:
- type: marking
id: TattooSilverburghLeftLeg
bodyPart: LLeg
- markingCategory: Legs
- speciesRestriction: [Human, Dwarf]
+ groupWhitelist: [Human]
coloring:
default:
type:
- type: marking
id: TattooSilverburghRightLeg
bodyPart: RLeg
- markingCategory: Legs
- speciesRestriction: [Human, Dwarf]
+ groupWhitelist: [Human]
coloring:
default:
type:
- type: marking
id: TattooCampbellLeftArm
bodyPart: LArm
- markingCategory: Arms
- speciesRestriction: [Human, Dwarf]
+ groupWhitelist: [Human]
coloring:
default:
type:
- type: marking
id: TattooCampbellRightArm
bodyPart: RArm
- markingCategory: Arms
- speciesRestriction: [Human, Dwarf]
+ groupWhitelist: [Human]
coloring:
default:
type:
- type: marking
id: TattooCampbellLeftLeg
bodyPart: LLeg
- markingCategory: Legs
- speciesRestriction: [Human, Dwarf]
+ groupWhitelist: [Human]
coloring:
default:
type:
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- type: marking
id: VoxHairAfro
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_hair.rsi
state: afro_s
- type: marking
id: VoxHairBraids
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_hair.rsi
state: braid_s
- type: marking
id: VoxHairBushy
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_hair.rsi
state: bushy_s
- type: marking
id: VoxHairCrestedQuills
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_hair.rsi
state: crestedquills_s
- type: marking
id: VoxHairEmperorQuills
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_hair.rsi
state: emperorquills_s
- type: marking
id: VoxHairFlowing
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_hair.rsi
state: flowing_s
- type: marking
id: VoxHairHawk
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_hair.rsi
state: hawk_s
- type: marking
id: VoxHairHedgehog
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_hair.rsi
state: hedgehog_s
- type: marking
id: VoxHairHorns
bodyPart: Hair
- markingCategory: Hair
canBeDisplaced: false
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_hair.rsi
state: horns_s
- 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
- type: marking
id: VoxBeak
bodyPart: Snout
- markingCategory: Snout
forcedColoring: true
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_parts.rsi
state: beak
# 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
- type: marking
id: VoxBeakShaved
bodyPart: Snout
- markingCategory: Snout
forcedColoring: true
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_parts.rsi
state: beak_shaved
- type: marking
id: VoxBeakHooked
bodyPart: Snout
- markingCategory: Snout
forcedColoring: true
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_parts.rsi
state: beak_hooked
- type: marking
id: VoxLArmScales
bodyPart: LArm
- markingCategory: Arms
forcedColoring: true
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_parts.rsi
state: l_arm
- type: marking
id: VoxLLegScales
bodyPart: LLeg
- markingCategory: Legs
forcedColoring: true
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_parts.rsi
state: l_leg
- 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
- type: marking
id: VoxScarEyeRight
bodyPart: Head
- markingCategory: Head
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
sprites:
- sprite: Mobs/Customization/vox_scars.rsi
state: vox_scar_eye_right
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- type: marking
id: TattooVoxHeartLeftArm
bodyPart: LArm
- markingCategory: Arms
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
coloring:
default:
type:
- type: marking
id: TattooVoxHeartRightArm
bodyPart: RArm
- markingCategory: Arms
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
coloring:
default:
type:
- type: marking
id: TattooVoxHiveChest
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
coloring:
default:
type:
- type: marking
id: TattooVoxNightlingChest
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
coloring:
default:
type:
- type: marking
id: TattooVoxNightbelt
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
coloring:
default:
type:
- type: marking
id: TattooVoxChestV
bodyPart: Chest
- markingCategory: Chest
- speciesRestriction: [Vox]
+ groupWhitelist: [Vox]
coloring:
default:
type:
- 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:
- type: NPCImprintingOnSpawnBehaviour
whitelist:
components:
- - HumanoidAppearance
+ - HumanoidProfile
- type: Sprite
sprite: Mobs/Demons/tomatokiller.rsi
noRot: true
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
- type: Devourer
foodPreferenceWhitelist:
components:
- - HumanoidAppearance
+ - HumanoidProfile
stomachStorageWhitelist:
components:
- MobState
components:
- type: RandomHumanoidAppearance
randomizeName: false
- hair: HairBald
- type: Loadout
prototypes: [SyndicateOperativeGearExtremelyBasic]
roleLoadout: [ RoleSurvivalSyndicate ]
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
mechToPilotDamageMultiplier: 0.75
pilotWhitelist:
components:
- - HumanoidAppearance
+ - HumanoidProfile
- type: MeleeWeapon
hidden: true
attackRate: 1
mechToPilotDamageMultiplier: 0.5
pilotWhitelist:
components:
- - HumanoidAppearance
+ - HumanoidProfile
- type: entity
parent: MechHonker
state: storage
sprite: Objects/Tools/scissors.rsi
- type: MagicMirror
+ organs:
+ - Head
+ layers:
+ - Hair
+ - FacialHair
- type: ActivatableUI
key: enum.MagicMirrorUiKey.Key
inHandsOnly: true
- type: Strap
whitelist:
components:
- - HumanoidAppearance
+ - HumanoidProfile
- type: DisposalUnit
blacklist:
components:
- - HumanoidAppearance
+ - HumanoidProfile
- Plunger
- SolutionTransfer
- EntityStorage
- 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
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
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
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
name: species-name-dwarf
roundStart: true
prototype: MobDwarf
- sprites: MobHumanSprites
- markingLimits: MobHumanMarkingLimits
dollPrototype: AppearanceDwarf
skinColoration: HumanToned
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
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
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
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
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
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
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
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
--- /dev/null
+<?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>