Utility class that compares two CharacterAppearanceData objects for deep equality without allocations. It compares primitive fields, lists, dictionaries, nested shape, photo, region and makeup data by value to detect any changes.
using System;
namespace CharacterEditor;
/// <summary>Exact, allocation-free comparison of mutable portable appearance data.
/// The presentation keeps a deep snapshot after successful application, so edits
/// to arrays, regional weights, photos or makeup are detected without JSON.</summary>
internal static class AppearanceComparison
{
public static bool Same(CharacterAppearanceData a, CharacterAppearanceData b)
{
if (ReferenceEquals(a, b))
return true;
if (a is null || b is null)
return false;
return a.Version == b.Version && a.VoiceId == b.VoiceId && a.SkinPath == b.SkinPath && a.HairPath == b.HairPath && a.FacialHairPath == b.FacialHairPath && a.HairColorR == b.HairColorR && a.HairColorG == b.HairColorG && a.HairColorB == b.HairColorB && a.SkinColorR == b.SkinColorR && a.SkinColorG == b.SkinColorG && a.SkinColorB == b.SkinColorB && a.CustomEyeColor == b.CustomEyeColor && a.EyeColorR == b.EyeColorR && a.EyeColorG == b.EyeColorG && a.EyeColorB == b.EyeColorB && a.NormalizeSkinTone == b.NormalizeSkinTone && a.NeckSize == b.NeckSize && a.TrapeziusSize == b.TrapeziusSize && a.CollarboneHeight == b.CollarboneHeight && a.BellySize == b.BellySize && a.WaistSize == b.WaistSize && a.ChestSize == b.ChestSize && a.HeadShape == b.HeadShape && a.NoseSize == b.NoseSize && a.ChinSize == b.ChinSize && a.NoseWidth == b.NoseWidth && a.ChinWidth == b.ChinWidth && a.Fitness == b.Fitness && a.JawWidth == b.JawWidth && a.NostrilWidth == b.NostrilWidth && a.ForeheadWidth == b.ForeheadWidth && a.CheekboneWidth == b.CheekboneWidth && a.EarSize == b.EarSize && a.EyeSize == b.EyeSize && a.CheekFullness == b.CheekFullness && a.NostrilFullness == b.NostrilFullness && a.NoseHeight == b.NoseHeight && a.ChinHeight == b.ChinHeight && a.SubmentalFullness == b.SubmentalFullness && a.BrowProminence == b.BrowProminence && a.BodyFeatureSize == b.BodyFeatureSize && a.ButtSize == b.ButtSize && a.FootSize == b.FootSize && a.PhiltrumWidth == b.PhiltrumWidth && a.ColumellaWidth == b.ColumellaWidth && a.MouthWidth == b.MouthWidth && a.MouthProjection == b.MouthProjection && a.UpperLipFullness == b.UpperLipFullness && a.LowerLipFullness == b.LowerLipFullness && a.EarTaper == b.EarTaper && Items(a.ClothingPaths, b.ClothingPaths) && Shape(a.BaseShape, b.BaseShape) && Makeup(a.Makeup, b.Makeup);
}
static bool Items<T>(IReadOnlyList<T> a, IReadOnlyList<T> b)
{
if (ReferenceEquals(a, b))
return true;
if (a is null || b is null || a.Count != b.Count)
return false;
for (int i = 0; i < a.Count; i++)
if (!EqualityComparer<T>.Default.Equals(a[i], b[i]))
return false;
return true;
}
static bool Values(Dictionary<string, float> a, Dictionary<string, float> b)
{
if (ReferenceEquals(a, b))
return true;
if (a is null || b is null || a.Count != b.Count)
return false;
foreach (var pair in a)
if (!b.TryGetValue(pair.Key, out var value) || pair.Value != value)
return false;
return true;
}
static bool Shape(CharacterBaseShape a, CharacterBaseShape b)
{
if (ReferenceEquals(a, b))
return true;
if (a is null || b is null || a.MaleBody != b.MaleBody || a.BreastSize != b.BreastSize || a.BulgeSize != b.BulgeSize || a.ClothingVariant != b.ClothingVariant || !Items(a.HeadWeights, b.HeadWeights))
return false;
if (!ReferenceEquals(a.PhotoFaces, b.PhotoFaces))
{
if (a.PhotoFaces is null || b.PhotoFaces is null || a.PhotoFaces.Count != b.PhotoFaces.Count)
return false;
for (int i = 0; i < a.PhotoFaces.Count; i++)
if (!Photo(a.PhotoFaces[i], b.PhotoFaces[i]))
return false;
}
if (!ReferenceEquals(a.Regions, b.Regions))
{
if (a.Regions is null || b.Regions is null || a.Regions.Count != b.Regions.Count)
return false;
foreach (var pair in a.Regions)
if (!b.Regions.TryGetValue(pair.Key, out var value) || !Region(pair.Value, value))
return false;
}
return true;
}
static bool Photo(PhotoFace a, PhotoFace b) => ReferenceEquals(a, b) || (a is not null && b is not null && a.Version == b.Version && a.Id == b.Id && a.Name == b.Name && a.Strength == b.Strength && a.Influence == b.Influence && Items(a.HeadWeights, b.HeadWeights) && Values(a.Offsets, b.Offsets));
static bool Region(FaceRegionBlend a, FaceRegionBlend b) => ReferenceEquals(a, b) || (a is not null && b is not null && a.OverallInfluence == b.OverallInfluence && Items(a.HeadWeights, b.HeadWeights) && Values(a.PhotoWeights, b.PhotoWeights));
static bool Ink(MakeupLayerData a, MakeupLayerData b) => ReferenceEquals(a, b) || (a is not null && b is not null && a.Red == b.Red && a.Green == b.Green && a.Blue == b.Blue && a.Amount == b.Amount);
static bool Marking(CharacterMarkingData a, CharacterMarkingData b) => ReferenceEquals(a, b) || (a is not null && b is not null && a.Style == b.Style && a.Region == b.Region && a.Horizontal == b.Horizontal && a.Vertical == b.Vertical && a.Scale == b.Scale && a.Rotation == b.Rotation && a.Spacing == b.Spacing && Ink(a.Ink, b.Ink));
static bool Makeup(CharacterMakeupData a, CharacterMakeupData b)
{
if (ReferenceEquals(a, b))
return true;
if (a is null || b is null || a.TattooStyle != b.TattooStyle || !Ink(a.Lipstick, b.Lipstick) || !Ink(a.Eyeshadow, b.Eyeshadow) || !Ink(a.Eyeliner, b.Eyeliner) || !Ink(a.LowerEyeshadow, b.LowerEyeshadow) || !Ink(a.Blush, b.Blush) || !Ink(a.Tattoo, b.Tattoo))
return false;
if (a.EyelinerLowerStrength != b.EyelinerLowerStrength ||
a.EyelinerInnerStrength != b.EyelinerInnerStrength ||
a.EyelinerWing != b.EyelinerWing ||
a.EyelinerWingStrength != b.EyelinerWingStrength ||
a.EyelinerWingHorizontal != b.EyelinerWingHorizontal ||
a.EyelinerWingVertical != b.EyelinerWingVertical ||
a.EyelinerWingThickness != b.EyelinerWingThickness)
return false;
if (ReferenceEquals(a.Markings, b.Markings))
return true;
if (a.Markings is null || b.Markings is null || a.Markings.Count != b.Markings.Count)
return false;
for (int i = 0; i < a.Markings.Count; i++)
if (!Marking(a.Markings[i], b.Markings[i]))
return false;
return true;
}
}