A utility class that compares two CharacterAppearanceData objects for exact equality without allocating. It checks many scalar fields, lists (clothing, head weights, photo faces), dictionaries (region/photo offsets/weights) and nested makeup/marking structures to detect any differences.
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;
}
}