Marking default coloring (#13039)

* Marking coloring WIP

* EnsureDefault now supports coloring!

* Now markings have coloring when they get added

* Many things

* yml files

* cleanup

* Some requested changes

* Nullable type and WIP caching

* Time to resolve that thing with deprecated hair fields

* Latest reviews + im still trying to use these hair markings

* FirstOrDefault thing and Tattoo docs

* IDK

* It's now works a bit more properly in preferences GUI

* THEY SYNCING! However preferences GUI still broken and doesn't work properly

* Markings now updating when changing in GUI. However they still don't work properly with bald humanoids

* Forgor...

* Default hair-colored markings will not color to hair if there is no hair

* Fixed default colors for customizable markings

* Fixed bug in prefs GUI that set current hair to null

* Now markings that must match skin color because of limb (e.x. Slimes) - will match skin color

* final tweaks: if hair uses skin color then markings will use skin color as hair color (slimes)

* fix

* fixed dirty. no more funni invis bug

* Mirrors and client profile loading

* default colors soon TM

* review + better coloring

* Hardcode is gone

* diona markings

* oh my god

* fixed CategoryColoring

* cool fallback, clean up and some other tweaks

* code style

* more style

* a
This commit is contained in:
csqrb
2023-03-05 08:59:07 +06:00
committed by GitHub
parent 0ad9af7ae2
commit 8b3d7728d7
26 changed files with 863 additions and 83 deletions

View File

@@ -70,6 +70,18 @@ public sealed class HumanoidAppearanceComponent : Component
[DataField("eyeColor")]
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;
}
[Serializable, NetSerializable]

View File

@@ -217,7 +217,6 @@ namespace Content.Shared.Humanoid
{
markingSet = new MarkingSet(appearance.Markings, speciesProto.MarkingPoints, markingManager, proto);
markingSet.EnsureValid(markingManager);
markingSet.FilterSpecies(species, markingManager);
switch (speciesProto.SkinColoration)
{
@@ -236,6 +235,7 @@ namespace Content.Shared.Humanoid
break;
}
markingSet.EnsureSpecies(species, skinColor, markingManager);
}
return new HumanoidCharacterAppearance(

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Serialization;
using Content.Shared.Humanoid.Markings;
using Robust.Shared.Serialization;
namespace Content.Shared.Humanoid
{

View File

@@ -0,0 +1,24 @@
using System.Linq;
namespace Content.Shared.Humanoid.Markings;
/// <summary>
/// Colors marking in color of first defined marking from specified category (in e.x. from Hair category)
/// </summary>
public sealed class CategoryColoring : LayerColoringType
{
[DataField("category", required: true)]
public MarkingCategories Category;
public override Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet)
{
Color? outColor = null;
if (markingSet.TryGetCategory(Category, out var markings) &&
markings.Count > 0)
{
outColor = markings[0].MarkingColors.FirstOrDefault();
}
return outColor;
}
}

View File

@@ -0,0 +1,12 @@
namespace Content.Shared.Humanoid.Markings;
/// <summary>
/// Colors layer in an eye color
/// </summary>
public sealed class EyeColoring : LayerColoringType
{
public override Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet)
{
return eyes;
}
}

View File

@@ -0,0 +1,15 @@
namespace Content.Shared.Humanoid.Markings;
/// <summary>
/// Colors layer in a specified color
/// </summary>
public sealed class SimpleColoring : LayerColoringType
{
[DataField("color", required: true)]
public Color Color = Color.White;
public override Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet)
{
return Color;
}
}

View File

@@ -0,0 +1,12 @@
namespace Content.Shared.Humanoid.Markings;
/// <summary>
/// Colors layer in a skin color
/// </summary>
public sealed class SkinColoring : LayerColoringType
{
public override Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet)
{
return skin;
}
}

View File

@@ -0,0 +1,20 @@
namespace Content.Shared.Humanoid.Markings;
/// <summary>
/// Colors layer in skin color but much darker.
/// </summary>
public sealed class TattooColoring : LayerColoringType
{
public override Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet)
{
if (skin == null)
{
return null;
}
var newColor = Color.ToHsv(skin.Value);
newColor.Z = .40f;
return Color.FromHsv(newColor);
}
}

View File

@@ -1,4 +1,6 @@
using System.Linq;
using Content.Shared.Humanoid.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Humanoid.Markings
@@ -67,6 +69,14 @@ namespace Content.Shared.Humanoid.Markings
public void SetColor(int colorIndex, Color color) =>
_markingColors[colorIndex] = color;
public void SetColor(Color color)
{
for (int i = 0; i < _markingColors.Count; i++)
{
_markingColors[i] = color;
}
}
public int CompareTo(Marking? marking)
{
if (marking == null)

View File

@@ -0,0 +1,147 @@
using Robust.Shared.Utility;
namespace Content.Shared.Humanoid.Markings;
/// <summary>
/// Default colors for marking
/// </summary>
[DataDefinition]
public sealed class MarkingColors
{
/// <summary>
/// Coloring properties that will be used on any unspecified layer
/// </summary>
[DataField("default", true)]
public LayerColoringDefinition Default = new LayerColoringDefinition();
/// <summary>
/// Layers with their own coloring type and properties
/// </summary>
[DataField("layers", true)]
public Dictionary<string, LayerColoringDefinition>? Layers;
}
public static class MarkingColoring
{
/// <summary>
/// Returns list of colors for marking layers
/// </summary>
public static List<Color> GetMarkingLayerColors
(
MarkingPrototype prototype,
Color? skinColor,
Color? eyeColor,
MarkingSet markingSet
)
{
var colors = new List<Color>();
// Coloring from default properties
var defaultColor = prototype.Coloring.Default.GetColor(skinColor, eyeColor, markingSet);
if (prototype.Coloring.Layers == null)
{
// If layers is not specified, then every layer must be default
for (var i = 0; i < prototype.Sprites.Count; i++)
{
colors.Add(defaultColor);
}
return colors;
}
else
{
// If some layers are specified.
for (var i = 0; i < prototype.Sprites.Count; i++)
{
// Getting layer name
string? name = prototype.Sprites[i] switch
{
SpriteSpecifier.Rsi rsi => rsi.RsiState,
SpriteSpecifier.Texture texture => texture.TexturePath.Filename,
_ => null
};
if (name == null)
{
colors.Add(defaultColor);
continue;
}
// 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);
colors.Add(marking_color);
}
else
{
colors.Add(defaultColor);
}
}
return colors;
}
}
}
/// <summary>
/// A class that defines coloring type and fallback for markings
/// </summary>
[DataDefinition]
public sealed class LayerColoringDefinition
{
[DataField("type")]
public LayerColoringType Type = new SkinColoring();
/// <summary>
/// Coloring types that will be used if main coloring type will return nil
/// </summary>
[DataField("fallbackTypes")]
public List<LayerColoringType> FallbackTypes = new() {};
/// <summary>
/// Color that will be used if coloring type and fallback type will return nil
/// </summary>
[DataField("fallbackColor")]
public Color FallbackColor = Color.White;
public Color GetColor(Color? skin, Color? eyes, MarkingSet markingSet)
{
var color = Type.GetColor(skin, eyes, markingSet);
if (color == null)
{
foreach (var type in FallbackTypes)
{
color = type.GetColor(skin, eyes, markingSet);
if (color != null) break;
}
}
return color ?? FallbackColor;
}
}
/// <summary>
/// An abstract class for coloring types
/// </summary>
[ImplicitDataDefinitionForInheritors]
public abstract class LayerColoringType
{
/// <summary>
/// Makes output color negative
/// </summary>
[DataField("negative")]
public bool Negative { get; } = false;
public abstract Color? GetCleanColor(Color? skin, Color? eyes, MarkingSet markingSet);
public Color? GetColor(Color? skin, Color? eyes, MarkingSet markingSet)
{
var color = GetCleanColor(skin, eyes, markingSet);
// Negative color
if (color != null && Negative)
{
var rcolor = color.Value;
rcolor.R = 1f-rcolor.R;
rcolor.G = 1f-rcolor.G;
rcolor.B = 1f-rcolor.B;
return rcolor;
}
return color;
}
}

View File

@@ -120,5 +120,69 @@ namespace Content.Shared.Humanoid.Markings
_index.Add(markingPrototype);
}
}
public bool CanBeApplied(string species, Marking marking, IPrototypeManager? prototypeManager = null)
{
IoCManager.Resolve(ref prototypeManager);
var speciesProto = prototypeManager.Index<SpeciesPrototype>(species);
var onlyWhitelisted = prototypeManager.Index<MarkingPointsPrototype>(speciesProto.MarkingPoints).OnlyWhitelisted;
if (!TryGetMarking(marking, out var prototype))
{
return false;
}
if (onlyWhitelisted && prototype.SpeciesRestrictions == null)
{
return false;
}
if (prototype.SpeciesRestrictions != null
&& !prototype.SpeciesRestrictions.Contains(species))
{
return false;
}
return true;
}
public bool CanBeApplied(string species, MarkingPrototype prototype, IPrototypeManager? prototypeManager = null)
{
IoCManager.Resolve(ref prototypeManager);
var speciesProto = prototypeManager.Index<SpeciesPrototype>(species);
var onlyWhitelisted = prototypeManager.Index<MarkingPointsPrototype>(speciesProto.MarkingPoints).OnlyWhitelisted;
if (onlyWhitelisted && prototype.SpeciesRestrictions == null)
{
return false;
}
if (prototype.SpeciesRestrictions != null &&
!prototype.SpeciesRestrictions.Contains(species))
{
return false;
}
return true;
}
public bool MustMatchSkin(string species, HumanoidVisualLayers layer, IPrototypeManager? prototypeManager = null)
{
IoCManager.Resolve(ref prototypeManager);
var speciesProto = prototypeManager.Index<SpeciesPrototype>(species);
if (
!prototypeManager.TryIndex(speciesProto.SpriteSet, out HumanoidSpeciesBaseSpritesPrototype? baseSprites) ||
!baseSprites.Sprites.TryGetValue(layer, out var spriteName) ||
!prototypeManager.TryIndex(spriteName, out HumanoidSpeciesSpriteLayer? sprite) ||
sprite == null ||
!sprite.MarkingsMatchSkin
)
{
return false;
}
return true;
}
}
}

View File

@@ -16,13 +16,19 @@ namespace Content.Shared.Humanoid.Markings
[DataField("markingCategory", required: true)]
public MarkingCategories MarkingCategory { get; } = default!;
[DataField("speciesRestriction")]
public List<string>? SpeciesRestrictions { get; }
[DataField("followSkinColor")]
public bool FollowSkinColor { get; } = false;
[DataField("forcedColoring")]
public bool ForcedColoring { get; } = false;
[DataField("coloring")]
public MarkingColors Coloring { get; } = new();
[DataField("sprites", required: true)]
public List<SpriteSpecifier> Sprites { get; private set; } = default!;

View File

@@ -4,6 +4,7 @@ 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;
@@ -104,6 +105,22 @@ public sealed class MarkingSet
}
}
/// <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>
@@ -122,12 +139,13 @@ public sealed class MarkingSet
}
/// <summary>
/// Filters markings based on species restrictions in the marking's prototype from this marking set.
/// 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 FilterSpecies(string species, MarkingManager? markingManager = null, IPrototypeManager? prototypeManager = null)
public void EnsureSpecies(string species, Color? skinColor, MarkingManager? markingManager = null, IPrototypeManager? prototypeManager = null)
{
IoCManager.Resolve(ref markingManager);
IoCManager.Resolve(ref prototypeManager);
@@ -163,6 +181,22 @@ public sealed class MarkingSet
{
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, prototypeManager))
{
marking.SetColor(skinColor.Value);
}
}
}
}
}
/// <summary>
@@ -200,9 +234,11 @@ public sealed class MarkingSet
/// <summary>
/// Ensures that the default markings as defined by the marking point set in this marking set are applied.
/// </summary>
/// <param name="skinColor">Color to apply.</param>
/// <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, MarkingManager? markingManager = null)
public void EnsureDefault(Color? skinColor = null, Color? eyeColor = null, MarkingManager? markingManager = null)
{
IoCManager.Resolve(ref markingManager);
@@ -218,22 +254,13 @@ public sealed class MarkingSet
{
if (markingManager.Markings.TryGetValue(points.DefaultMarkings[index], out var prototype))
{
Marking marking;
if (skinColor == null)
{
marking = new Marking(points.DefaultMarkings[index], prototype.Sprites.Count);
}
else
{
var colors = new List<Color>();
for (var i = 0; i < prototype.Sprites.Count; i++)
{
colors.Add(skinColor.Value);
}
marking = new Marking(points.DefaultMarkings[index], colors);
}
var colors = MarkingColoring.GetMarkingLayerColors(
prototype,
skinColor,
eyeColor,
this
);
var marking = new Marking(points.DefaultMarkings[index], colors);
AddBack(category, marking);
}

View File

@@ -129,7 +129,7 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
}
humanoid.Species = species;
humanoid.MarkingSet.FilterSpecies(species, _markingManager);
humanoid.MarkingSet.EnsureSpecies(species, humanoid.SkinColor, _markingManager);
var oldMarkings = humanoid.MarkingSet.GetForwardEnumerator().ToList();
humanoid.MarkingSet = new(oldMarkings, prototype.MarkingPoints, _markingManager, _prototypeManager);

View File

@@ -42,7 +42,12 @@ public sealed class HumanoidMarkingModifierBaseLayersSetMessage : BoundUserInter
public sealed class HumanoidMarkingModifierState : BoundUserInterfaceState
{
// TODO just use the component state, remove the BUI state altogether.
public HumanoidMarkingModifierState(MarkingSet markingSet, string species, Color skinColor, Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> customBaseLayers)
public HumanoidMarkingModifierState(
MarkingSet markingSet,
string species,
Color skinColor,
Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> customBaseLayers
)
{
MarkingSet = markingSet;
Species = species;
@@ -53,5 +58,8 @@ public sealed class HumanoidMarkingModifierState : BoundUserInterfaceState
public MarkingSet MarkingSet { get; }
public string Species { get; }
public Color SkinColor { get; }
public Color EyeColor { get; }
public Color? HairColor { get; }
public Color? FacialHairColor { get; }
public Dictionary<HumanoidVisualLayers, CustomBaseLayerInfo> CustomBaseLayers { get; }
}