s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=vendetta.characterkit&take=20
Showing code results for query:
*
(94 total matches found)
Game
library
using System;
using System.IO;
using System.Text;
using System.Runtime.InteropServices;
namespace CharacterEditor;
/// <summary>Fits loaded human clothes against shared body surfaces, without garment authoring files.</summary>
public static class AutomaticWardrobeFit
{
const string ReferencePath = "models/characterkit/wardrobe-reference.bin";
public const string Morph = "ce_body_female";
// Cache by resource identity and authored cut, never by the current slider value.
static readonly Dictionary<(Model, bool, bool), Model> fittedModels = new();
static readonly Dictionary<(Model, bool, bool), string> fittingFailures = new();
static Surface originalMale, unifiedMale, unifiedFemale;
static Surface headSurface;
static Model replacementNeck;
public static int CachedModels => fittedModels.Count;
public static void ClearCache()
{
fittedModels.Clear();
fittingFailures.Clear();
replacementNeck = null;
headSurface = null;
}
/// <summary>Keep the lower neck when clothing replaces the head bodygroup.</summary>
public static Model ReplacementNeck()
{
if (replacementNeck is not null) return replacementNeck;
References();
return replacementNeck = Build(Model.Load(UnifiedHuman.ModelPath), false, neckOnly: true);
}
public static bool IsFitted(Model model) => model?.Name.StartsWith("automatic-human-fit:", StringComparison.Ordinal) == true;
public static Model Fit(Model source, bool female, bool fitFace = true)
{
if (source is null || source.IsError || IsFitted(source) || source.BoneCount == 0 || source.Bounds.Mins.z > 60)
return source;
if (fittedModels.TryGetValue((source, female, fitFace), out var ready))
return ready;
if (fittingFailures.TryGetValue((source, female, fitFace), out var problem))
throw new InvalidOperationException(problem);
try
{
References();
var fitted = Build(source, female, fitFace: fitFace);
fittedModels[(source, female, fitFace)] = fitted;
return fitted;
}
catch (Exception error)
{
var message = "Cannot automatically fit " + source.Name + ": " + error.Message;
fittingFailures[(source, female, fitFace)] = message;
throw new InvalidOperationException(message, error);
}
}
static void References()
{
if (unifiedMale is not null)
return;
using var stream = new MemoryStream(FileSystem.Mounted.ReadAllBytes(ReferencePath).ToArray());
using var r = new BinaryReader(stream);
if (r.ReadInt32() != 1)
throw new InvalidOperationException("Unsupported human fitting reference.");
originalMale = ReadSurface(r);
unifiedMale = ReadSurface(r);
unifiedFemale = new(unifiedMale.Female, unifiedMale.Female, unifiedMale.Triangles, unifiedMale.Names, unifiedMale.Weights);
}
static Surface HeadSurface()
{
if (headSurface is not null) return headSurface;
var source = Model.Load(UnifiedHuman.ModelPath);
var buffers = ReadBuffers(source);
var points = new List<Vector3>();
var triangles = new List<int>();
for (int mi = 0; mi < buffers.Count; mi++)
{
var info = source.MeshInfo.Meshes[mi];
if (!info.Name.StartsWith("head", StringComparison.OrdinalIgnoreCase)) continue;
var buffer = buffers[mi];
int callIndex = 0;
foreach (var call in info.DrawCalls)
{
int vb = buffer.Vertices.Length == 1 ? 0 : callIndex;
int ib = buffer.Indices.Length == 1 ? 0 : callIndex;
var vertices = Vertices(buffer.Vertices[vb], null, buffer.Bones.Length);
var indices = Indices(buffer.Indices[ib]);
int offset = points.Count;
points.AddRange(vertices.Select(v => v.Position));
triangles.AddRange(indices.Skip(call.StartIndex).Take(call.Indices).Select(i => i + call.BaseVertex + offset));
callIndex++;
}
}
var p = points.ToArray();
return headSurface = new(p, p, triangles.ToArray(), Array.Empty<string>(), Array.Empty<(int, float)[]>());
}
static Vector3 V(BinaryReader r) => new(r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
static Surface ReadSurface(BinaryReader r)
{
var names = new string[r.ReadInt32()];
for (int i = 0; i < names.Length; i++)
names[i] = Encoding.UTF8.GetString(r.ReadBytes(r.ReadInt32()));
var count = r.ReadInt32();
var triangles = new int[r.ReadInt32() * 3];
var p = new Vector3[count];
var f = new Vector3[count];
var weights = new (int, float)[count][];
for (int i = 0; i < count; i++)
{
p[i] = V(r);
f[i] = V(r);
weights[i] = new (int, float)[r.ReadInt32()];
for (int j = 0; j < weights[i].Length; j++)
weights[i][j] = (r.ReadInt32(), r.ReadSingle());
}
for (int i = 0; i < triangles.Length; i++)
triangles[i] = r.ReadInt32();
return new(p, f, triangles, names, weights);
}
[StructLayout(LayoutKind.Sequential)]
struct SkinVertex
{
[VertexLayout.Position]
public Vector3 Position;
[VertexLayout.Normal]
public Vector3 Normal;
[VertexLayout.Tangent]
public Vector4 Tangent;
[VertexLayout.TexCoord(0)]
public Vector4 UV;
[VertexLayout.TexCoord(1)]
public Vector4 UV2;
[VertexLayout.Color]
public Color32 Color;
[VertexLayout.BlendIndices]
public Color32 Indices;
[VertexLayout.BlendWeight]
public Color32 Weights;
}
readonly record struct Attribute(string Name, int Index, int Format, int Offset);
sealed record Buffer(int Count, int Stride, Attribute[] Attributes, byte[] Data);
sealed record Buffers(Buffer[] Vertices, Buffer[] Indices, string[] Bones);
// SaveToVmdl emits unpacked MBUF data, even when the installed resource uses
// compressed buffers. Reading that public serialization retains authored weights.
static List<Buffers> ReadBuffers(Model source)
{
var data = source.SaveToVmdl();
using var r = new BinaryReader(new MemoryStream(data));
r.BaseStream.Position = 8;
var table = 8 + r.ReadInt32();
var blocks = r.ReadInt32();
var result = new List<Buffers>();
for (int i = 0; i < blocks; i++)
{
r.BaseStream.Position = table + i * 12;
var tag = Encoding.ASCII.GetString(r.ReadBytes(4));
var anchor = (int)r.BaseStream.Position;
var offset = r.ReadInt32();
var size = r.ReadInt32();
if (tag == "MDAT")
{
r.BaseStream.Position = anchor + offset;
var bones = SerializedMeshBones.Read(r.ReadBytes(size));
result[^1] = result[^1] with
{
Bones = bones
};
continue;
}
if (tag != "MBUF")
continue;
var start = anchor + offset;
r.BaseStream.Position = start;
var vo = r.ReadInt32();
var vc = r.ReadInt32();
var io = r.ReadInt32();
var ic = r.ReadInt32();
result.Add(new(Enumerable.Range(0, vc).Select(j => ReadBuffer(r, start + vo + j * 24)).ToArray(), Enumerable.Range(0, ic).Select(j => ReadBuffer(r, start + 8 + io + j * 24)).ToArray(), null));
}
if (result.Count != source.MeshCount)
throw new InvalidOperationException($"{source.Name}: missing serialized mesh buffers.");
return result;
}
static Buffer ReadBuffer(BinaryReader r, int position)
{
r.BaseStream.Position = position;
var count = r.ReadInt32();
var stride = r.ReadInt32() & 0x3ffffff;
var ap = (int)r.BaseStream.Position;
var ao = r.ReadInt32();
var ac = r.ReadInt32();
var dp = (int)r.BaseStream.Position;
var offset = r.ReadInt32();
var bytes = r.ReadInt32();
if (count < 0 || count > 2000000 || stride < 1 || stride > 1024 || bytes != count * stride)
throw new InvalidOperationException("Unexpected serialized human clothing buffer.");
var attributes = new Attribute[ac];
for (int i = 0; i < ac; i++)
{
r.BaseStream.Position = ap + ao + i * 56;
var name = Encoding.ASCII.GetString(r.ReadBytes(32)).TrimEnd('\0');
attributes[i] = new(name, r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
}
r.BaseStream.Position = dp + offset;
return new(count, stride, attributes, r.ReadBytes(bytes));
}
static float F(byte[] b, int p) => BitConverter.ToSingle(b, p);
static ushort U16(byte[] b, int p) => BitConverter.ToUInt16(b, p);
static uint U32(byte[] b, int p) => BitConverter.ToUInt32(b, p);
static Color32 Bytes(byte[] b, int p) => new(b[p], b[p + 1], b[p + 2], b[p + 3]);
static Vector4 Values(byte[] b, int p, int format)
{
float HalfAt(int i) => (float)BitConverter.UInt16BitsToHalf(U16(b, p + i * 2));
return format switch
{
2 => new(F(b, p), F(b, p + 4), F(b, p + 8), F(b, p + 12)),
6 => new(F(b, p), F(b, p + 4), F(b, p + 8), 0),
16 => new(F(b, p), F(b, p + 4), 0, 0),
10 => new(HalfAt(0), HalfAt(1), HalfAt(2), HalfAt(3)),
34 => new(HalfAt(0), HalfAt(1), 0, 0),
28 => new(b[p] / 255f, b[p + 1] / 255f, b[p + 2] / 255f, b[p + 3] / 255f),
_ => throw new InvalidOperationException("Unsupported clothing vertex format " + format)};
}
static SkinVertex[] Vertices(Buffer b, Vertex[] native, int boneCount)
{
var vertices = new SkinVertex[b.Count];
for (int i = 0; i < b.Count; i++)
{
var v = new SkinVertex
{
Color = new(255, 255, 255, 255),
Weights = new(255, 0, 0, 0)
};
foreach (var a in b.Attributes)
{
var p = i * b.Stride + a.Offset;
switch (a.Name)
{
case "POSITION":
var xyz = Values(b.Data, p, a.Format);
v.Position = new(xyz.x, xyz.y, xyz.z);
break;
case "TEXCOORD":
if (a.Index == 0)
v.UV = Values(b.Data, p, a.Format);
else if (a.Index == 1)
v.UV2 = Values(b.Data, p, a.Format);
break;
case "BLENDINDICES":
if (a.Format == 30)
v.Indices = Bytes(b.Data, p);
else if (a.Format == 12)
{
var ids = Enumerable.Range(0, 4).Select(j => (int)U16(b.Data, p + j * 2)).ToArray();
if (ids.Any(j => j >= boneCount || j > 255))
throw new InvalidOperationException("Clothing bone index exceeds fitting layout.");
v.Indices = new((byte)ids[0], (byte)ids[1], (byte)ids[2], (byte)ids[3]);
}
else
throw new InvalidOperationException("Unsupported clothing bone-index format " + a.Format);
break;
case "BLENDWEIGHT":
if (a.Format != 28)
throw new InvalidOperationException("Unsupported clothing weight format " + a.Format);
v.Weights = Bytes(b.Data, p);
break;
case "COLOR":
if (a.Format == 28)
v.Color = Bytes(b.Data, p);
break;
case "NORMAL":
if (a.Format == 6)
{
var n = Values(b.Data, p, a.Format);
v.Normal = new(n.x, n.y, n.z);
}
break;
case "TANGENT":
v.Tangent = Values(b.Data, p, a.Format);
break;
}
}
if (v.Normal.LengthSquared < .01f && native is not null && native.Length == b.Count && (native[i].Position - v.Position).Length < .001f)
{
v.Normal = native[i].Normal;
v.Tangent = native[i].Tangent;
}
if (new[]
{
v.Indices.r,
v.Indices.g,
v.Indices.b,
v.Indices.a
}.Any(j => j >= boneCount))
throw new InvalidOperationException("Serialized garment bone map does not match its skeleton.");
vertices[i] = v;
}
return vertices;
}
static int[] Indices(Buffer b) => Enumerable.Range(0, b.Count).Select(i => b.Stride == 2 ? (int)U16(b.Data, i * 2) : checked((int)U32(b.Data, i * 4))).ToArray();
static Vector3[] Normals(Vector3[] p, int[] indices)
{
var n = new Vector3[p.Length];
for (int i = 0; i + 2 < indices.Length; i += 3)
{
var a = indices[i];
var b = indices[i + 1];
var c = indices[i + 2];
var cross = Vector3.Cross(p[b] - p[a], p[c] - p[a]);
n[a] += cross;
n[b] += cross;
n[c] += cross;
}
return n.Select(v => v.Normal).ToArray();
}
static Vector4[] Tangents(SkinVertex[] vertices, Vector3[] p, Vector3[] normals, int[] indices)
{
var t = new Vector3[p.Length];
var b = new Vector3[p.Length];
for (int i = 0; i + 2 < indices.Length; i += 3)
{
int a = indices[i], c = indices[i + 1], d = indices[i + 2];
var e = p[c] - p[a];
var f = p[d] - p[a];
var uv = vertices[c].UV - vertices[a].UV;
var uw = vertices[d].UV - vertices[a].UV;
float determinant = uv.x * uw.y - uv.y * uw.x;
if (MathF.Abs(determinant) < 1e-8f)
continue;
var tangent = (e * uw.y - f * uv.y) / determinant;
var bitangent = (f * uv.x - e * uw.x) / determinant;
foreach (int id in new[]
{
a,
c,
d
}
)
{
t[id] += tangent;
b[id] += bitangent;
}
}
return Enumerable.Range(0, p.Length).Select(i =>
{
var n = normals[i];
var tangent = (t[i] - n * Vector3.Dot(n, t[i])).Normal;
return new Vector4(tangent, Vector3.Dot(Vector3.Cross(n, tangent), b[i]) < 0 ? -1 : 1);
}).ToArray();
}
static Model Build(Model source, bool female, bool neckOnly = false, bool fitFace = true)
{
var buffers = ReadBuffers(source);
var builder = Model.Builder.WithName("automatic-human-fit:" + source.Name + ":" + (neckOnly ? "replacement-neck" : female ? "female" : "male"));
for (int i = 0; i < source.BoneCount; i++)
{
var transform = source.GetBoneTransform(i);
var parent = source.GetBoneParent(i);
builder.AddBone(source.GetBoneName(i), transform.Position, transform.Rotation, parent < 0 ? null : source.GetBoneName(parent));
}
var native = source.GetVertices();
var baseMaterials = (source.MaterialGroupCount > 0 ? source.GetMaterials(0) : source.Materials).ToArray();
var meshMaterials = new List<Material>();
var work = new List<Action>();
var clothMale = new List<Vector3>();
var clothFemale = new List<Vector3>();
var clothIndices = new List<int>();
var head = neckOnly || !fitFace ? null : HeadSurface();
for (int mi = 0; mi < buffers.Count; mi++)
{
var info = source.MeshInfo.Meshes[mi];
if (neckOnly && !info.Name.StartsWith("head", StringComparison.OrdinalIgnoreCase)) continue;
var buffer = buffers[mi];
var boneMap = buffer.Bones.Select(name => Enumerable.Range(0, source.BoneCount).First(i => source.GetBoneName(i) == name)).ToArray();
int callIndex = 0;
foreach (var call in info.DrawCalls)
{
// SaveToVmdl emits individual buffers in draw-call order when a mesh uses
// separate streams (its metadata currently labels each stream as zero).
int vb = buffer.Vertices.Length == 1 ? 0 : callIndex;
int ib = buffer.Indices.Length == 1 ? 0 : callIndex;
if (vb >= buffer.Vertices.Length || ib >= buffer.Indices.Length)
throw new InvalidOperationException("Unexpected garment draw-call buffer mapping.");
var verts = Vertices(buffer.Vertices[vb], mi == 0 && buffer.Vertices.Length == 1 ? native : null, buffer.Bones.Length);
var indices = Indices(buffer.Indices[ib]);
for (int i = 0; i < verts.Length; i++)
{
var ids = verts[i].Indices;
verts[i].Indices = new((byte)boneMap[ids.r], (byte)boneMap[ids.g], (byte)boneMap[ids.b], (byte)boneMap[ids.a]);
}
callIndex++;
var selectedIndices = indices.Skip(call.StartIndex).Take(call.Indices).Select(i => i + call.BaseVertex).ToArray();
if (neckOnly)
selectedIndices = Enumerable.Range(0, selectedIndices.Length / 3)
.Where(t => Enumerable.Range(0, 3).All(j => verts[selectedIndices[t * 3 + j]].Position.z <= 60))
.SelectMany(t => selectedIndices.Skip(t * 3).Take(3)).ToArray();
if (selectedIndices.Length == 0) continue;
var used = selectedIndices.Distinct().ToArray();
var lookup = used.Select((id, i) => (id, i)).ToDictionary(x => x.id, x => x.i);
var localIndices = selectedIndices.Select(i => lookup[i]).ToArray();
var v = used.Select(i => verts[i]).ToArray();
var original = v.Select(x => x.Position).ToArray();
var male = new Vector3[v.Length];
var target = new Vector3[v.Length];
bool skin = neckOnly || call.Material?.Name.Contains("/bodies/", StringComparison.OrdinalIgnoreCase) == true;
for (int i = 0; i < v.Length; i++)
{
var p = original[i];
if (!skin && !neckOnly && fitFace && p.z >= 57 && p.z <= 73)
{
var hit = head.Nearest(p);
if (hit.Distance < 1.2f && hit.Distance < originalMale.Nearest(p).Distance)
{
var normal = head.FaceNormal(hit.Triangle);
float clearance = Vector3.Dot(p - hit.Point, normal);
// Close face coverings need space for the difference in falloff
// between the skin and fabric under inherited facial deformers.
float margin = .60f * Math.Clamp((62.2f - p.z) / 1.0f, 0, 1);
if (margin > 0) p += normal * Math.Clamp(margin - clearance, 0, .85f);
male[i] = target[i] = p;
continue;
}
}
if (p.z > 60)
{
male[i] = p;
target[i] = p;
continue;
}
var from = (neckOnly ? unifiedMale : female ? unifiedFemale : originalMale).Nearest(p);
var mapped = female || neckOnly ? from : unifiedMale.Nearest(from.Point);
var m = unifiedMale.Interpolate(mapped.Triangle, mapped.Barycentric);
var f = unifiedFemale.Interpolate(mapped.Triangle, mapped.Barycentric);
// Preserve authored clearance, including compressed skin under clothing.
var offset = p - from.Point;
bool snap = skin && offset.Length < .12f;
male[i] = m + (snap ? Vector3.Zero : offset);
target[i] = f + (snap ? Vector3.Zero : offset);
}
int clothOffset = clothMale.Count;
if (!skin)
{
clothIndices.AddRange(localIndices.Select(i => i + clothOffset));
clothMale.AddRange(male);
clothFemale.AddRange(target);
}
work.Add(() =>
{
if (!skin)
{
for (int i = 0; i < v.Length; i++)
{
male[i] = clothMale[clothOffset + i];
target[i] = clothFemale[clothOffset + i];
}
}
var originalNormals = Normals(original, localIndices);
var maleNormals = Normals(male, localIndices);
var femaleNormals = Normals(target, localIndices);
var tangents = Tangents(v, male, maleNormals, localIndices);
var morph = new MorphDelta[v.Length];
for (int i = 0; i < v.Length; i++)
{
if (v[i].Normal.LengthSquared < .01f)
v[i].Normal = originalNormals[i];
v[i].Normal = (v[i].Normal + maleNormals[i] - originalNormals[i]).Normal;
v[i].Tangent = tangents[i];
v[i].Position = male[i];
morph[i] = new(i, target[i] - male[i], femaleNormals[i] - maleNormals[i]);
}
var mesh = new Mesh(call.Material, (MeshPrimitiveType)call.PrimitiveType);
mesh.UvDensity = call.UvDensity;
mesh.CreateVertexBuffer(v.Length, v.ToList());
mesh.CreateIndexBuffer(localIndices.Length, localIndices.ToList());
mesh.AddMorph(Morph, morph);
mesh.Bounds = new BBox(male.Concat(target).Aggregate((a, b) => Vector3.Min(a, b)), male.Concat(target).Aggregate((a, b) => Vector3.Max(a, b)));
var lods = info.LodMask;
if (source.MeshInfo.LodCount == 0)
builder.AddMesh(mesh);
else
for (int lod = 0; lod < source.MeshInfo.LodCount; lod++)
if ((lods & (1 << lod)) != 0)
builder.AddMesh(mesh, lod);
meshMaterials.Add(call.Material);
});
}
}
if (clothMale.Count > 0)
{
var m = clothMale.ToArray();
var f = clothFemale.ToArray();
var t = clothIndices.ToArray();
ClearTriangles(unifiedMale, m, t);
ClearTriangles(unifiedFemale, f, t);
clothMale = m.ToList();
clothFemale = f.ToList();
}
foreach (var build in work)
build();
for (int group = 0; group < source.MaterialGroupCount; group++)
{
var materials = source.GetMaterials(group).ToArray();
var g = builder.AddMaterialGroup(source.GetMaterialGroupName(group));
foreach (var material in meshMaterials)
{
var index = Array.IndexOf(baseMaterials, material);
g.AddMaterial(index >= 0 && index < materials.Length ? materials[index] : material);
}
}
for (int lod = 1; lod < source.MeshInfo.LodSwitchDistances.Length; lod++)
builder.WithLodDistance(lod, source.MeshInfo.LodSwitchDistances[lod]);
return builder.Create();
}
static void ClearTriangles(Surface body, Vector3[] points, int[] triangles)
{
// A smooth spatial clearance field moves folded/layered fabric together.
// Local per-triangle pushes can pull an inner layer through its outer flap.
var source = points.ToArray();
var min = points.Aggregate((a, b) => Vector3.Min(a, b)) - Vector3.One * 1.2f;
var max = points.Aggregate((a, b) => Vector3.Max(a, b)) + Vector3.One * 1.2f;
var candidates = body.Points.Where(p => p.x >= min.x && p.x <= max.x && p.y >= min.y && p.y <= max.y && p.z >= min.z && p.z <= max.z).ToArray();
var pushes = new List<(Vector3 Point, Vector3 Push)>();
var fabric = new Surface(points, points, triangles, Array.Empty<string>(), Array.Empty<(int, float)[]>());
foreach (var p in candidates)
{
var hit = fabric.Nearest(p);
if (hit.Distance > 1.2f)
continue;
var w = hit.Barycentric;
if (MathF.Min(w.x, MathF.Min(w.y, w.z)) < .015f)
continue;
var n = body.FaceNormal(body.Nearest(p).Triangle);
if (MathF.Abs(Vector3.Dot(n, fabric.FaceNormal(hit.Triangle))) < .25f)
continue;
var penetration = Vector3.Dot(p - hit.Point, n) + .14f;
if (penetration <= 0 || penetration > 1.2f)
continue;
pushes.Add((hit.Point, n * MathF.Min(.7f, penetration + .04f)));
}
// Spatial buckets bound the search and apply identical displacement to UV
// duplicates, overlapping material sections, and neighbouring LOD vertices.
const float radius = 2.5f;
(int, int, int) Cell(Vector3 p) => ((int)MathF.Floor(p.x / radius), (int)MathF.Floor(p.y / radius), (int)MathF.Floor(p.z / radius));
var buckets = pushes.GroupBy(p => Cell(p.Point)).ToDictionary(g => g.Key, g => g.ToArray());
for (int i = 0; i < points.Length; i++)
{
var cell = Cell(source[i]);
Vector3 best = default;
for (int x = -1; x <= 1; x++)
for (int y = -1; y <= 1; y++)
for (int z = -1; z <= 1; z++)
{
if (!buckets.TryGetValue((cell.Item1 + x, cell.Item2 + y, cell.Item3 + z), out var values))
continue;
foreach (var p in values)
{
var d = (source[i] - p.Point).Length / radius;
if (d >= 1)
continue;
var falloff = 1 - d * d * (3 - 2 * d);
var push = p.Push * falloff;
if (push.LengthSquared > best.LengthSquared)
best = push;
}
}
points[i] += best;
}
}
readonly record struct Hit(int Triangle, Vector3 Barycentric, Vector3 Point, float Distance);
sealed class Surface
{
public readonly Vector3[] Points, Female;
public readonly int[] Triangles;
public readonly string[] Names;
public readonly (int, float)[][] Weights;
readonly Node tree;
sealed class Node
{
public Vector3 Min, Max;
public Node Left, Right;
public int[] Faces;
}
public Surface(Vector3[] p, Vector3[] f, int[] t, string[] names, (int, float)[][] weights)
{
Points = p;
Female = f;
Triangles = t;
Names = names;
Weights = weights;
tree = BuildTree(Enumerable.Range(0, t.Length / 3).ToArray());
}
Vector3 A(int t, int corner) => Points[Triangles[t * 3 + corner]];
Node BuildTree(int[] faces)
{
var p = faces.SelectMany(i => new[] { A(i, 0), A(i, 1), A(i, 2) }).ToArray();
var node = new Node
{
Min = p.Aggregate((a, b) => Vector3.Min(a, b)),
Max = p.Aggregate((a, b) => Vector3.Max(a, b))
};
if (faces.Length <= 12)
{
node.Faces = faces;
return node;
}
var extent = node.Max - node.Min;
int axis = extent.x > extent.y ? (extent.x > extent.z ? 0 : 2) : (extent.y > extent.z ? 1 : 2);
float Coord(Vector3 v) => axis == 0 ? v.x : axis == 1 ? v.y : v.z;
var sorted = faces.OrderBy(i => Coord(A(i, 0) + A(i, 1) + A(i, 2))).ToArray();
int half = sorted.Length / 2;
node.Left = BuildTree(sorted.Take(half).ToArray());
node.Right = BuildTree(sorted.Skip(half).ToArray());
return node;
}
public Vector3 Interpolate(int t, Vector3 w) => A(t, 0) * w.x + A(t, 1) * w.y + A(t, 2) * w.z;
public Vector3 FaceNormal(int t) => Vector3.Cross(A(t, 1) - A(t, 0), A(t, 2) - A(t, 0)).Normal;
static float Distance(Node n, Vector3 p) => (p - Vector3.Max(n.Min, Vector3.Min(n.Max, p))).LengthSquared;
public Hit Nearest(Vector3 p)
{
float best = float.MaxValue;
int face = 0;
Vector3 bary = default, point = default;
void Search(Node node)
{
if (Distance(node, p) > best)
return;
if (node.Faces is null)
{
var a = node.Left;
var b = node.Right;
if (Distance(a, p) > Distance(b, p))
(a, b) = (b, a);
Search(a);
Search(b);
return;
}
foreach (var t in node.Faces)
{
var w = Closest(p, A(t, 0), A(t, 1), A(t, 2));
var q = Interpolate(t, w);
var distance = (p - q).LengthSquared;
if (distance < best)
{
best = distance;
face = t;
bary = w;
point = q;
}
}
}
Search(tree);
return new(face, bary, point, MathF.Sqrt(best));
}
static Vector3 Closest(Vector3 p, Vector3 a, Vector3 b, Vector3 c)
{
var ab = b - a;
var ac = c - a;
var ap = p - a;
float d1 = Vector3.Dot(ab, ap), d2 = Vector3.Dot(ac, ap);
if (d1 <= 0 && d2 <= 0)
return new(1, 0, 0);
var bp = p - b;
float d3 = Vector3.Dot(ab, bp), d4 = Vector3.Dot(ac, bp);
if (d3 >= 0 && d4 <= d3)
return new(0, 1, 0);
float vc = d1 * d4 - d3 * d2;
if (vc <= 0 && d1 >= 0 && d3 <= 0)
{
var v = d1 / (d1 - d3);
return new(1 - v, v, 0);
}
var cp = p - c;
float d5 = Vector3.Dot(ab, cp), d6 = Vector3.Dot(ac, cp);
if (d6 >= 0 && d5 <= d6)
return new(0, 0, 1);
float vb = d5 * d2 - d1 * d6;
if (vb <= 0 && d2 >= 0 && d6 <= 0)
{
var w = d2 / (d2 - d6);
return new(1 - w, 0, w);
}
float va = d3 * d6 - d5 * d4;
if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0)
{
var w = (d4 - d3) / (d4 - d3 + d5 - d6);
return new(0, 1 - w, w);
}
float sum = va + vb + vc;
if (MathF.Abs(sum) < 1e-12f)
return new(1, 0, 0);
return new(va / sum, vb / sum, vc / sum);
}
}
}// Runtime fitting integration.
Game
library
using System;
namespace CharacterEditor;
public static class CharacterAppearance
{
public static string ValidateForApply(CharacterAppearanceData data)
{
if (CharacterAppearanceData.Validate(data)is string error)
return error;
var skin = ClothingResources.Get(UnifiedHuman.PresentationSkin(data));
if (skin?.HasHumanSkin != true)
return "The selected native skin resource is unavailable.";
var body = Model.Load(data.BaseShape is null ? HumanMarkingModels.ModelPath(skin.HumanSkinModel, data.Makeup) : UnifiedHuman.ModelPath);
if (!body.IsValid || body.IsError)
return "The selected native human model is unavailable.";
var clothes = new List<Clothing>
{
skin
};
foreach (var path in new[]
{
data.HairPath,
data.FacialHairPath
}.Where(x => !string.IsNullOrEmpty(x)))
{
var clothing = ClothingResources.Get(path);
if (clothing is null)
return "A selected native hairstyle resource is unavailable.";
if (path == data.HairPath && !ClothingResources.IsHair(clothing) || path == data.FacialHairPath && !ClothingResources.IsFacialHair(clothing))
return "The selected item does not belong in this hair slot.";
clothes.Add(clothing);
}
var tags = new TagSet(skin.HumanSkinTags.TryGetAll());
tags.Add("human");
foreach (var clothing in clothes.Skip(1))
{
var path = clothing.GetModel(clothes, tags);
if (string.IsNullOrEmpty(path))
return "A selected hairstyle does not support this native human body.";
var model = Model.Load(path);
if (!model.IsValid || model.IsError)
return "A selected native hairstyle model is unavailable.";
}
return null;
}
}
Game
library
@using Sandbox
@using Sandbox.UI
@using System
@using System.Text.Json
@using System.Threading.Tasks
@namespace CharacterEditor
@inherits Panel
<root class="interactive creatoroverlay">
@if(true)
{
<div class="creatorwindow">
<div class="creatorheader"><div><div class="creatorbreadcrumb"><span>@ProductLabel</span><span class="material-icons">chevron_right</span><span>@LocationLabel</span></div><div class="creatortitle">@Title</div></div><button class="creatorbutton quiet cancelcreation" onclick=@Back><span class="material-icons">arrow_back</span>Back</button></div>
<div class="creatorbody">
<div class="creatorcontrols">
<div class="creatortabs">@foreach(var tab in Tabs){<button class=@($"creatorbutton creatortab {(Tab==tab?"chosen":"")}") onclick=@(()=>ChangeTab(tab))>@tab</button>}</div>
<div class=@($"creatoroptions {(Tab=="Voice"?"voiceoptions":"")}")>
<ReferenceFacePanel style=@(Tab=="Photos"?"display: flex;":"display: none;") Appearance=@Appearance Generated=@AddPhotoFace />
@if(Tab=="Presets")
{
<div class="optionheading">Choose a starting look</div><div class="optiondescription">Start with a preset, then make it your own.</div><div class="presetgrid">
@foreach(var preset in AppearanceCatalog.Presets){<button class=@($"creatorbutton presetchoice {(ChosenPreset==preset.Id?"chosen":"")}") onclick=@(()=>ApplyPreset(preset.Id))><div class="presetportrait"><CharacterCreatorPreview [email protected] Portrait=@true /></div><div class="presetlabel">@preset.Label</div></button>}
</div>
}
else if(Tab=="Face & body")
{
@if(Appearance.BaseShape is not null)
{
<div class="optionheading">Blend your base face</div><div class="optiondescription">Overall sets the whole face. In regional tabs, blend that result with individual faces for that area. Refine the result in Shape; choose textures in Skin.</div>
<div class="blendtabs">@foreach(var region in new[]{"overall"}.Concat(FaceRegions.Ids)){<button class=@($"creatorbutton blendtab blend-tab-{region} {(BlendRegion==region?"chosen":"")}") onclick=@(()=>BlendRegion=region)>@FaceRegions.Label(region)</button>}</div>
@if(BlendRegion!="overall")
{
<div class="regionstatus"><span>Mix the live Overall blend with individual faces. Relative weights determine each share.</span><button class="creatorbutton quiet reset-region" onclick=@ResetRegion>Reset region</button></div>
<div class="overallblendrow"><div class="shapelabel"><span>Overall blend</span><span>@($"{MathF.Round(OverallWeight*100)} influence · {MathF.Round(FaceRegions.OverallShare(Appearance.BaseShape,BlendRegion)*100)}% of region")</span></div><SliderControl class="shapeslider region-overall-weight" Value=@OverallWeight OnValueChanged=@SetOverallWeight Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /><div class="optiondescription">Follows changes made in the Overall tab.</div></div>
}
@for(var i=0;i<7;i++)
{
var index=i;
<div class="shaperow"><div class="shapelabel"><span>Face @(index+1)</span><span>@BlendWeightLabel(index)</span></div><SliderControl class=@($"shapeslider head-weight-{index+1}") Value=@HeadWeight(index) OnValueChanged=@((float value)=>SetHeadWeight(index,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
}
@foreach(var face in Appearance.BaseShape.PhotoFaces)
{
<div class="photoblendcard">
<div class="photoblendheader"><span class="photoblendname">@face.Name</span><button class="creatorbutton quiet delete-photo-face" tooltip="Delete this custom face from the character and all regions" onclick=@(()=>FaceRegions.RemovePhoto(Appearance.BaseShape,face))><span class="material-icons">delete_outline</span></button></div>
<div class="shapelabel"><span>Blend influence</span><span>@($"{MathF.Round(PhotoWeight(face)*100)} influence · {MathF.Round(FaceRegions.PhotoEntryShare(Appearance.BaseShape,face,BlendRegion)*100)}% of blend")</span></div>
<SliderControl class="shapeslider photo-face-weight" Value=@PhotoWeight(face) OnValueChanged=@((float value)=>SetPhotoWeight(face,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false />
@if(BlendRegion=="overall")
{
<div class="shapelabel"><span>Photo detail strength</span><span>@MathF.Round(face.Strength*100)%</span></div>
<SliderControl class="shapeslider photo-face-strength" [email protected] OnValueChanged=@((float value)=>{face.Strength=Math.Clamp(value,0,2);}) Min=@(0f) Max=@(2f) Step=@(.01f) ShowTextEntry=@false />
<div class="optiondescription">100% is the fitted shape. Higher values exaggerate its slider changes, within normal Shape limits. This also affects regions using this face.</div>
}
</div>
}
@if(Appearance.BaseShape.TotalInfluence==0){<div class="optiondescription">All influences are zero: using Face 3 as the neutral base.</div>}
<div class="optionheading">Base body</div><div class="shapelabel"><span>@MathF.Round((1-Appearance.BaseShape.MaleBody)*100)% female</span><span>@MathF.Round(Appearance.BaseShape.MaleBody*100)% male</span></div><SliderControl class="shapeslider base-body" [email protected] OnValueChanged=@SetBodyBlend Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false />
@foreach(var variant in new[]{"male","female"})
{
<button class=@($"creatorbutton quiet clothing-prefer-{variant}") role="checkbox" aria-checked=@(Appearance.BaseShape.ClothingVariant==variant?"true":"false") onclick=@(()=>ToggleClothingVariant(variant))><span class="material-icons">@(Appearance.BaseShape.ClothingVariant==variant?"check_box":"check_box_outline_blank")</span>Prefer @variant clothing version</button>
}
<div class="optiondescription">Neither selected: use the body blend. Available versions still fit your body shape.</div>
<div class="optiondescription">Body blend is independent of face and skin. Breast adjustments fade with the female amount; bulge adjustments fade with the male amount.</div>
}
else
{
<div class="optionheading">Native base</div><div class="optiondescription">This saved character uses its original native face and body.</div><button class="creatorbutton" onclick=@EnableBlending>Enable weighted base blending</button>
<div class="bodychoices"><button class="creatorbutton" onclick=@(()=>ChooseBody("male"))>Masculine</button><button class="creatorbutton" onclick=@(()=>ChooseBody("female"))>Feminine</button></div><div class="skingrid">@foreach(var skin in AppearanceCatalog.Skins.Where(x=>x.BodyType==BodyType)){<button class="creatorbutton skinchoice" onclick=@(()=>ChooseSkin(skin))>@FaceLabel(skin)</button>}</div>
}
<div class="optionheading">Eye color</div><div class="optiondescription">Color the iris while keeping the pupil and whites of the eyes.</div>
<div class="colorchoices"><button class=@($"creatorbutton colorchoice eyecolorchoice {(!Appearance.CustomEyeColor?"chosen":"")}") onclick=@(()=>ChooseEyeColor(null))>Native</button>@foreach(var color in EyeColors){<button class=@($"creatorbutton colorchoice eyecolorchoice {(Appearance.CustomEyeColor && Appearance.EyeColor==color.Color?"chosen":"")}") onclick=@(()=>ChooseEyeColor(color.Color))><div class="colorswatch" style=@($"background-color: {ColorCss(color.Color)};")></div><span>@color.Label</span></button>}</div>
<button class="creatorbutton quiet eyecustomtoggle" onclick=@(()=>ChooseEyeColor(Appearance.EyeColor))><span class="material-icons">palette</span>Custom eye color</button>
@if(Appearance.CustomEyeColor){@foreach(var channel in new[]{"Red","Green","Blue"}){<div class="skinchannel"><div class="shapelabel">@channel</div><SliderControl class=@($"shapeslider eye-{channel.ToLowerInvariant()}") Value=@EyeChannel(channel) OnValueChanged=@((float value)=>SetEyeChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}}
}
else if(Tab=="Skin")
{
@if(Appearance.BaseShape is not null){<div class="optionheading">Skin details</div><div class="optiondescription">Choose the native texture set: pigment, pores, freckles and other details. This does not change your face weights or body shape.</div><div class="skingrid">@foreach(var skin in AppearanceCatalog.Skins){<button class=@($"creatorbutton skinchoice {(Appearance.SkinPath==skin.ResourcePath?"chosen":"")}") onclick=@(()=>ChooseSkin(skin))>Skin @skin.Id.Replace("human","")</button>}</div>}
<div class="optionheading">Skin color</div><div class="optiondescription">Choose a complexion independently of face and body. Native restores the selected skin's original color.</div>
<div class="shapecategories"><button class=@($"creatorbutton skinpalette {(SkinPalette=="Human"?"chosen":"")}") onclick=@(()=>SkinPalette="Human")>Human</button><button class=@($"creatorbutton skinpalette {(SkinPalette=="Fantasy"?"chosen":"")}") onclick=@(()=>SkinPalette="Fantasy")>Fantasy & custom</button></div>
<div class="colorchoices">@foreach(var color in SkinPalette=="Human"?AppearanceCatalog.HumanSkinColors:AppearanceCatalog.FantasySkinColors){<button class=@($"creatorbutton colorchoice skincolorchoice skincolor-{color.Id} {(Appearance.SkinColor==color.Color && Appearance.NormalizeSkinTone==color.NormalizeSkinTone?"chosen":"")}") onclick=@(()=>ChooseSkinColor(color.Color,color.NormalizeSkinTone)) [email protected]><div class="colorswatch" style=@($"background-color: {ColorCss(color.Color)};")></div><span>@color.Label</span></button>}</div>
@if(SkinPalette=="Fantasy")
{
<div class="fieldlabel colorlabel">Custom color</div><div class="skincolorpreview"><div class="colorswatch" style=@($"background-color: {ColorCss(Appearance.SkinColor)};")></div><span>@($"RGB {MathF.Round(Appearance.SkinColorR*255)} · {MathF.Round(Appearance.SkinColorG*255)} · {MathF.Round(Appearance.SkinColorB*255)}")</span></div>
@foreach(var channel in new[]{"Red","Green","Blue"}){<div class="skinchannel"><div class="shapelabel">@channel</div><SliderControl class=@($"shapeslider skin-{channel.ToLowerInvariant()}") Value=@SkinChannel(channel) OnValueChanged=@((float value)=>SetSkinChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}
}
<button class="creatorbutton quiet resetshape resetskincolor" onclick=@(()=>ChooseSkinColor(Color.White))><span class="material-icons">restart_alt</span>Reset skin color</button>
}
else if(Tab=="Makeup")
{
<div class="optionheading">Makeup & markings</div><div class="optiondescription">Make it yours. Choose None to leave any makeup layer bare.</div>
<div class="shapecategories">@foreach(var layer in MakeupCatalog.Layers){<button class=@($"creatorbutton makeuplayer {(MakeupLayer==layer?"chosen":"")}") onclick=@(()=>ChooseMakeupLayer(layer))>@(layer=="Tattoo"?"Markings":layer)</button>}</div>
@if(MakeupLayer=="Tattoo")
{
<div class="markinglayers"><div class="markinglist">@for(var i=0;i<Appearance.Makeup.Markings.Count;i++){var index=i;var mark=Appearance.Makeup.Markings[i];<button class=@($"creatorbutton markinglayer {(SelectedMarking==i?"chosen":"")}") onclick=@(()=>SelectMarking(index))>@($"{i+1} · {MarkingCatalog.Style(mark.Style).Label}")</button>}</div><button class="creatorbutton addmarking" onclick=@AddMarking tooltip="Add an independent marking layer"><span class="material-icons">add</span>Add marking</button></div>
@if(CurrentMarking is null)
{<div class="markingempty"><span class="material-icons">brush</span><div class="fieldlabel">None</div><div class="optiondescription">Add a marking to choose its design, location and ink. Combine up to six.</div></div>}
else
{
<div class="markingeditorheading"><button class="creatorbutton quiet inspectmarking" onclick=@InspectMarking tooltip="Focus the preview on this marking"><span class="material-icons">center_focus_strong</span>@MarkingCatalog.Region(CurrentMarking.Region).Label</button><button class="creatorbutton quiet deletemarking" onclick=@DeleteMarking tooltip="Delete this marking layer"><span class="material-icons">delete_outline</span>Delete layer</button></div>
<div class="markingsections">@foreach(var section in new[]{"Design","Placement","Ink"}){<button class=@($"creatorbutton markingsection {(MarkingSection==section?"chosen":"")}") onclick=@(()=>MarkingSection=section)>@section</button>}</div>
@if(MarkingSection=="Design")
{
<div class="markingdesigns">@foreach(var style in MarkingCatalog.Styles){<button class=@($"creatorbutton markingstyle style-{style.Id} {(CurrentMarking.Style==style.Id?"chosen":"")}") onclick=@(()=>ChooseMarkingStyle(style.Id))><span class="material-icons">@MarkingSymbol(style.Id)</span><span>@style.Label</span></button>}</div>
<div class="fieldlabel markinglocationlabel">Location</div><div class="markinglocations">@foreach(var region in MarkingCatalog.Locations(CurrentMarking.Style)){<button class=@($"creatorbutton markinglocation location-{region.Id} {(CurrentMarking.Region==region.Id?"chosen":"")}") onclick=@(()=>ChooseMarkingRegion(region.Id))>@region.Label</button>}</div>
}
else if(MarkingSection=="Placement")
{
<div class="optiondescription">@(MarkingCatalog.Style(CurrentMarking.Style).Band?"This band wraps around the selected region. Slide it along the body and adjust its size and barb spacing.":"Move, turn and resize this marking within the selected region.")</div>
<div class="markingplacement">@foreach(var control in MarkingCatalog.Style(CurrentMarking.Style).Band?new[]{"Vertical","Scale","Spacing"}:new[]{"Horizontal","Vertical","Scale","Rotation"}){<div class="skinchannel"><div class="shapelabel">@(control=="Vertical" && MarkingCatalog.Style(CurrentMarking.Style).Band?"Position along region":control=="Scale"?"Size":control=="Spacing"?"Barb spacing":control)</div><SliderControl class=@($"shapeslider marking-{control.ToLowerInvariant()}") Value=@MarkingValue(control) OnValueChanged=@((float value)=>SetMarkingValue(control,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>}</div><button class="creatorbutton quiet resetmarkingplacement" onclick=@ResetMarkingPlacement><span class="material-icons">restart_alt</span>Reset placement</button>
}
}
}
@if(MakeupLayer!="Tattoo" || CurrentMarking is not null && MarkingSection=="Ink")
{
<div class="colorchoices">
@if(MakeupLayer!="Tattoo"){<button class=@($"creatorbutton colorchoice makeupnone {(!CurrentMakeupActive?"chosen":"")}") onclick=@ChooseNoMakeup><span class="material-icons">block</span><span>None</span></button>}
@foreach(var color in MakeupCatalog.Colors(MakeupLayer)){<button class=@($"creatorbutton colorchoice makeupcolor makeup-{color.Id} {(CurrentMakeupActive && CurrentMakeup.Color==color.Color?"chosen":"")}") onclick=@(()=>ChooseMakeupColor(color.Color)) [email protected]><div class="colorswatch" style=@($"background-color: {ColorCss(color.Color)};")></div><span>@color.Label</span></button>}
</div>
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>@(MakeupLayer=="Eyeliner"?"Upper eyelid strength":"Strength")</span><span class="shapevalue">@(MathF.Round(CurrentMakeup.Amount*100))%</span></div><SliderControl class="shapeslider makeupamount" [email protected] OnValueChanged=@SetMakeupAmount Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
@if(MakeupLayer=="Eyeliner")
{
<div class="optiondescription">Upper and lower liner share one color. Wings follow the upper liner strength; inner-corner liner is optional.</div>
@foreach(var part in new[]{"Lower eyelid","Inner corner"})
{
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>@part strength</span><span class="shapevalue">@(MathF.Round(EyelinerStrength(part)*100))%</span></div><SliderControl class=@($"shapeslider eyeliner-{(part=="Lower eyelid"?"lower":"inner")}") Value=@EyelinerStrength(part) OnValueChanged=@((float value)=>SetEyelinerStrength(part,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
}
<div class="fieldlabel colorlabel">Wing</div>
<div class="choicegrid">@foreach(var wing in new[]{"none","wing"}){<button class=@($"creatorbutton eyelinerwing {((wing=="none")== (Appearance.Makeup.EyelinerWing=="none")?"chosen":"")}") onclick=@(()=>ChooseEyelinerWing(wing))>@(wing=="none"?"No wing":"Wing")</button>}</div>
@if(Appearance.Makeup.EyelinerWing!="none")
{
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>Wing strength</span><span class="shapevalue">@(MathF.Round(Appearance.Makeup.EyelinerWingStrength*100))%</span></div><SliderControl class="shapeslider eyeliner-wing" [email protected] OnValueChanged=@((float value)=>SetEyelinerStrength("Wing",value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>Wing tip horizontal</span><span class="shapevalue">@(MathF.Round(Appearance.Makeup.WingTipHorizontal*100))%</span></div><SliderControl class="shapeslider eyeliner-wing-horizontal" [email protected] OnValueChanged=@((float value)=>SetWingTip(false,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>Wing tip vertical</span><span class="shapevalue">@(Appearance.Makeup.WingTipVertical==0?"Level":$"{MathF.Round(MathF.Abs(Appearance.Makeup.WingTipVertical)*100)}% {(Appearance.Makeup.WingTipVertical>0?"up":"down")}")</span></div><SliderControl class="shapeslider eyeliner-wing-vertical" [email protected] OnValueChanged=@((float value)=>SetWingTip(true,value)) Min=@(-1f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>Wing thickness</span><span class="shapevalue">@(MathF.Round(Appearance.Makeup.EyelinerWingThickness*100))%</span></div><SliderControl class="shapeslider eyeliner-wing-thickness" [email protected] OnValueChanged=@SetWingThickness Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
}
}
<button class="creatorbutton quiet makeupcustomtoggle" onclick=@(()=>CustomMakeup=!CustomMakeup)><span class="material-icons">palette</span>@(CustomMakeup?"Hide custom color":"Custom color")</button>
@if(CustomMakeup){<div class="makeupcustomgrid">@foreach(var channel in new[]{"Red","Green","Blue"}){<div class="skinchannel"><div class="shapelabel">@channel</div><SliderControl class=@($"shapeslider makeup-{channel.ToLowerInvariant()}") Value=@MakeupChannel(channel) OnValueChanged=@((float value)=>SetMakeupChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}</div>}
}
<div class="makeupresets"><button class="creatorbutton quiet clearmakeup" onclick=@ClearMakeup>Clear all makeup & markings</button></div>
}
else if(Tab=="Clothing")
{
<div class="optionheading">Your wardrobe</div><div class="optiondescription">Click an item to wear it, click again to remove it. Outfits replace tops and bottoms; jackets and accessories can be layered.</div>
<button class="creatorbutton quiet browsecloud" onclick=@(()=>CloudWardrobeOpen=!CloudWardrobeOpen)>@(CloudWardrobeOpen?"Close cloud browser":"Browse cloud clothing & purchased cosmetics")</button>
@if(CloudWardrobeOpen){<CloudClothingBrowser Selected=@ChooseCloudClothing Appearance=@Appearance />}
<TextEntry class="wardrobesearch" Value=@WardrobeSearch OnTextEdited=@((string value)=>{WardrobeSearch=value;}) Placeholder="Search clothing" />
<div class="shapecategories">@foreach(var category in WardrobeCatalog.Categories){<button class=@($"creatorbutton {(WardrobeCategory==category?"chosen":"")}") onclick=@(()=>WardrobeCategory=category)>@category</button>}</div>
<div class="fieldlabel">Wearing @WardrobeCatalog.Paths(Appearance).Count() items</div><div class="choicegrid">@foreach(var path in WardrobeCatalog.Paths(Appearance).ToArray()){var worn=ClothingResources.Available.FirstOrDefault(x=>x.Path==path);@if(worn is not null){<button class="creatorbutton chosen" onclick=@(()=>ChooseClothing(worn))>@($"{worn.Name} ×")</button>}}</div>
<div class="fieldlabel">@($"{WardrobeCategory} · {VisibleWardrobe.Count()} choices")</div><div class="choicegrid thumbnailgrid">@foreach(var item in VisibleWardrobe){<button class=@($"creatorbutton wardrobechoice {((WardrobeCatalog.Paths(Appearance).Contains(item.Path)||Appearance.HairPath==item.Path||Appearance.FacialHairPath==item.Path)?"chosen":"")}") onclick=@(()=>ChooseClothing(item))><div class="thumbnailframe">@if(ClothingThumbnails.For(item.Path) is { Length: > 0 } icon){<div class="itemthumbnail" style=@($"background-image: url('{icon}');")></div>}else{<span class="material-icons thumbnailplaceholder">block</span>}</div><span class="itemlabel">@item.Name</span></button>}</div>
@if(!VisibleWardrobe.Any()){<div class="optiondescription">No clothing matches your search in this category.</div>}
<div class="choicegrid"><button class="creatorbutton quiet" onclick=@ClearClothing>Remove clothing</button><button class="creatorbutton quiet" onclick=@ResetClothing>Default outfit</button></div>
}
else if(Tab=="Hair")
{
<div class="optionheading">Hair</div><div class="choicegrid thumbnailgrid">@foreach(var hair in AvailableHair){<button class=@($"creatorbutton hairchoice {(Appearance.HairPath==hair.ResourcePath?"chosen":"")}") onclick=@(()=>ChooseHair(hair))><div class="thumbnailframe">@if(ClothingThumbnails.For(hair.ResourcePath) is { Length: > 0 } icon){<div class="itemthumbnail" style=@($"background-image: url('{icon}');")></div>}else{<span class="material-icons thumbnailplaceholder">block</span>}</div><span class="itemlabel">@hair.Label</span></button>}</div><div class="fieldlabel colorlabel">Hair color</div><div class="colorchoices">@foreach(var color in AppearanceCatalog.Colors){<button class=@($"creatorbutton colorchoice {(SameColor(color.Color)?"chosen":"")}") onclick=@(()=>ChooseColor(color)) [email protected]><div class="colorswatch" style=@($"background-color: {ColorCss(color.Color)};")></div><span>@color.Label</span></button>}</div><div class="fieldlabel colorlabel">Facial hair</div><div class="choicegrid thumbnailgrid">@foreach(var facial in AvailableFacialHair.Where(x=>x.BodyType==""||x.BodyType==BodyType)){<button class=@($"creatorbutton facialchoice {(Appearance.FacialHairPath==facial.ResourcePath?"chosen":"")}") onclick=@(()=>ChooseFacial(facial))><div class="thumbnailframe">@if(ClothingThumbnails.For(facial.ResourcePath) is { Length: > 0 } icon){<div class="itemthumbnail" style=@($"background-image: url('{icon}');")></div>}else{<span class="material-icons thumbnailplaceholder">block</span>}</div><span class="itemlabel">@facial.Label</span></button>}</div>
}
else if(Tab=="Shape")
{
<div class="optionheading">Refine your look</div><div class="optiondescription">Neutral is 50%; ear taper and belly start at 0%.</div>
<div class="shapecategories">@foreach(var category in AppearanceCatalog.Sliders.Select(x=>x.Group).Distinct()){<button class=@($"creatorbutton shapecategory {(ShapeCategory==category?"chosen":"")}") onclick=@(()=>ChangeShapeCategory(category))>@category</button>}</div>
@if(ShapeCategory=="Body" && Appearance.BaseShape is not null)
{
@foreach(var breast in new[]{true,false}){<div class="shaperow"><div class="shapelabel"><span>@(breast?"Breast size":"Bulge size")</span><span>@MathF.Round((breast?1-Appearance.BaseShape.MaleBody:Appearance.BaseShape.MaleBody)*100)% effect</span><button class="creatorbutton quiet" onclick=@(()=>SetBodyFeature(breast,.5f))>Reset</button></div><SliderControl class=@($"shapeslider {(breast?"breast":"bulge")}-size") Value=@(breast?Appearance.BaseShape.BreastSize:Appearance.BaseShape.BulgeSize) OnValueChanged=@((float value)=>SetBodyFeature(breast,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>}
}
<div class="shapegrid">@foreach(var slider in AppearanceCatalog.Sliders.Where(x=>x.Group==ShapeCategory && (Appearance.BaseShape is null || x.Id!="bodyfeature")))
{
<div class=@($"shaperow shape-{slider.Id}")><div class="shapelabel"><span>@slider.LabelFor(Appearance)</span><div class="shapeactions"><span class="shapevalue">@(MathF.Round(slider.Get(Appearance)*100))%</span><button class="creatorbutton quiet resetslider" onclick=@(()=>SetShape(slider,slider.Neutral)) tooltip=@($"Reset {slider.LabelFor(Appearance).ToLowerInvariant()} to {MathF.Round(slider.Neutral*100)}%")><span class="material-icons">restart_alt</span></button></div></div><SliderControl class="shapeslider" [email protected](Appearance) OnValueChanged=@((float value)=>SetShape(slider,value)) [email protected] [email protected] Step=@(0.01f) ShowTextEntry=@false />@if(slider.Id=="columella"){<div class="fitnesshint">Width between the nostrils</div>}@if(slider.Id=="philtrum"){<div class="fitnesshint">Width between nose and upper lip</div>}@if(slider.Id=="eartaper"){<div class="fitnesshint">Normal · Pointed</div>}@if(slider.Id=="fitness"){<div class="fitnesshint">Soft · Neutral · Muscular</div>}@if(slider.Id=="nostrils"){<div class="fitnesshint">Width of the nostril wings</div>}@if(slider.Id=="nostrilfullness"){<div class="fitnesshint">Smaller · Neutral · Fuller wings</div>}@if(slider.Id=="noseheight" || slider.Id=="chinheight"){<div class="fitnesshint">Lower · Neutral · Higher</div>}</div>
}</div>
<button class="creatorbutton quiet resetshape" onclick=@ResetShapes>Reset body & face</button>
}
@if(ExtraTabs.Contains(Tab) && ExtraContent is not null){@ExtraContent(this)}
</div>
<div class="creatorutility"><button class="creatorbutton quiet" onclick=@Randomize><span class="material-icons">shuffle</span>Randomize</button><button class="creatorbutton quiet" onclick=@Reset><span class="material-icons">restart_alt</span>Reset appearance</button></div>
</div>
<div class="creatorpresentation"><div class="previewheading"><div class="previewname">@(string.IsNullOrWhiteSpace(CharacterName)?"Your character":CharacterName.Trim())</div><div class="previewcaption">@(PreviewPan.Length>0.01f || MathF.Abs(PreviewPitch)>0.01f?"Free inspection":Zoom>CharacterCreatorPreview.FaceZoom+0.001f?"Face detail":Portrait?"Face preview":"Character preview")</div></div><CharacterCreatorPreview @ref=Preview class="mainpreview" Hero=@true Appearance=@Appearance Portrait=@Portrait Zoom=@Zoom Yaw=@Yaw Pitch=@PreviewPitch PitchChanged=@SetPitch PanOffset=@PreviewPan PanChanged=@SetPan Interactive=@(!LoadingCatalog) OrbitChanged=@SetYaw ZoomChanged=@SetZoom ShowClothing=@ShowClothing /><div class="previewcontrols"><div class="previewviewrow"><button class="creatorbutton quiet rotateleft" onclick=@(()=>Rotate(-30)) tooltip="Turn left"><span class="material-icons">rotate_left</span></button><button class=@($"creatorbutton bodyview {(Zoom==0?"chosen":"")}") onclick=@(()=>SetView(0))>Full body</button><button class=@($"creatorbutton previewzoom {(MathF.Abs(Zoom-CharacterCreatorPreview.FaceZoom)<0.001f?"chosen":"")}") onclick=@(()=>SetView(CharacterCreatorPreview.FaceZoom))>Face view</button><button class="creatorbutton quiet rotateright" onclick=@(()=>Rotate(30)) tooltip="Turn right"><span class="material-icons">rotate_right</span></button></div><button class="creatorbutton quiet clothingtoggle" onclick=@ToggleClothing><span class="material-icons">@(ShowClothing?"visibility_off":"visibility")</span>@(ShowClothing?"Hide clothing":"Show clothing")</button></div><div class="previewzoomrow"><span>Zoom</span><SliderControl class="customzoom" Value=@Zoom OnValueChanged=@SetZoom Min=@(0f) [email protected] Step=@(0.005f) ShowTextEntry=@false /></div><div class="previewrecenter"><button class="creatorbutton quiet recenterpreview" onclick=@RecenterPreview><span class="material-icons">center_focus_strong</span>Recenter preview</button></div><div class="previewhint">Left drag: turn · Right: tilt · Middle: pan · Scroll: zoom</div></div>
</div>
<div class="creatorfooter"><div class="creatoridentity"><div class="identityfields"><div class="namefield"><div class="fieldlabel">Character name</div><TextEntry class="charactername" Value=@CharacterName OnTextEdited=@SetCharacterName MaxLength=@(40) Placeholder="Your character's name" /></div>@FooterContent</div></div><div class="creatorfeedback">@(Feedback??(LoadingCatalog?"Loading character options…":Preview?.Error)??(""))</div><button class="creatorbutton primary startplaythrough" disabled=@Disabled onclick=@Start><span class="material-icons">play_arrow</span>@(SaveLabel)</button></div>
</div>
}
</root>
@code {
/// <summary>Consumer-specific pages; built-in categories remain owned by Character Kit.</summary>
public IReadOnlyList<string> ExtraTabs { get; set; } = Array.Empty<string>();
public IReadOnlyList<string> HiddenTabs { get; set; } = Array.Empty<string>();
public RenderFragment<CharacterCreatorPanel> ExtraContent { get; set; }
public RenderFragment FooterContent { get; set; }
public Action StopPreview { get; set; }
public bool Disabled { get; set; }
public string ProductLabel { get; set; } = "Character Editor";
public string LocationLabel { get; set; } = "Studio";
public string Title { get; set; } = "Create your character";
public CharacterAppearanceData EditableAppearance => Appearance;
public SkinnedModelRenderer PreviewRenderer => Preview?.Body;
public string SelectedTab => Tab;
public string SaveLabel { get; set; } = "Save character";
public Func<string, CharacterAppearanceData, string> Saved { get; set; }
public Action Closed { get; set; }
public CharacterPreset InitialPreset { get; set; }
CharacterPreset loadedPreset;
public CharacterAppearanceData DraftAppearance => Appearance.DeepCopy();
public string DraftCharacterName => CharacterName;
public void LoadDraft(string name, CharacterAppearanceData data)
{
CharacterName = name;
Appearance = data.DeepCopy();
ChosenPreset = null;
Tab = "Face & body";
}
public bool AppearanceReady => CatalogReady && Preview?.Loading != true && CharacterAppearance.ValidateForApply(Appearance)is null;
IReadOnlyList<string> Tabs => new[]
{
"Presets",
"Skin",
"Face & body",
"Photos",
"Hair",
"Clothing",
"Shape",
"Makeup"
}.Where(x => !HiddenTabs.Contains(x)).Concat(ExtraTabs).Distinct().ToArray();
bool CloudWardrobeOpen;
string WardrobeSearch = "", WardrobeCategory = "Tops";
IEnumerable<WardrobeItem> VisibleWardrobe => ClothingResources.Available.Where(x => x.Category == WardrobeCategory && (string.IsNullOrWhiteSpace(WardrobeSearch) || x.Name.Contains(WardrobeSearch, StringComparison.OrdinalIgnoreCase))).OrderBy(x => x.Name);
void ChooseCloudClothing(WardrobeItem item)
{
WardrobeCategory = item.Category;
WardrobeSearch = "";
ChooseClothing(item);
Appearance.Version = 2;
ChosenPreset = null;
Feedback = null;
StateHasChanged();
}
void ChooseClothing(WardrobeItem item)
{
var clothing = ClothingResources.Get(item.Path);
if (clothing is not null && ClothingResources.IsHair(clothing))
Appearance.HairPath = Appearance.HairPath == item.Path || ClothingResources.Get(Appearance.HairPath) == clothing ? "" : item.Path;
else if (clothing is not null && ClothingResources.IsFacialHair(clothing))
Appearance.FacialHairPath = Appearance.FacialHairPath == item.Path || ClothingResources.Get(Appearance.FacialHairPath) == clothing ? "" : item.Path;
else
WardrobeCatalog.Toggle(Appearance, item);
Appearance.Version = 2;
ShowClothing = true;
ChosenPreset = null;
Feedback = null;
}
void ClearClothing()
{
Appearance.ClothingPaths = Array.Empty<string>();
Appearance.Version = 2;
ShowClothing = true;
ChosenPreset = null;
}
void ResetClothing()
{
Appearance.ClothingPaths = WardrobeCatalog.DefaultPaths.ToArray();
Appearance.Version = 2;
ShowClothing = true;
ChosenPreset = null;
}
CharacterAppearanceData Appearance = AppearanceCatalog.CreateDefault();
string CharacterName = "Rowan", PlaythroughName = "A new adventure", Tab = "Presets", ShapeCategory = "Body", ChosenPreset = "traveller", Feedback;
string BlendRegion = "overall";
bool CatalogReady, LoadingCatalog;
string SkinPalette = "Human";
string MakeupLayer = "Lipstick";
bool CustomMakeup;
int SelectedMarking;
string MarkingSection = "Design";
CharacterMarkingData CurrentMarking => Appearance.Makeup.Markings.Count == 0 ? null : Appearance.Makeup.Markings[Math.Clamp(SelectedMarking, 0, Appearance.Makeup.Markings.Count - 1)];
MakeupLayerData CurrentMakeup => MakeupLayer == "Tattoo" ? CurrentMarking?.Ink ?? Appearance.Makeup.Tattoo : MakeupCatalog.Layer(Appearance.Makeup, MakeupLayer);
void ChooseMakeupLayer(string layer)
{
if (LoadingCatalog)
return;
MakeupLayer = layer;
if (layer == "Tattoo")
Appearance.Makeup.EditMarkings();
}
void SelectMarking(int index)
{
if (LoadingCatalog || index < 0 || index >= Appearance.Makeup.Markings.Count)
return;
SelectedMarking = index;
}
void AddMarking()
{
if (LoadingCatalog || Appearance.Makeup.Markings.Count >= MarkingCatalog.Maximum)
{
Feedback = "You can combine up to six marking layers.";
return;
}
var marking = new CharacterMarkingData();
MarkingCatalog.ResetPosition(marking);
Appearance.Makeup.Markings.Add(marking);
SelectedMarking = Appearance.Makeup.Markings.Count - 1;
MarkingSection = "Design";
ChangedCosmetics();
}
void DeleteMarking()
{
if (LoadingCatalog || CurrentMarking is null)
return;
Appearance.Makeup.Markings.Remove(CurrentMarking);
SelectedMarking = Math.Clamp(SelectedMarking, 0, Math.Max(0, Appearance.Makeup.Markings.Count - 1));
ChangedCosmetics();
}
string MarkingSymbol(string style) => style switch
{
"crescent" => "brightness_2",
"teardrop" => "water_drop",
"star" => "star_outline",
"rune" => "gesture",
"barbed-band" => "link",
_ => "block"
};
void ChooseMarkingStyle(string style)
{
if (LoadingCatalog || CurrentMarking is null)
return;
MarkingCatalog.ChangeStyle(CurrentMarking, style);
ChangedCosmetics();
}
void ChooseMarkingRegion(string region)
{
if (LoadingCatalog || CurrentMarking is null)
return;
MarkingCatalog.ChangeRegion(CurrentMarking, region);
ChangedCosmetics();
}
float MarkingValue(string control) => control switch
{
"Horizontal" => CurrentMarking.Horizontal,
"Vertical" => CurrentMarking.Vertical,
"Scale" => CurrentMarking.Scale,
"Spacing" => CurrentMarking.Spacing,
_ => CurrentMarking.Rotation
};
void SetMarkingValue(string control, float value)
{
if (LoadingCatalog || CurrentMarking is null || !float.IsFinite(value))
return;
value = Math.Clamp(value, 0, 1);
if (MarkingCatalog.Style(CurrentMarking.Style).Band && control is not "Vertical" and not "Scale" and not "Spacing")
return;
if (control == "Horizontal")
CurrentMarking.Horizontal = value;
else if (control == "Vertical")
CurrentMarking.Vertical = value;
else if (control == "Scale")
CurrentMarking.Scale = value;
else if (control == "Spacing")
CurrentMarking.Spacing = value;
else if (control == "Rotation")
CurrentMarking.Rotation = value;
ChangedCosmetics();
}
void ResetMarkingPlacement()
{
if (LoadingCatalog || CurrentMarking is null)
return;
MarkingCatalog.ResetPosition(CurrentMarking);
CurrentMarking.Scale = .5f;
CurrentMarking.Rotation = .5f;
CurrentMarking.Spacing = .5f;
ChangedCosmetics();
}
void InspectMarking()
{
if (LoadingCatalog || CurrentMarking is null)
return;
var region = MarkingCatalog.Region(CurrentMarking.Region);
var center = MarkingCatalog.Focus(CurrentMarking);
var outward = MarkingCatalog.Outward(CurrentMarking);
Yaw = -MathF.Atan2(outward.y, outward.x) * 180 / MathF.PI;
SetZoom(region.Head ? 1.55f : 1.15f);
var rotated = Rotation.FromYaw(Yaw) * center;
var targetHeight = 62 + 1.5f * MathF.Max(0, Zoom - 1);
PreviewPan = new(rotated.x, rotated.y, rotated.z - targetHeight);
PreviewPitch = 0;
}
void ChangedCosmetics()
{
ChosenPreset = null;
Feedback = null;
}
void ChooseNoMakeup()
{
if (LoadingCatalog)
return;
CurrentMakeup.Amount = 0;
if (MakeupLayer == "Eyeliner") MakeupCatalog.Reset(Appearance.Makeup, "Eyeliner");
ChangedCosmetics();
}
void ChooseMakeupColor(Color color)
{
if (LoadingCatalog)
return;
CurrentMakeup.Color = color;
if (!CurrentMakeupActive)
CurrentMakeup.Amount = MakeupLayer == "Eyeliner" ? 1f : .65f;
ChangedCosmetics();
}
void SetMakeupAmount(float value)
{
if (LoadingCatalog || !float.IsFinite(value))
return;
CurrentMakeup.Amount = Math.Clamp(value, 0, 1);
ChangedCosmetics();
}
bool CurrentMakeupActive => MakeupLayer == "Eyeliner" ? Appearance.Makeup.EyelinerActive : CurrentMakeup.Amount > 0;
float EyelinerStrength(string part) => part == "Lower eyelid" ? Appearance.Makeup.EyelinerLowerStrength : part == "Wing" ? Appearance.Makeup.EyelinerWingStrength : Appearance.Makeup.EyelinerInnerStrength;
void SetEyelinerStrength(string part, float value)
{
if (LoadingCatalog || !float.IsFinite(value)) return;
value = Math.Clamp(value, 0, 1);
if (part == "Lower eyelid") Appearance.Makeup.EyelinerLowerStrength = value;
else if (part == "Wing") Appearance.Makeup.EyelinerWingStrength = value;
else Appearance.Makeup.EyelinerInnerStrength = value;
ChangedCosmetics();
}
void PrepareAdjustableWing()
{
var makeup = Appearance.Makeup;
makeup.EyelinerWingHorizontal = makeup.WingTipHorizontal;
makeup.EyelinerWingVertical = makeup.WingTipVertical;
makeup.EyelinerWing = "wing";
}
void SetWingTip(bool vertical, float value)
{
if (LoadingCatalog || !float.IsFinite(value)) return;
PrepareAdjustableWing();
if (vertical) Appearance.Makeup.EyelinerWingVertical = Math.Clamp(value, -1, 1);
else Appearance.Makeup.EyelinerWingHorizontal = Math.Clamp(value, 0, 1);
ChangedCosmetics();
}
void SetWingThickness(float value)
{
if (LoadingCatalog || !float.IsFinite(value)) return;
PrepareAdjustableWing();
Appearance.Makeup.EyelinerWingThickness = Math.Clamp(value, 0, 1);
ChangedCosmetics();
}
void ChooseEyelinerWing(string wing)
{
if (LoadingCatalog) return;
if (wing == "wing") PrepareAdjustableWing();
else Appearance.Makeup.EyelinerWing = "none";
if (wing != "none" && CurrentMakeup.Amount == 0) CurrentMakeup.Amount = 1;
ChangedCosmetics();
}
float MakeupChannel(string channel) => channel == "Red" ? CurrentMakeup.Red : channel == "Green" ? CurrentMakeup.Green : CurrentMakeup.Blue;
void SetMakeupChannel(string channel, float value)
{
if (LoadingCatalog || !float.IsFinite(value))
return;
value = Math.Clamp(value, 0, 1);
if (channel == "Red")
CurrentMakeup.Red = value;
else if (channel == "Green")
CurrentMakeup.Green = value;
else
CurrentMakeup.Blue = value;
ChangedCosmetics();
}
void ClearMakeup()
{
if (LoadingCatalog)
return;
Appearance.Makeup = new();
SelectedMarking = 0;
ChangedCosmetics();
}
bool Portrait = true, ShowClothing = true;
float Yaw, Zoom = CharacterCreatorPreview.FaceZoom;
CharacterCreatorPreview Preview;
Vector3 PreviewPan;
float PreviewPitch;
string BodyType => AppearanceCatalog.IsFemale(Appearance) ? "female" : "male";
IEnumerable<AppearanceOption> AvailableHair => AppearanceCatalog.Hair.Concat(ClothingResources.Available.Where(x=>x.Category=="Hair").Select(x=>new AppearanceOption(x.Path,x.Name,x.Path))).DistinctBy(x=>x.ResourcePath);
IEnumerable<AppearanceOption> AvailableFacialHair => AppearanceCatalog.FacialHair.Concat(ClothingResources.Available.Where(x=>x.Category=="Facial hair").Select(x=>new AppearanceOption(x.Path,x.Name,x.Path))).DistinctBy(x=>x.ResourcePath);
string HairLabel => AppearanceCatalog.Hair.FirstOrDefault(x => x.ResourcePath == Appearance.HairPath)?.Label ?? "Hair";
string FaceLabel(AppearanceOption option) => option.Label.Replace(" · male", "").Replace(" · female", "");
string ColorCss(Color color) => $"rgb({(int)(color.r * 255)},{(int)(color.g * 255)},{(int)(color.b * 255)})";
void SetCharacterName(string value)
{
CharacterName = value;
Feedback = null;
}
void SetPlaythroughName(string value)
{
PlaythroughName = value;
Feedback = null;
}
bool SameColor(Color color) => MathF.Abs(Appearance.HairColorR - color.r) < 0.001f && MathF.Abs(Appearance.HairColorG - color.g) < 0.001f && MathF.Abs(Appearance.HairColorB - color.b) < 0.001f;
void ChangeTab(string tab)
{
if (tab != Tab)
StopVoicePreview();
Tab = tab;
if (tab == "Clothing")
SetZoom(0);
Feedback = null;
}
void StopVoicePreview()
{
StopPreview?.Invoke();
}
void ChangeShapeCategory(string category)
{
ShapeCategory = category;
Feedback = null;
}
void ApplyPreset(string id)
{
if (LoadingCatalog)
return;
var voice = Appearance.VoiceId;
bool unified = Appearance.BaseShape is not null;
Appearance = AppearanceCatalog.ApplyPreset(id);
if (unified)
UnifiedHuman.Enable(Appearance);
Appearance.VoiceId = voice;
ChosenPreset = id;
Feedback = null;
}
void EnableBlending()
{
UnifiedHuman.Enable(Appearance);
ChosenPreset = null;
Feedback = null;
}
void AddPhotoFace(PhotoFace face)
{
if (!face.Valid)
return;
UnifiedHuman.Enable(Appearance);
if (Appearance.BaseShape.PhotoFaces.Count >= 4)
return;
Array.Fill(Appearance.BaseShape.HeadWeights, 0f);
foreach (var old in Appearance.BaseShape.PhotoFaces)
old.Influence = 0;
Appearance.BaseShape.PhotoFaces.Add(face);
ChosenPreset = null;
Tab = "Face & body";
BlendRegion = "overall";
SetView(CharacterCreatorPreview.FaceZoom);
Feedback = "Custom base added. Blend it here or refine it in Shape, then save your character.";
}
FaceRegionBlend Region(bool create = false)
{
if (BlendRegion == "overall")
return null;
if (!Appearance.BaseShape.Regions.TryGetValue(BlendRegion, out var value) && create)
{
value = new()
{
OverallInfluence = .5f
};
Appearance.BaseShape.Regions.Add(BlendRegion, value);
}
return value;
}
float OverallWeight => Region()?.OverallInfluence ?? .5f;
void SetOverallWeight(float value)
{
if (!float.IsFinite(value) || BlendRegion == "overall")
return;
Region(true).OverallInfluence = Math.Clamp(value, 0, 1);
ChosenPreset = null;
}
float HeadWeight(int index) => BlendRegion == "overall" ? Appearance.BaseShape.HeadWeights[index] : Region()?.HeadWeights[index] ?? 0;
float PhotoWeight(PhotoFace face) => BlendRegion == "overall" ? face.Influence : Region()?.PhotoWeights.GetValueOrDefault(face.Id) ?? 0;
string BlendWeightLabel(int index)
{
float share = FaceRegions.NativeShare(Appearance.BaseShape, index, BlendRegion);
return $"{MathF.Round(HeadWeight(index) * 100)} influence · {MathF.Round(share * 100)}% of blend";
}
void SetHeadWeight(int index, float value)
{
if (Appearance.BaseShape is null || !float.IsFinite(value))
return;
(BlendRegion == "overall" ? Appearance.BaseShape.HeadWeights : Region(true).HeadWeights)[index] = Math.Clamp(value, 0, 1);
ChosenPreset = null;
}
void SetPhotoWeight(PhotoFace face, float value)
{
if (!float.IsFinite(value))
return;
if (BlendRegion == "overall")
face.Influence = Math.Clamp(value, 0, 1);
else
Region(true).PhotoWeights[face.Id] = Math.Clamp(value, 0, 1);
ChosenPreset = null;
}
void ResetRegion()
{
if (BlendRegion != "overall")
Appearance.BaseShape.Regions.Remove(BlendRegion);
ChosenPreset = null;
}
void SetBodyBlend(float value)
{
if (Appearance.BaseShape is null || !float.IsFinite(value))
return;
Appearance.BaseShape.MaleBody = Math.Clamp(value, 0, 1);
ChosenPreset = null;
}
void ToggleClothingVariant(string variant)
{
if (Appearance.BaseShape is null)
return;
Appearance.BaseShape.ClothingVariant = Appearance.BaseShape.ClothingVariant == variant ? null : variant;
ChosenPreset = null;
}
void SetBodyFeature(bool breast, float value)
{
if (Appearance.BaseShape is null || !float.IsFinite(value))
return;
if (breast)
Appearance.BaseShape.BreastSize = Math.Clamp(value, 0, 1);
else
Appearance.BaseShape.BulgeSize = Math.Clamp(value, 0, 1);
ChosenPreset = null;
}
void ChooseBody(string body)
{
var skin = AppearanceCatalog.Skins.FirstOrDefault(x => x.BodyType == body);
if (skin is not null)
ChooseSkin(skin);
}
void ChooseSkin(AppearanceOption skin)
{
if (LoadingCatalog)
return;
Appearance.SkinPath = skin.ResourcePath;
if (Appearance.BaseShape is null && !AppearanceCatalog.FacialHair.Any(x => x.ResourcePath == Appearance.FacialHairPath && (x.BodyType == "" || x.BodyType == skin.BodyType)))
Appearance.FacialHairPath = "";
ChosenPreset = null;
Feedback = null;
}
void ChooseHair(AppearanceOption hair)
{
if (LoadingCatalog)
return;
Appearance.HairPath = hair.ResourcePath;
ChosenPreset = null;
Feedback = null;
}
void ChooseFacial(AppearanceOption facial)
{
if (LoadingCatalog)
return;
Appearance.FacialHairPath = facial.ResourcePath;
ChosenPreset = null;
Feedback = null;
}
void ChooseColor(AppearanceColorOption color)
{
if (LoadingCatalog)
return;
Appearance.HairColor = color.Color;
ChosenPreset = null;
Feedback = null;
}
void ChooseSkinColor(Color color, bool normalize = false)
{
if (LoadingCatalog)
return;
Appearance.SkinColor = color;
Appearance.NormalizeSkinTone = normalize;
ChosenPreset = null;
Feedback = null;
}
static IReadOnlyList<AppearanceColorOption> EyeColors => new[]
{
new AppearanceColorOption("brown", "Brown", new(.24f, .12f, .055f)),
new("hazel", "Hazel", new(.34f, .3f, .11f)),
new("amber", "Amber", new(.58f, .34f, .075f)),
new("green", "Green", new(.13f, .34f, .19f)),
new("blue", "Blue", new(.09f, .3f, .55f)),
new("grey", "Grey", new(.32f, .36f, .4f)),
new("violet", "Violet", new(.32f, .16f, .46f))
};
void ChooseEyeColor(Color? color)
{
if (LoadingCatalog || LoadingCatalog)
return;
Appearance.CustomEyeColor = color.HasValue;
if (color.HasValue)
Appearance.EyeColor = color.Value;
}
float EyeChannel(string channel) => channel == "Red" ? Appearance.EyeColorR : channel == "Green" ? Appearance.EyeColorG : Appearance.EyeColorB;
void SetEyeChannel(string channel, float value)
{
if (LoadingCatalog || LoadingCatalog)
return;
Appearance.CustomEyeColor = true;
value = Math.Clamp(value, 0, 1);
if (channel == "Red")
Appearance.EyeColorR = value;
else if (channel == "Green")
Appearance.EyeColorG = value;
else
Appearance.EyeColorB = value;
}
float SkinChannel(string channel) => channel == "Red" ? Appearance.SkinColorR : channel == "Green" ? Appearance.SkinColorG : Appearance.SkinColorB;
void SetSkinChannel(string channel, float value)
{
if (LoadingCatalog || !float.IsFinite(value))
return;
value = Math.Clamp(value, 0, 1);
Appearance.NormalizeSkinTone = true;
if (channel == "Red")
Appearance.SkinColorR = value;
else if (channel == "Green")
Appearance.SkinColorG = value;
else
Appearance.SkinColorB = value;
ChosenPreset = null;
Feedback = null;
}
void SetShape(AppearanceSlider slider, float value)
{
if (LoadingCatalog)
return;
slider.Set(Appearance, value);
ChosenPreset = null;
Feedback = null;
}
void ResetShapes()
{
if (LoadingCatalog)
return;
AppearanceCatalog.ResetShapes(Appearance);
ChosenPreset = null;
Feedback = null;
}
void Randomize()
{
if (LoadingCatalog)
return;
var voice = Appearance.VoiceId;
var variant = Appearance.BaseShape?.ClothingVariant;
var photos = Appearance.BaseShape?.PhotoFaces.Select(x => x.DeepCopy()).ToList();
Appearance = AppearanceCatalog.Randomize(blendHeads: Appearance.BaseShape is not null);
Appearance.VoiceId = voice;
if (Appearance.BaseShape is not null)
{
Appearance.BaseShape.ClothingVariant = variant;
if (photos is not null)
{
foreach (var face in photos)
face.Influence = 0;
Appearance.BaseShape.PhotoFaces = photos;
}
}
ChosenPreset = null;
Feedback = null;
}
void Reset()
{
if (LoadingCatalog)
return;
var voice = Appearance.VoiceId;
bool unified = Appearance.BaseShape is not null;
Appearance = AppearanceCatalog.CreateDefault();
if (unified)
UnifiedHuman.Enable(Appearance);
Appearance.VoiceId = voice;
ChosenPreset = "traveller";
Yaw = 0;
RecenterPreview();
SetZoom(CharacterCreatorPreview.FaceZoom);
Feedback = null;
}
void Rotate(float delta)
{
Yaw = (Yaw + delta) % 360;
}
void SetYaw(float value)
{
Yaw = value;
}
void SetView(float value)
{
RecenterPreview();
SetZoom(value);
}
void SetZoom(float value)
{
if (float.IsFinite(value))
{
Zoom = Math.Clamp(value, 0, CharacterCreatorPreview.MaxZoom);
Portrait = Zoom >= CharacterCreatorPreview.FaceZoom - 0.001f;
}
}
void SetPitch(float value)
{
PreviewPitch = value;
}
void SetPan(Vector3 value)
{
PreviewPan = value;
}
void RecenterPreview()
{
Yaw = 0;
PreviewPan = Vector3.Zero;
PreviewPitch = 0;
Preview?.Recenter();
}
void ToggleClothing()
{
ShowClothing = !ShowClothing;
}
void Back()
{
if (!LoadingCatalog)
{
StopVoicePreview();
Closed?.Invoke();
}
}
void Start()
{
if (Disabled || LoadingCatalog || Preview?.Loading == true)
return;
if (!CatalogReady)
{
Feedback = LoadingCatalog ? "Loading character options…" : "Character options could not be loaded.";
return;
}
if (CharacterAppearance.ValidateForApply(Appearance)is string error)
{
Feedback = error;
return;
}
StopVoicePreview();
Feedback = Saved?.Invoke(CharacterName, Appearance.DeepCopy());
}
public override void Delete(bool immediate = false)
{
StopVoicePreview();
base.Delete(immediate);
}
protected override async Task OnParametersSetAsync()
{
if (InitialPreset is not null && !ReferenceEquals(loadedPreset, InitialPreset))
{
LoadDraft(InitialPreset.Name, InitialPreset.Appearance);
loadedPreset = InitialPreset;
}
if (CatalogReady || LoadingCatalog)
return;
LoadingCatalog = true;
try
{
await AppearanceCatalog.EnsureLoadedAsync();
CatalogReady = true;
}
catch (Exception)
{
Feedback = "Character options could not be loaded. Please try again.";
}
finally
{
LoadingCatalog = false;
}
}
protected override int BuildHash() => HashCode.Combine(CharacterName, PlaythroughName, HashCode.Combine(Tab, ShapeCategory, SkinPalette, MakeupLayer, CustomMakeup, SelectedMarking, MarkingSection, BlendRegion), ChosenPreset, JsonSerializer.Serialize(Appearance) + Appearance.Makeup.PresentationKey(), HashCode.Combine(Portrait, Yaw, Feedback, LoadingCatalog, SaveLabel, Preview?.Error, CatalogReady, HashCode.Combine(LoadingCatalog, Disabled, ProductLabel, LocationLabel, Title)), Zoom, HashCode.Combine(ShowClothing, PreviewPan, PreviewPitch, WardrobeSearch, WardrobeCategory, ClothingResources.Revision, CloudWardrobeOpen));
}
Game
library
namespace CharacterEditor;
// Generated by ce_calibrate_photo_profile from native topology and packed engine deformers.
public static class PhotoProfileAnchors
{
public static readonly int[] Ids={10,168,4,1,13,14,152};
public static readonly string[] Controls={"head","jaw","eyes","nosewidth","nostrils","noseheight","mouthwidth","nose","chin","mouthprojection","chinheight"};
public static readonly Vector3[][] Heads={
new Vector3[]{new(3.6992545f,-0.066790596f,68.14279f),new(5.2946215f,0.11305821f,64.60188f),new(5.959813f,-0.041913833f,63.655262f),new(6.1343274f,-0.058205567f,63.346245f),new(5.8273835f,0.07009718f,61.76828f),new(5.7244573f,0.09594756f,61.383133f),new(4.6132507f,-0.040823318f,59.63866f)},
new Vector3[]{new(3.6878302f,-0.014352961f,68.02986f),new(5.085779f,0.0712296f,64.49504f),new(5.841306f,-0.03587849f,63.486412f),new(6.0319343f,-0.045126636f,63.211014f),new(5.53363f,0.09146978f,61.719315f),new(5.485255f,0.1368592f,61.244686f),new(4.676547f,-0.012164734f,59.841347f)},
new Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2011194f,0.07334158f,64.609566f),new(5.979442f,-0.033484418f,63.572296f),new(6.1241097f,-0.03964384f,63.281433f),new(5.778057f,0.053285245f,61.74f),new(5.719415f,0.069648676f,61.50413f),new(4.7025223f,-0.06262799f,59.70981f)},
new Vector3[]{new(3.5149546f,-0.009116913f,68.08719f),new(5.0824428f,0.056152936f,64.4079f),new(5.857973f,-0.08385171f,63.312653f),new(6.0649567f,-0.10289183f,62.99997f),new(5.4969316f,0.05211438f,61.666157f),new(5.502199f,0.10385137f,61.29811f),new(4.67235f,-0.1021266f,59.774113f)},
new Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2132f,0.087325275f,64.6164f),new(5.957733f,0.064072326f,63.39234f),new(6.137288f,0.064377435f,63.1475f),new(5.6911297f,0.052945673f,61.71075f),new(5.713699f,0.06934876f,61.489025f),new(4.702329f,-0.055987295f,59.563744f)},
new Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2012277f,0.1540686f,64.59866f),new(6.0016494f,-0.08192249f,63.62896f),new(6.165094f,-0.049669266f,63.36159f),new(5.7699647f,0.05389818f,61.78059f),new(5.719415f,0.06964957f,61.493782f),new(4.700662f,-0.0723621f,59.84928f)},
new Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.173374f,0.09990916f,64.60435f),new(5.9552946f,0.042667035f,63.536186f),new(6.1126857f,0.03899601f,63.296906f),new(5.6911297f,0.0529435f,61.727646f),new(5.713699f,0.07963325f,61.420948f),new(4.702329f,-0.05598729f,59.74437f)}
};
// [head * Controls.Length * 2 + control * 2 + positiveEndpoint]
public static readonly Vector3[][] Deltas={
new Vector3[]{new(-0.13209844f,0.0031800792f,-0.07652283f),new(-0.24389982f,-0.0069441274f,-0.03074646f),new(-0.23990822f,0.0022496209f,-0.010990143f),new(-0.23487473f,0.0029714853f,-0.0055236816f),new(-0.23757076f,-0.003810294f,0.020923615f),new(-0.23392248f,-0.0052277073f,0.02753067f),new(-0.2026701f,0.0023912787f,0.061531067f)},
new Vector3[]{new(0.13209844f,-0.0031800792f,0.07652283f),new(0.24389982f,0.0069441274f,0.03074646f),new(0.23990822f,-0.0022496209f,0.010990143f),new(0.23487473f,-0.0029714853f,0.0055236816f),new(0.23757076f,0.003810294f,-0.020923615f),new(0.23392248f,0.0052277073f,-0.02753067f),new(0.2026701f,-0.0023912787f,-0.061531067f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.011735871f,0f),new(0f,0.016297556f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.011735871f,0f),new(0f,-0.016297556f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3449974f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.0107391365f,0f),new(0f,-0.0056006014f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.010739133f,0f),new(0f,0.0056006014f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.041153908f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.0532794f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.13788319f,0f,0f),new(-0.052534103f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.19303656f,0f,0f),new(0.07354784f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.47891617f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.31927872f)},
new Vector3[]{new(-0.1366136f,0.0007089311f,-0.07763672f),new(-0.24782324f,-0.004627906f,-0.03035736f),new(-0.2453003f,0.002008915f,-0.00851059f),new(-0.23964739f,0.0023905002f,-0.0034942627f),new(-0.24498606f,-0.0053994358f,0.02362442f),new(-0.23736048f,-0.007896334f,0.031650543f),new(-0.20918512f,0.0007255161f,0.05886841f)},
new Vector3[]{new(0.1366136f,-0.0007089311f,0.07763672f),new(0.24782324f,0.004627906f,0.03035736f),new(0.2453003f,-0.002008915f,0.00851059f),new(0.23964739f,-0.0023905002f,0.0034942627f),new(0.24498606f,0.0053994358f,-0.02362442f),new(0.23736048f,0.007896334f,-0.031650543f),new(0.20918512f,-0.0007255161f,-0.05886841f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.010045975f,0f),new(0f,0.012437068f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.010045975f,0f),new(0f,-0.012437068f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.33958435f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.33958435f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.018210128f,0f),new(0f,-0.0033276677f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.018210128f,0f),new(0f,0.0033276677f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4882121f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4882121f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.2155137f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.27901363f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.17917538f,0f,0f),new(-0.02188301f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.25084543f,0f,0f),new(0.03063631f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5400009f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3600006f)},
new Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.2449646f,-0.004605703f,-0.031585693f),new(-0.2398367f,0.001790762f,-0.009563446f),new(-0.23560143f,0.0020335242f,-0.004512787f),new(-0.2387433f,-0.002935592f,0.021694183f),new(-0.23645878f,-0.0038393363f,0.02576828f),new(-0.2047453f,0.0036357194f,0.059688568f)},
new Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.2449646f,0.004605703f,0.031585693f),new(0.2398367f,-0.001790762f,0.009563446f),new(0.23560143f,-0.0020335242f,0.004512787f),new(0.2387433f,0.002935592f,-0.021694183f),new(0.23645878f,0.0038393363f,-0.02576828f),new(0.2047453f,-0.0036357194f,-0.059688568f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009375636f,0f),new(0f,0.011100274f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009375636f,0f),new(0f,-0.011100274f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.008732874f,0f),new(0f,-0.008089323f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.008732874f,0f),new(0f,0.008089326f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.1130929f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.14641476f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.14750004f,0f,0f),new(-0.104530334f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.20650053f,0f,0f),new(0.14634228f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5328598f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.35523987f)},
new Vector3[]{new(-0.13274455f,0.0004590759f,-0.08004761f),new(-0.24919367f,-0.0036709309f,-0.028762817f),new(-0.24560404f,0.004687473f,-0.0054626465f),new(-0.238554f,0.0053960904f,0f),new(-0.24498224f,-0.0030967854f,0.02476883f),new(-0.23813248f,-0.0059928596f,0.03068924f),new(-0.20696688f,0.006031759f,0.059539795f)},
new Vector3[]{new(0.13274455f,-0.0004590759f,0.08004761f),new(0.24919367f,0.0036709309f,0.028762817f),new(0.24560404f,-0.004687473f,0.0054626465f),new(0.238554f,-0.0053960904f,0f),new(0.24498224f,0.0030967854f,-0.02476883f),new(0.23813248f,0.0059928596f,-0.03068924f),new(0.20696688f,-0.006031759f,-0.059539795f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,-5.4202974E-05f,0f),new(0f,0.02339752f,0f),new(0f,0.020724386f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,5.4202974E-05f,0f),new(0f,-0.02339752f,0f),new(0f,-0.020724379f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,-0.0011901855f),new(0f,0f,-0.34381104f),new(0f,0f,-0.24817657f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0.0011901855f),new(0f,0f,0.34381104f),new(0f,0f,0.24817657f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.01034468f,0f),new(0f,-0.0050033107f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.01034468f,0f),new(0f,0.0050033107f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(-0.001709938f,0f,0f),new(-0.49428988f,0f,0f),new(-0.3567996f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0.001709938f,0f,0f),new(0.49428988f,0f,0f),new(0.3567996f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.15790367f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.20442915f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.17864943f,0f,0f),new(-0.043359756f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.2501092f,0f,0f),new(0.060703754f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.53889465f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.35926437f)},
new Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.24469805f,-0.0054651946f,-0.03161621f),new(-0.24176359f,-0.0034667253f,-0.0066337585f),new(-0.23539543f,-0.0032922663f,-0.002357483f),new(-0.24088907f,-0.0029880553f,0.02273941f),new(-0.23634768f,-0.0038248226f,0.026042938f),new(-0.19970894f,0.0031703971f,0.060806274f)},
new Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.24469805f,0.0054651946f,0.03161621f),new(0.24176359f,0.0034667253f,0.0066337585f),new(0.23539543f,0.0032922626f,0.002357483f),new(0.24088907f,0.0029880553f,-0.02273941f),new(0.23634768f,0.0038248226f,-0.026042938f),new(0.19970894f,-0.0031703971f,-0.060806274f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.017940246f,0f),new(0f,-0.017056447f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.017940246f,0f),new(0f,0.017056443f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.32645035f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.32645035f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009510666f,0f),new(0f,-0.0076843873f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009510666f,0f),new(0f,0.0076843947f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4693303f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4693303f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.015622139f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.020225048f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.16166735f,0f,0f),new(-0.099727154f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.22633457f,0f,0f),new(0.13961792f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.4568138f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.30454254f)},
new Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.2450552f,-0.009678587f,-0.03138733f),new(-0.23850155f,0.004340723f,-0.010414124f),new(-0.23345089f,0.0025077388f,-0.005706787f),new(-0.2396326f,-0.002984602f,0.021102905f),new(-0.2362647f,-0.003836237f,0.025924683f),new(-0.2093401f,0.004296787f,0.05846405f)},
new Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.2450552f,0.009678587f,0.03138733f),new(0.23850155f,-0.004340723f,0.010414124f),new(0.23345089f,-0.0025077388f,0.005706787f),new(0.2396326f,0.002984602f,-0.021102905f),new(0.2362647f,0.003836237f,-0.025924683f),new(0.2093401f,-0.004296787f,-0.05846405f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.022938296f,0f),new(0f,0.013907395f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.022938296f,0f),new(0f,-0.013907395f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009057671f,0f),new(0f,-0.0077751838f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009057675f,0f),new(0f,0.00777518f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.22866583f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.29604006f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.15124607f,0f,0f),new(-0.10046959f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.21174479f,0f,0f),new(0.14065742f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5400009f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3600006f)},
new Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.24530983f,-0.006316647f,-0.031700134f),new(-0.24099922f,-0.002302207f,-0.0090408325f),new(-0.23603249f,-0.0020077005f,-0.004776001f),new(-0.24115801f,-0.0029912703f,0.022464752f),new(-0.23502398f,-0.004367456f,0.027061462f),new(-0.2059083f,0.0032688156f,0.05939865f)},
new Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.24530983f,0.006316647f,0.031700134f),new(0.24099922f,0.002302207f,0.0090408325f),new(0.23603249f,0.0020077005f,0.004776001f),new(0.24115801f,0.0029912703f,-0.022464752f),new(0.23502398f,0.004367456f,-0.027061462f),new(0.2059083f,-0.0032688156f,-0.05939865f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.011946769f,0f),new(0f,-0.010918882f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.011946768f,0f),new(0f,0.0109188855f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009599518f,0f),new(0f,-0.0062645525f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009599518f,0f),new(0f,0.0062645525f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.14241362f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.18437433f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.16318464f,0f,0f),new(-0.07080078f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.2284584f,0f,0f),new(0.099121094f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5386009f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.359066f)}
};
}
Game
library
using System;
using System.Threading.Tasks;
namespace CharacterEditor;
public sealed record AppearanceOption(string Id, string Label, string ResourcePath, string BodyType = "", string Icon = "");
public sealed record AppearancePreset(string Id, string Label, CharacterAppearanceData Appearance);
public sealed record AppearanceColorOption(string Id, string Label, Color Color, bool NormalizeSkinTone = false);
public sealed class AppearanceSlider
{
public string Id { get; }
public string Label { get; }
public string Group { get; }
public float Min => 0;
public float Max => 1;
public float Neutral => Id is "eartaper" or "belly" ? 0 : 0.5f;
public string LabelFor(CharacterAppearanceData data) => Id == "bodyfeature" ? (AppearanceCatalog.IsFemale(data) ? "Breast size" : "Bulge size") : Label;
public AppearanceSlider(string id, string label, string group)
{
Id = id;
Label = label;
Group = group;
}
public float Get(CharacterAppearanceData data) => Id switch
{
"neck" => data.NeckSize,
"trapezius" => data.TrapeziusSize,
"collarbone" => data.CollarboneHeight,
"belly" => data.BellySize,
"waist" => data.WaistSize,
"chest" => data.ChestSize,
"fitness" => data.Fitness,
"head" => data.HeadShape,
"nose" => data.NoseSize,
"nosewidth" => data.NoseWidth,
"chin" => data.ChinSize,
"chinwidth" => data.ChinWidth,
"jaw" => data.JawWidth,
"nostrils" => data.NostrilWidth,
"forehead" => data.ForeheadWidth,
"cheekbones" => data.CheekboneWidth,
"ears" => data.EarSize,
"eyes" => data.EyeSize,
"cheeks" => data.CheekFullness,
"nostrilfullness" => data.NostrilFullness,
"noseheight" => data.NoseHeight,
"chinheight" => data.ChinHeight,
"submental" => data.SubmentalFullness,
"brow" => data.BrowProminence,
"bodyfeature" => data.BodyFeatureSize,
"butt" => data.ButtSize,
"feet" => data.FootSize,
"eartaper" => data.EarTaper,
"philtrum" => data.PhiltrumWidth,
"columella" => data.ColumellaWidth,
"mouthwidth" => data.MouthWidth,
"mouthprojection" => data.MouthProjection,
"upperlip" => data.UpperLipFullness,
"lowerlip" => data.LowerLipFullness,
_ => Neutral
};
public void Set(CharacterAppearanceData data, float value)
{
if (data is null || !float.IsFinite(value))
return;
value = Math.Clamp(value, Min, Max);
// Store data rather than delegates in the catalog so native hotload can preserve these selectors.
switch (Id)
{
case "neck":
data.NeckSize = value;
break;
case "trapezius":
data.TrapeziusSize = value;
break;
case "collarbone":
data.CollarboneHeight = value;
break;
case "belly":
data.BellySize = value;
break;
case "waist":
data.WaistSize = value;
break;
case "chest":
data.ChestSize = value;
break;
case "fitness":
data.Fitness = value;
break;
case "head":
data.HeadShape = value;
break;
case "nose":
data.NoseSize = value;
break;
case "nosewidth":
data.NoseWidth = value;
break;
case "chin":
data.ChinSize = value;
break;
case "chinwidth":
data.ChinWidth = value;
break;
case "jaw":
data.JawWidth = value;
break;
case "nostrils":
data.NostrilWidth = value;
break;
case "forehead":
data.ForeheadWidth = value;
break;
case "cheekbones":
data.CheekboneWidth = value;
break;
case "ears":
data.EarSize = value;
break;
case "eyes":
data.EyeSize = value;
break;
case "cheeks":
data.CheekFullness = value;
break;
case "nostrilfullness":
data.NostrilFullness = value;
break;
case "noseheight":
data.NoseHeight = value;
break;
case "chinheight":
data.ChinHeight = value;
break;
case "submental":
data.SubmentalFullness = value;
break;
case "brow":
data.BrowProminence = value;
break;
case "bodyfeature":
data.BodyFeatureSize = value;
break;
case "butt":
data.ButtSize = value;
break;
case "feet":
data.FootSize = value;
break;
case "eartaper":
data.EarTaper = value;
break;
case "philtrum":
data.PhiltrumWidth = value;
break;
case "columella":
data.ColumellaWidth = value;
break;
case "mouthwidth":
data.MouthWidth = value;
break;
case "mouthprojection":
data.MouthProjection = value;
break;
case "upperlip":
data.UpperLipFullness = value;
break;
case "lowerlip":
data.LowerLipFullness = value;
break;
}
}
}
/// <summary>Curated installed, non-inventory native assets. IDs and paths are stable; no cloud discovery or external download is needed.</summary>
public static class AppearanceCatalog
{
static readonly Dictionary<string, Task> resourceLoads = new(StringComparer.OrdinalIgnoreCase);
public static bool Loading => resourceLoads.Values.Any(x => x is not null && !x.IsCompleted);
public static string Error { get; private set; }
public static int Revision { get; private set; }
const string Root = "models/citizen_clothes/";
static AppearanceOption Skin(string number, string label, string body) => new("human" + number, label, Root + "body/human" + number + ".clothing", body, Root + "body/human" + number + ".clothing.png");
static AppearanceOption Style(string id, string label, string relative, string body = "") => new(id, label, Root + "hair/" + relative + ".clothing", body, Root + "hair/" + relative + ".clothing.png");
public static IReadOnlyList<AppearanceOption> Skins { get; } = Array.AsReadOnly(new[] { Skin("01", "Face 1 · dark · male", "male"), Skin("02", "Face 2 · dark · female", "female"), Skin("03", "Face 3 · light · male", "male"), Skin("04", "Face 4 · light · female", "female"), Skin("05", "Face 5 · light · male", "male"), Skin("06", "Face 6 · dark · male", "male"), Skin("07", "Face 7 · light · male", "male") });
public static IReadOnlyList<AppearanceOption> Hair { get; } = Array.AsReadOnly(new[] { new AppearanceOption("none", "No hair", ""), Style("scruffy", "Short scruffy", "hair_shortscruffy/hair_shortscruffy_brown"), Style("short_braids", "Short braids", "hair_short_braids/hair_short_braids"), Style("short_dreads", "Short dreadlocks", "hair_short_dreadlocks/short_dreadlocks"), Style("bob", "Bob cut", "hair_bobcut/hair_bobcut"), Style("long_bob", "Long bob", "hair_longbob/hair_longbob"), Style("messy_bun", "Messy bun", "hair_messy_bun/hair_messy_bun"), Style("ponytail", "Tight ponytail", "hair_tight_ponytail/hair_tight_ponytail_brown"), Style("long_curly", "Long curly", "hair_longcurly/hair_longcurly_brown"), Style("long_bangs", "Long bangs", "hair_long_bangs/hair_long_bangs"), Style("long_dreads", "Long dreadlocks", "hair_long_dreadlocks/hair_long_dreadlocks"), Style("tied_braids", "Tied braids", "hair_tied_braids/hair_tied_braids"), Style("tied_dreads", "Tied dreadlocks", "hair_tied_dreads/hair_tied_dreads"), Style("slicked", "Slicked back", "hair_slickedback/hair_slickedback"), Style("balding", "Balding", "hair_balding/hair_baldingbrown"), Style("thinning", "Thinning short", "hair_thinning_short/thinning_short"), Style("pigtails", "Pigtails", "hair_pigtails/hair_pigtails"), Style("long_brown", "Long hair", "hair_longbrown/Models/hair_longbrown"), Style("loose", "Loose hair", "hair_looseblonde/hair.loose.grey"), Style("slicked_balding", "Slicked back balding", "hair_slicked_back_balding/hair_slicked_back_balding") });
public static IReadOnlyList<AppearanceOption> FacialHair { get; } = Array.AsReadOnly(new[] { new AppearanceOption("none", "No facial hair", ""), Style("patchy", "Patchy beard", "patchy_beard/patchy_beard", "male"), Style("medium", "Medium beard", "medium_beard/medium_beard", "male"), Style("goatee", "Goatee", "goatee/goatee", "male"), Style("moustache", "Moustache", "moustache/moustache", "male"), Style("handlebar", "Handlebar moustache", "handlebar_moustache/handlebar_moustache", "male"), Style("chops", "Sideburns", "mutton_chops/mutton_chops", "male"), Style("braided", "Braided beard", "braided_long_beard/braided_long_beard", "male"), Style("decorated", "Decorated beard", "decorated_beard/decorated_beard", "male") });
public static IReadOnlyList<AppearanceColorOption> Colors { get; } = Array.AsReadOnly(new[] { new AppearanceColorOption("black", "Black", new Color(0.045f, 0.035f, 0.028f)), new AppearanceColorOption("brown", "Brown", new Color(0.22f, 0.13f, 0.08f)), new AppearanceColorOption("auburn", "Auburn", new Color(0.48f, 0.17f, 0.07f)), new AppearanceColorOption("blonde", "Blonde", new Color(0.74f, 0.57f, 0.31f)), new AppearanceColorOption("silver", "Silver", new Color(0.66f, 0.67f, 0.7f)), new AppearanceColorOption("white", "White", new Color(0.92f, 0.89f, 0.82f)) });
public const int ShapeControlCount = 34;
public static bool IsFemale(CharacterAppearanceData data) => data?.BaseShape is not null ? data.BaseShape.MaleBody < .5f : Skins.FirstOrDefault(x => string.Equals(x.ResourcePath, data?.SkinPath, StringComparison.OrdinalIgnoreCase))?.BodyType == "female";
static IReadOnlyList<AppearanceColorOption> humanSkinColors;
public static IReadOnlyList<AppearanceColorOption> HumanSkinColors
{
get
{
if (humanSkinColors?.Count != 7 || !humanSkinColors[1].NormalizeSkinTone)
humanSkinColors = CreateHumanSkinColors();
return humanSkinColors;
}
}
static IReadOnlyList<AppearanceColorOption> CreateHumanSkinColors() => Array.AsReadOnly(new[] { new AppearanceColorOption("native", "Native", Color.White), new AppearanceColorOption("fair", "Fair", Color.White, true), new AppearanceColorOption("rose", "Rose", new Color(1, 0.96f, 0.96f), true), new AppearanceColorOption("golden", "Golden", new Color(1, 0.96f, 0.88f), true), new AppearanceColorOption("olive", "Olive", new Color(0.91f, 0.88f, 0.77f), true), new AppearanceColorOption("tan", "Tan", new Color(0.72f, 0.6f, 0.5f), true), new AppearanceColorOption("umber", "Umber", new Color(0.42f, 0.3f, 0.23f), true) });
static IReadOnlyList<AppearanceColorOption> fantasySkinColors;
public static IReadOnlyList<AppearanceColorOption> FantasySkinColors
{
get
{
if (fantasySkinColors?.Count != 6 || fantasySkinColors.Any(x => !x.NormalizeSkinTone))
fantasySkinColors = CreateFantasySkinColors();
return fantasySkinColors;
}
}
static IReadOnlyList<AppearanceColorOption> CreateFantasySkinColors() => Array.AsReadOnly(new[] { new AppearanceColorOption("blue", "Blue", new Color(0.18f, 0.4f, 1), true), new AppearanceColorOption("purple", "Purple", new Color(0.6f, 0.25f, 1), true), new AppearanceColorOption("green", "Green", new Color(0.3f, 0.85f, 0.4f), true), new AppearanceColorOption("red", "Crimson", new Color(1, 0.25f, 0.22f), true), new AppearanceColorOption("teal", "Teal", new Color(0.18f, 0.85f, 0.9f), true), new AppearanceColorOption("grey", "Slate", new Color(0.5f, 0.6f, 0.74f), true) });
// Native hotload can retain the previous static list when the catalog grows.
// Build the supported immutable catalog lazily so new controls are available
// in an already running editor as well as a cold-start game.
public static IReadOnlyList<AppearanceSlider> Sliders
{
get
{
if (contourControls?.Count != ShapeControlCount)
contourControls = CreateContourControls();
return contourControls;
}
}
static IReadOnlyList<AppearanceSlider> contourControls;
static IReadOnlyList<AppearanceSlider> CreateContourControls() => Array.AsReadOnly(new[] { new AppearanceSlider("neck", "Neck size", "Body"), new AppearanceSlider("trapezius", "Trapezius", "Body"), new AppearanceSlider("collarbone", "Collarbone height", "Body"), new AppearanceSlider("belly", "Belly size", "Body"), new AppearanceSlider("waist", "Waist size", "Body"), new AppearanceSlider("chest", "Chest size", "Body"), new AppearanceSlider("fitness", "Fitness", "Body"), new AppearanceSlider("bodyfeature", "Body contour", "Body"), new AppearanceSlider("butt", "Butt size", "Body"), new AppearanceSlider("feet", "Foot size", "Body"), new AppearanceSlider("head", "Head shape", "Face"), new AppearanceSlider("forehead", "Forehead width", "Face"), new AppearanceSlider("cheekbones", "Cheekbone width", "Face"), new AppearanceSlider("cheeks", "Cheek fullness", "Face"), new AppearanceSlider("jaw", "Jaw width", "Jaw & chin"), new AppearanceSlider("chin", "Chin projection", "Jaw & chin"), new AppearanceSlider("chinwidth", "Chin width", "Jaw & chin"), new AppearanceSlider("chinheight", "Chin height", "Jaw & chin"), new AppearanceSlider("submental", "Under-chin fullness", "Jaw & chin"), new AppearanceSlider("nose", "Nose projection", "Nose"), new AppearanceSlider("nosewidth", "Nose width", "Nose"), new AppearanceSlider("nostrils", "Nostril width", "Nose"), new AppearanceSlider("nostrilfullness", "Nostril-wing fullness", "Nose"), new AppearanceSlider("noseheight", "Nose tip height", "Nose"), new AppearanceSlider("columella", "Columella width", "Nose"), new AppearanceSlider("philtrum", "Philtrum width", "Mouth"), new AppearanceSlider("mouthwidth", "Mouth width", "Mouth"), new AppearanceSlider("mouthprojection", "Mouth projection", "Mouth"), new AppearanceSlider("upperlip", "Upper lip fullness", "Mouth"), new AppearanceSlider("lowerlip", "Lower lip fullness", "Mouth"), new AppearanceSlider("eyes", "Eye size", "Eyes & ears"), new AppearanceSlider("ears", "Ear size", "Eyes & ears"), new AppearanceSlider("brow", "Brow prominence", "Eyes & ears"), new AppearanceSlider("eartaper", "Ear taper", "Eyes & ears") });
static CharacterAppearanceData Preset(string skin, string hair, string facial, string color) => new()
{
SkinPath = Skins.Single(x => x.Id == skin).ResourcePath,
HairPath = Hair.Single(x => x.Id == hair).ResourcePath,
FacialHairPath = FacialHair.Single(x => x.Id == facial).ResourcePath,
HairColor = Colors.Single(x => x.Id == color).Color
};
static IReadOnlyList<AppearancePreset> presets;
public static IReadOnlyList<AppearancePreset> Presets
{
get
{
if (presets?.Count != 7)
presets = Array.AsReadOnly(new[]
{
new AppearancePreset("traveller", "Traveller", Preset("human03", "scruffy", "none", "brown")),
new AppearancePreset("scout", "Scout", Preset("human02", "short_braids", "none", "black")),
new AppearancePreset("scholar", "Scholar", Preset("human04", "messy_bun", "none", "auburn")),
new AppearancePreset("veteran", "Veteran", Preset("human07", "balding", "medium", "silver")),
new AppearancePreset("wanderer", "Wanderer", Preset("human06", "short_dreads", "none", "black")),
new AppearancePreset("artisan", "Artisan", Preset("human05", "slicked", "goatee", "brown")),
new AppearancePreset("explorer", "Explorer", Preset("human01", "tied_braids", "none", "black"))
});
return presets;
}
}
public static CharacterAppearanceData CreateDefault() => ApplyPreset("traveller");
public static CharacterAppearanceData ApplyPreset(string id) => Presets.FirstOrDefault(x => x.Id == id)?.Appearance.DeepCopy() ?? new CharacterAppearanceData();
public static void ResetShapes(CharacterAppearanceData data)
{
if (data is null)
return;
foreach (var slider in Sliders)
slider.Set(data, slider.Neutral);
if (data.BaseShape is not null)
{
data.BaseShape.BreastSize = .5f;
data.BaseShape.BulgeSize = .5f;
}
}
public static CharacterAppearanceData Randomize(Random rng = null, bool blendHeads = false)
{
rng ??= new Random();
var body = rng.Next(2) == 0 ? "male" : "female";
// Cross-body skins need weighted heads; native (v1) characters keep their native pool.
var skins = Skins.Where(x => blendHeads ? (body == "male" || x.Id is not ("human05" or "human07")) : x.BodyType == body).ToArray();
var skin = skins[rng.Next(skins.Length)];
var hair = RandomHairFor(body);
var facial = FacialHair.Where(x => x.BodyType == "" || x.BodyType == body).ToArray();
var tones = HumanSkinColors.Where(x => body == "female" && skin.Id == "human06" ? x.Id is "native" or "tan" or "umber" : x.NormalizeSkinTone).ToArray();
var tone = tones[rng.Next(tones.Length)];
var data = new CharacterAppearanceData
{
SkinPath = skin.ResourcePath,
HairPath = hair[rng.Next(hair.Count)].ResourcePath,
FacialHairPath = facial[rng.Next(facial.Length)].ResourcePath,
HairColor = Colors[rng.Next(Colors.Count)].Color,
SkinColor = tone.Color,
NormalizeSkinTone = tone.NormalizeSkinTone
};
foreach (var slider in Sliders.Where(x => x.Id != "belly"))
slider.Set(data, 0.3f + (float)rng.NextDouble() * 0.4f);
// An added belly is an occasional, modest feature rather than a default.
data.BellySize = rng.NextDouble() < 0.10 ? 0.1f + (float)rng.NextDouble() * 0.3f : 0;
if (body == "female" && rng.NextDouble() < 0.60)
{
AddMakeup("Lipstick", 0.70, 0.35f, 0.85f, 4);
AddMakeup("Eyeshadow", 0.45, 0.20f, 0.65f, 3);
AddMakeup("Blush", 0.55, 0.15f, 0.55f, 4);
}
if (blendHeads)
{
UnifiedHuman.Enable(data);
// Body identity must follow the roll, not the selected skin's native body.
bool female = body == "female";
data.BaseShape.MaleBody = female ? 0 : 1;
data.BaseShape.BreastSize = female ? data.BodyFeatureSize : .5f;
data.BaseShape.BulgeSize = female ? .5f : data.BodyFeatureSize;
data.BaseShape.HeadWeights = RandomHeadWeights(rng, female, skin.Id, false);
foreach (var region in FaceRegions.Ids)
{
// Each part gets its own dominant source rather than averaging every head
// equally. Keep jaw/neck identity especially close to the body's family.
data.BaseShape.Regions[region] = new()
{
OverallInfluence = .15f + (float)rng.NextDouble() * .20f,
HeadWeights = RandomHeadWeights(rng, female, skin.Id, region is "jaw" or "throat")
};
}
// A shared jaw/throat source keeps the lower-face silhouette coherent.
data.BaseShape.Regions["throat"] = data.BaseShape.Regions["jaw"].DeepCopy();
}
data.ClothingPaths = WardrobeCatalog.Randomize(rng, body == "female");
data.Version = 2;
return data;
void AddMakeup(string layer, double chance, float low, float high, int paletteCount)
{
if (rng.NextDouble() >= chance)
return;
var options = MakeupCatalog.Colors(layer);
var target = MakeupCatalog.Layer(data.Makeup, layer);
target.Color = options[rng.Next(Math.Min(paletteCount, options.Count))].Color;
target.Amount = low + (float)rng.NextDouble() * (high - low);
}
}
static float[] RandomHeadWeights(Random rng, bool female, string skin, bool structural)
{
// Allocate by family before choosing individual heads: five masculine faces
// must not overwhelm the two feminine ones. Skin 04 on men excludes them.
float ownShare = structural ? .95f + (float)rng.NextDouble() * .05f : .85f + (float)rng.NextDouble() * .15f;
float femaleShare = female ? ownShare : skin == "human04" ? 0 : 1 - ownShare;
var weights = new float[7];
Fill(new[] { 1, 3 }, femaleShare);
Fill(new[] { 0, 2, 4, 5, 6 }, 1 - femaleShare);
float max = weights.Max();
for (int i = 0; i < weights.Length; i++)
weights[i] /= max;
return weights;
void Fill(int[] indices, float share)
{
int primary = indices[rng.Next(indices.Length)];
float dominance = .50f + (float)rng.NextDouble() * .40f;
weights[primary] = share * dominance;
float total = 0;
foreach (int i in indices)
if (i != primary)
{
weights[i] = .05f + (float)rng.NextDouble();
total += weights[i];
}
foreach (int i in indices)
if (i != primary)
weights[i] *= share * (1 - dominance) / total;
}
}
// Preferences apply only to Randomize. Every hairstyle remains manually
// selectable and valid on either human body, including male pigtails.
public static IReadOnlyList<AppearanceOption> RandomHairFor(string bodyType) => Hair.Where(x => bodyType == "female" ? x.Id is not ("balding" or "thinning" or "slicked_balding") : x.Id is "none" or "scruffy" or "short_braids" or "short_dreads" or "long_dreads" or "tied_braids" or "tied_dreads" or "slicked" or "balding" or "thinning" or "ponytail" or "long_curly" or "long_brown" or "slicked_balding").ToArray();
/// <summary>Loads only curated native resources, either the whole creator catalog or one selected/saved appearance.</summary>
public static async Task EnsureLoadedAsync(CharacterAppearanceData data = null)
{
if (data is not null && CharacterAppearanceData.Validate(data)is string error)
throw new InvalidOperationException(error);
var paths = data is null ? Skins.Concat(Hair).Concat(FacialHair).Select(x => x.ResourcePath) : new[]
{
data.SkinPath,
UnifiedHuman.PresentationSkin(data),
data.HairPath,
data.FacialHairPath
};
try
{
foreach (var path in paths.Where(x => !string.IsNullOrEmpty(x)).Distinct(StringComparer.OrdinalIgnoreCase))
await Load(path);
if (data is null || data.Makeup.Active)
foreach (var skin in data is null ? Skins : Skins.Where(x => x.ResourcePath == data.SkinPath))
await HumanMakeupAssets.EnsureLoadedAsync(skin.Id);
if (data is null || data.CustomEyeColor)
await EyeColorPresentation.EnsureLoadedAsync();
if (data is not null)
await HumanMarkingModels.EnsureLoadedAsync(data);
if (data?.BaseShape is not null)
{
var model = await Model.LoadAsync(UnifiedHuman.ModelPath);
if (model is null || model.IsError)
throw new InvalidOperationException("The unified human model is unavailable.");
await UnifiedNeckAssets.LoadHeadAsync(data.SkinPath);
}
Error = null;
}
catch (Exception e)
{
Error = e.Message;
throw;
}
static Task Load(string path)
{
if (resourceLoads.TryGetValue(path, out var pending) && pending is not null && !pending.IsFaulted && !pending.IsCanceled)
return pending;
return resourceLoads[path] = LoadResource(path);
}
static async Task LoadResource(string path)
{
Clothing clothing = null;
for (var attempt = 0; attempt < 3; attempt++)
{
clothing = await ClothingResources.Load(path);
if (clothing is not null && !clothing.IsError)
break;
if (attempt < 2)
await Task.Delay(150);
}
if (clothing is null || clothing.IsError)
throw new InvalidOperationException("A native appearance resource is unavailable: " + path);
foreach (var modelPath in new[]
{
clothing.HumanSkinModel,
clothing.HumanAltModel,
clothing.HumanAltFemaleModel
}.Where(x => !string.IsNullOrEmpty(x)).Distinct(StringComparer.OrdinalIgnoreCase))
{
var model = await Model.LoadAsync(modelPath);
if (model is null || model.IsError)
throw new InvalidOperationException("A native appearance model is unavailable: " + modelPath);
}
Revision++;
}
}
}
Game
library
using System;
namespace CharacterEditor;
/// <summary>Version 2 identity shape, independent of skin. Weights are in human01..human07 order.</summary>
public sealed class CharacterBaseShape
{
public float[] HeadWeights { get; set; } = new[]
{
0f,
0f,
1f,
0f,
0f,
0f,
0f
};
public List<PhotoFace> PhotoFaces { get; set; } = new();
public Dictionary<string, FaceRegionBlend> Regions { get; set; } = new();
[System.Text.Json.Serialization.JsonIgnore]
public float TotalInfluence => HeadWeights.Sum() + (PhotoFaces?.Sum(x => x.Influence) ?? 0);
public float NativeShare(int i) => TotalInfluence > 0 ? HeadWeights[i] / TotalInfluence : (i == 2 ? 1 : 0);
public float PhotoShare(PhotoFace face) => TotalInfluence > 0 ? face.Influence / TotalInfluence : 0;
public float MaleBody { get; set; } = 1;
public string ClothingVariant { get; set; }
public float BreastSize { get; set; } = .5f;
public float BulgeSize { get; set; } = .5f;
[System.Text.Json.Serialization.JsonIgnore]
public bool Valid => HeadWeights?.Length == 7 && HeadWeights.All(Unit) && PhotoFaces is not null && PhotoFaces.Count <= 4 && PhotoFaces.All(x => x?.Valid == true) && PhotoFaces.Select(x => x.Id).Distinct().Count() == PhotoFaces.Count && Regions is not null && Regions.Count <= FaceRegions.Ids.Length && Regions.All(x => FaceRegions.Ids.Contains(x.Key) && x.Value?.Valid(PhotoFaces) == true) && Unit(MaleBody) && Unit(BreastSize) && Unit(BulgeSize) && ClothingVariant is null or "male" or "female";
static bool Unit(float x) => float.IsFinite(x) && x >= 0 && x <= 1;
public float[] Normalized()
{
if (!Valid)
return new[]
{
0f,
0f,
1f,
0f,
0f,
0f,
0f
};
float sum = TotalInfluence;
return sum > 0 ? HeadWeights.Select((x, i) => (x + PhotoFaces.Sum(f => f.Influence * f.HeadWeights[i])) / sum).ToArray() : new[]
{
0f,
0f,
1f,
0f,
0f,
0f,
0f
};
}
public CharacterBaseShape DeepCopy() => new()
{
HeadWeights = HeadWeights?.ToArray(),
PhotoFaces = PhotoFaces?.Select(x => x?.DeepCopy()).ToList(),
Regions = Regions?.ToDictionary(x => x.Key, x => x.Value.DeepCopy()),
MaleBody = MaleBody,
BreastSize = BreastSize,
BulgeSize = BulgeSize,
ClothingVariant = ClothingVariant
};
}
public static class UnifiedHuman
{
public const string ModelPath = "models/characterkit/unified_human.vmdl";
public static void Enable(CharacterAppearanceData data)
{
if (data.BaseShape is not null)
return;
bool female = AppearanceCatalog.IsFemale(data);
var index = AppearanceCatalog.Skins.ToList().FindIndex(s => s.ResourcePath == data.SkinPath);
data.BaseShape = new()
{
MaleBody = female ? 0 : 1,
HeadWeights = new float[7],
BreastSize = female ? data.BodyFeatureSize : .5f,
BulgeSize = female ? .5f : data.BodyFeatureSize
};
data.BaseShape.HeadWeights[Math.Max(0, index)] = 1;
data.Version = 2;
}
public static float MaleAmount(CharacterAppearanceData a) => a.BaseShape?.MaleBody ?? (AppearanceCatalog.IsFemale(a) ? 0 : 1);
public static string PresentationSkin(CharacterAppearanceData a) => a.BaseShape is null ? a.SkinPath : (MaleAmount(a) >= .5f ? "models/citizen_clothes/body/human03.clothing" : "models/citizen_clothes/body/human04.clothing");
public static void Apply(SkinnedModelRenderer body, CharacterAppearanceData a)
{
if (a.BaseShape is null)
return;
var weights = FaceRegions.ReferenceWeights(a.BaseShape);
for (int i = 0; i < 7; i++)
if (i != 2)
body.Morphs.Set($"ce_human{i + 1:00}", weights[i]);
FaceRegions.Apply(body, a.BaseShape, weights);
body.Morphs.Set("ce_body_female", 1 - a.BaseShape.MaleBody);
}
public static string HeadMaterial(string skin)
{
var id = System.IO.Path.GetFileNameWithoutExtension(skin);
var suffix = id switch
{
"human01" => "malehead01",
"human02" => "femalehead01",
"human03" => "malehead02",
"human04" => "femalehead02",
"human05" => "malehead03",
"human06" => "malehead04",
_ => "malehead05"
};
return $"models/citizen_human/heads/citizen_human_{suffix}/citizen_human_{suffix}.vmat";
}
public static Material SkinMaterial(Material slot, string skin, bool unifiedBody = true)
{
var original = HumanMarkingModels.NativeMaterial(slot);
if (original.Name.Contains("/heads/") && original.ShaderName == "shaders/skin.shader" && !original.Name.EndsWith("/mouth.vmat"))
return Material.Load(HeadMaterial(skin));
if (original.Name.Contains("/bodies/") && original.ShaderName == "shaders/skin.shader")
{
// The unified surface uses one body material family. Female chest normal maps
// encode breast contours that become distorted on the flattened male endpoint.
var id = System.IO.Path.GetFileNameWithoutExtension(skin);
bool female = !unifiedBody && (id is "human02" or "human04"), dark = id is "human01" or "human02" or "human06";
return Material.Load($"models/citizen_human/bodies/{(female ? "female" : "male")}/{(female ? (dark ? "female_body_mode_dark" : "female_body_mode_light") : (dark ? "body_mode_dark" : "body_male_light"))}.vmat");
}
return original;
}
public static Color BodyToneTint(Material material, string skin)
{
if (!material.Name.Contains("/bodies/"))
return Color.White;
return BodyToneTint(skin);
}
public static Color BodyToneTint(string skin)
{
// Albedo ratios of the native female/male materials sampled just inside
// the shared neck boundary. Keep the female faces' authored complexion.
return System.IO.Path.GetFileNameWithoutExtension(skin) switch
{
"human02" => new Color(1.0192f, .9194f, .8924f),
"human04" => new Color(1.0856f, 1.0137f, .8969f),
_ => Color.White
};
}
}
Game
library
using System;
using System.Text.Json.Serialization;
namespace CharacterEditor;
/// <summary>One independently editable ink layer. Placement is normalized within a curated surface region.</summary>
public sealed class CharacterMarkingData
{
public string Style { get; set; } = "crescent";
public string Region { get; set; } = "face";
public float Horizontal { get; set; } = .5f;
public float Vertical { get; set; } = .5f;
public float Scale { get; set; } = .5f;
public float Rotation { get; set; } = .5f;
public float Spacing { get; set; } = .5f;
public MakeupLayerData Ink { get; set; } = new()
{
Red = .1f,
Green = .12f,
Blue = .16f,
Amount = .8f
};
[JsonIgnore]
public bool Valid => MarkingCatalog.ValidPlacement(Style, Region) && Ink?.Valid == true && new[]
{
Horizontal,
Vertical,
Scale,
Rotation,
Spacing
}.All(x => float.IsFinite(x) && x >= 0 && x <= 1) && (Style != "barbed-band" || (Horizontal == .5f && Rotation == .5f));
public CharacterMarkingData DeepCopy() => new()
{
Style = Style,
Region = Region,
Horizontal = Horizontal,
Vertical = Vertical,
Scale = Scale,
Rotation = Rotation,
Spacing = Spacing,
Ink = Ink?.DeepCopy()
};
}
Game
library
namespace CharacterEditor;
// Generated by ce_calibrate_photo_profile from native topology and packed engine deformers.
public static class PhotoProfileAnchors
{
public static readonly int[] Ids={10,168,4,1,13,14,152};
public static readonly string[] Controls={"head","jaw","eyes","nosewidth","nostrils","noseheight","mouthwidth","nose","chin","mouthprojection","chinheight"};
public static readonly Vector3[][] Heads={
new Vector3[]{new(3.6992545f,-0.066790596f,68.14279f),new(5.2946215f,0.11305821f,64.60188f),new(5.959813f,-0.041913833f,63.655262f),new(6.1343274f,-0.058205567f,63.346245f),new(5.8273835f,0.07009718f,61.76828f),new(5.7244573f,0.09594756f,61.383133f),new(4.6132507f,-0.040823318f,59.63866f)},
new Vector3[]{new(3.6878302f,-0.014352961f,68.02986f),new(5.085779f,0.0712296f,64.49504f),new(5.841306f,-0.03587849f,63.486412f),new(6.0319343f,-0.045126636f,63.211014f),new(5.53363f,0.09146978f,61.719315f),new(5.485255f,0.1368592f,61.244686f),new(4.676547f,-0.012164734f,59.841347f)},
new Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2011194f,0.07334158f,64.609566f),new(5.979442f,-0.033484418f,63.572296f),new(6.1241097f,-0.03964384f,63.281433f),new(5.778057f,0.053285245f,61.74f),new(5.719415f,0.069648676f,61.50413f),new(4.7025223f,-0.06262799f,59.70981f)},
new Vector3[]{new(3.5149546f,-0.009116913f,68.08719f),new(5.0824428f,0.056152936f,64.4079f),new(5.857973f,-0.08385171f,63.312653f),new(6.0649567f,-0.10289183f,62.99997f),new(5.4969316f,0.05211438f,61.666157f),new(5.502199f,0.10385137f,61.29811f),new(4.67235f,-0.1021266f,59.774113f)},
new Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2132f,0.087325275f,64.6164f),new(5.957733f,0.064072326f,63.39234f),new(6.137288f,0.064377435f,63.1475f),new(5.6911297f,0.052945673f,61.71075f),new(5.713699f,0.06934876f,61.489025f),new(4.702329f,-0.055987295f,59.563744f)},
new Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2012277f,0.1540686f,64.59866f),new(6.0016494f,-0.08192249f,63.62896f),new(6.165094f,-0.049669266f,63.36159f),new(5.7699647f,0.05389818f,61.78059f),new(5.719415f,0.06964957f,61.493782f),new(4.700662f,-0.0723621f,59.84928f)},
new Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.173374f,0.09990916f,64.60435f),new(5.9552946f,0.042667035f,63.536186f),new(6.1126857f,0.03899601f,63.296906f),new(5.6911297f,0.0529435f,61.727646f),new(5.713699f,0.07963325f,61.420948f),new(4.702329f,-0.05598729f,59.74437f)}
};
// [head * Controls.Length * 2 + control * 2 + positiveEndpoint]
public static readonly Vector3[][] Deltas={
new Vector3[]{new(-0.13209844f,0.0031800792f,-0.07652283f),new(-0.24389982f,-0.0069441274f,-0.03074646f),new(-0.23990822f,0.0022496209f,-0.010990143f),new(-0.23487473f,0.0029714853f,-0.0055236816f),new(-0.23757076f,-0.003810294f,0.020923615f),new(-0.23392248f,-0.0052277073f,0.02753067f),new(-0.2026701f,0.0023912787f,0.061531067f)},
new Vector3[]{new(0.13209844f,-0.0031800792f,0.07652283f),new(0.24389982f,0.0069441274f,0.03074646f),new(0.23990822f,-0.0022496209f,0.010990143f),new(0.23487473f,-0.0029714853f,0.0055236816f),new(0.23757076f,0.003810294f,-0.020923615f),new(0.23392248f,0.0052277073f,-0.02753067f),new(0.2026701f,-0.0023912787f,-0.061531067f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.011735871f,0f),new(0f,0.016297556f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.011735871f,0f),new(0f,-0.016297556f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3449974f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.0107391365f,0f),new(0f,-0.0056006014f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.010739133f,0f),new(0f,0.0056006014f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.041153908f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.0532794f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.13788319f,0f,0f),new(-0.052534103f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.19303656f,0f,0f),new(0.07354784f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.47891617f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.31927872f)},
new Vector3[]{new(-0.1366136f,0.0007089311f,-0.07763672f),new(-0.24782324f,-0.004627906f,-0.03035736f),new(-0.2453003f,0.002008915f,-0.00851059f),new(-0.23964739f,0.0023905002f,-0.0034942627f),new(-0.24498606f,-0.0053994358f,0.02362442f),new(-0.23736048f,-0.007896334f,0.031650543f),new(-0.20918512f,0.0007255161f,0.05886841f)},
new Vector3[]{new(0.1366136f,-0.0007089311f,0.07763672f),new(0.24782324f,0.004627906f,0.03035736f),new(0.2453003f,-0.002008915f,0.00851059f),new(0.23964739f,-0.0023905002f,0.0034942627f),new(0.24498606f,0.0053994358f,-0.02362442f),new(0.23736048f,0.007896334f,-0.031650543f),new(0.20918512f,-0.0007255161f,-0.05886841f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.010045975f,0f),new(0f,0.012437068f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.010045975f,0f),new(0f,-0.012437068f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.33958435f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.33958435f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.018210128f,0f),new(0f,-0.0033276677f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.018210128f,0f),new(0f,0.0033276677f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4882121f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4882121f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.2155137f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.27901363f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.17917538f,0f,0f),new(-0.02188301f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.25084543f,0f,0f),new(0.03063631f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5400009f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3600006f)},
new Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.2449646f,-0.004605703f,-0.031585693f),new(-0.2398367f,0.001790762f,-0.009563446f),new(-0.23560143f,0.0020335242f,-0.004512787f),new(-0.2387433f,-0.002935592f,0.021694183f),new(-0.23645878f,-0.0038393363f,0.02576828f),new(-0.2047453f,0.0036357194f,0.059688568f)},
new Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.2449646f,0.004605703f,0.031585693f),new(0.2398367f,-0.001790762f,0.009563446f),new(0.23560143f,-0.0020335242f,0.004512787f),new(0.2387433f,0.002935592f,-0.021694183f),new(0.23645878f,0.0038393363f,-0.02576828f),new(0.2047453f,-0.0036357194f,-0.059688568f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009375636f,0f),new(0f,0.011100274f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009375636f,0f),new(0f,-0.011100274f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.008732874f,0f),new(0f,-0.008089323f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.008732874f,0f),new(0f,0.008089326f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.1130929f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.14641476f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.14750004f,0f,0f),new(-0.104530334f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.20650053f,0f,0f),new(0.14634228f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5328598f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.35523987f)},
new Vector3[]{new(-0.13274455f,0.0004590759f,-0.08004761f),new(-0.24919367f,-0.0036709309f,-0.028762817f),new(-0.24560404f,0.004687473f,-0.0054626465f),new(-0.238554f,0.0053960904f,0f),new(-0.24498224f,-0.0030967854f,0.02476883f),new(-0.23813248f,-0.0059928596f,0.03068924f),new(-0.20696688f,0.006031759f,0.059539795f)},
new Vector3[]{new(0.13274455f,-0.0004590759f,0.08004761f),new(0.24919367f,0.0036709309f,0.028762817f),new(0.24560404f,-0.004687473f,0.0054626465f),new(0.238554f,-0.0053960904f,0f),new(0.24498224f,0.0030967854f,-0.02476883f),new(0.23813248f,0.0059928596f,-0.03068924f),new(0.20696688f,-0.006031759f,-0.059539795f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,-5.4202974E-05f,0f),new(0f,0.02339752f,0f),new(0f,0.020724386f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,5.4202974E-05f,0f),new(0f,-0.02339752f,0f),new(0f,-0.020724379f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,-0.0011901855f),new(0f,0f,-0.34381104f),new(0f,0f,-0.24817657f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0.0011901855f),new(0f,0f,0.34381104f),new(0f,0f,0.24817657f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.01034468f,0f),new(0f,-0.0050033107f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.01034468f,0f),new(0f,0.0050033107f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(-0.001709938f,0f,0f),new(-0.49428988f,0f,0f),new(-0.3567996f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0.001709938f,0f,0f),new(0.49428988f,0f,0f),new(0.3567996f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.15790367f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.20442915f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.17864943f,0f,0f),new(-0.043359756f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.2501092f,0f,0f),new(0.060703754f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.53889465f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.35926437f)},
new Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.24469805f,-0.0054651946f,-0.03161621f),new(-0.24176359f,-0.0034667253f,-0.0066337585f),new(-0.23539543f,-0.0032922663f,-0.002357483f),new(-0.24088907f,-0.0029880553f,0.02273941f),new(-0.23634768f,-0.0038248226f,0.026042938f),new(-0.19970894f,0.0031703971f,0.060806274f)},
new Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.24469805f,0.0054651946f,0.03161621f),new(0.24176359f,0.0034667253f,0.0066337585f),new(0.23539543f,0.0032922626f,0.002357483f),new(0.24088907f,0.0029880553f,-0.02273941f),new(0.23634768f,0.0038248226f,-0.026042938f),new(0.19970894f,-0.0031703971f,-0.060806274f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.017940246f,0f),new(0f,-0.017056447f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.017940246f,0f),new(0f,0.017056443f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.32645035f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.32645035f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009510666f,0f),new(0f,-0.0076843873f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009510666f,0f),new(0f,0.0076843947f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4693303f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4693303f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.015622139f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.020225048f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.16166735f,0f,0f),new(-0.099727154f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.22633457f,0f,0f),new(0.13961792f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.4568138f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.30454254f)},
new Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.2450552f,-0.009678587f,-0.03138733f),new(-0.23850155f,0.004340723f,-0.010414124f),new(-0.23345089f,0.0025077388f,-0.005706787f),new(-0.2396326f,-0.002984602f,0.021102905f),new(-0.2362647f,-0.003836237f,0.025924683f),new(-0.2093401f,0.004296787f,0.05846405f)},
new Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.2450552f,0.009678587f,0.03138733f),new(0.23850155f,-0.004340723f,0.010414124f),new(0.23345089f,-0.0025077388f,0.005706787f),new(0.2396326f,0.002984602f,-0.021102905f),new(0.2362647f,0.003836237f,-0.025924683f),new(0.2093401f,-0.004296787f,-0.05846405f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.022938296f,0f),new(0f,0.013907395f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.022938296f,0f),new(0f,-0.013907395f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009057671f,0f),new(0f,-0.0077751838f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009057675f,0f),new(0f,0.00777518f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.22866583f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.29604006f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.15124607f,0f,0f),new(-0.10046959f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.21174479f,0f,0f),new(0.14065742f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5400009f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3600006f)},
new Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.24530983f,-0.006316647f,-0.031700134f),new(-0.24099922f,-0.002302207f,-0.0090408325f),new(-0.23603249f,-0.0020077005f,-0.004776001f),new(-0.24115801f,-0.0029912703f,0.022464752f),new(-0.23502398f,-0.004367456f,0.027061462f),new(-0.2059083f,0.0032688156f,0.05939865f)},
new Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.24530983f,0.006316647f,0.031700134f),new(0.24099922f,0.002302207f,0.0090408325f),new(0.23603249f,0.0020077005f,0.004776001f),new(0.24115801f,0.0029912703f,-0.022464752f),new(0.23502398f,0.004367456f,-0.027061462f),new(0.2059083f,-0.0032688156f,-0.05939865f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.011946769f,0f),new(0f,-0.010918882f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.011946768f,0f),new(0f,0.0109188855f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009599518f,0f),new(0f,-0.0062645525f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009599518f,0f),new(0f,0.0062645525f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.14241362f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.18437433f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.16318464f,0f,0f),new(-0.07080078f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.2284584f,0f,0f),new(0.099121094f,0f,0f),new(0f,0f,0f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5386009f)},
new Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.359066f)}
};
}
Game
library
namespace CharacterEditor;
public static partial class WardrobeCatalog
{
public static IReadOnlyList<WardrobeItem> Items=>entries??=CreateEntries();
static WardrobeItem[] entries;
static WardrobeItem[] CreateEntries()=>new WardrobeItem[]{
new("Denim Skirt Black","Bottoms","Bottoms","models/citizen_clothes/dress/denim_skirt/denim_skirt_black.clothing","LeftThigh, RightThigh, LeftKnee, RightKnee","","","Skirt"),
new("Denim Skirt Blue","Bottoms","Bottoms","models/citizen_clothes/dress/denim_skirt/denim_skirt_blue.clothing","LeftThigh, RightThigh, LeftKnee, RightKnee","","","Skirt"),
new("Denim Skirt Green","Bottoms","Bottoms","models/citizen_clothes/dress/denim_skirt/denim_skirt_green.clothing","LeftThigh, RightThigh, LeftKnee, RightKnee","","","Skirt"),
new("Denim Skirt Leapard","Bottoms","Bottoms","models/citizen_clothes/dress/denim_skirt/denim_skirt_leapard.clothing","LeftThigh, RightThigh, LeftKnee, RightKnee","","","Skirt"),
new("Dotted Dress","Outfits","Outfits","models/citizen_clothes/dress/dotted_dress/dotted_dress.clothing","Chest, Groin, LeftThigh, RightThigh","","Chest","Dress"),
new("Grand Ball Gown","Outfits","Outfits","models/citizen_clothes/dress/posh_dress/models/posh_dress.clothing","Chest, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Chest, Legs","Dress"),
new("Simple Dress","Outfits","Outfits","models/citizen_clothes/dress/simple_dress/simple_dress.clothing","Chest, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Chest","Dress"),
new("Simple Dress Black","Outfits","Outfits","models/citizen_clothes/dress/simple_dress/simple_dress_black.clothing","Chest, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Chest","Dress"),
new("Simple Dress Blue","Outfits","Outfits","models/citizen_clothes/dress/simple_dress/simple_dress_blue.clothing","Chest, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Chest","Dress"),
new("Standard Skirt","Bottoms","Bottoms","models/citizen_clothes/dress/standard_skirt/standard_skirt.clothing","LeftThigh, RightThigh, LeftKnee, RightKnee","","","Skirt"),
new("Pirate Outfit","Outfits","Outfits","models/citizen_clothes/full_outfit/pirate_outfit/pirate_outfit.clothing","Chest, LeftArm, RightArm, LeftWrist, RightWrist, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot","Chest, LeftArm, RightArm, LeftWrist, RightWrist, LeftHand, RightHand, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot, Waist","Chest, Legs, Feet","Costume"),
new("Nerdy Glasses","Glasses","Glasses","models/citizen_clothes/glasses/nerdy_glasses/nerdy_glasses.clothing","Glasses","","","GlassesEye"),
new("Party Glasses","Glasses","Glasses","models/citizen_clothes/glasses/party_glasses/party_glasses.clothing","Glasses","","","GlassesSun"),
new("Boxing Gloves","Gloves","Gloves","models/citizen_clothes/gloves/boxing_gloves/boxing_gloves.clothing","LeftHand, RightHand","","Hands","Gloves"),
new("Boxing Gloves Blue","Gloves","Gloves","models/citizen_clothes/gloves/boxing_gloves/boxing_gloves_blue.clothing","LeftHand, RightHand","","Hands","Gloves"),
new("Boxing Gloves Red","Gloves","Gloves","models/citizen_clothes/gloves/boxing_gloves/boxing_gloves_red.clothing","LeftHand, RightHand","","Hands","Gloves"),
new("Fingerless Gloves","Gloves","Gloves","models/citizen_clothes/gloves/fingerless_gloves/fingerless_gloves.clothing","LeftHand, RightHand","","Hands","Gloves"),
new("Goalie Gloves","Gloves","Gloves","models/citizen_clothes/gloves/goalie_gloves/goalie_gloves.clothing","LeftHand, RightHand","","Hands","Gloves"),
new("Blue Rubber Gloves","Gloves","Gloves","models/citizen_clothes/gloves/rubber_gloves/rubber_gloves.clothing","LeftHand, RightHand","","Hands","Gloves"),
new("Black Rubber Gloves","Gloves","Gloves","models/citizen_clothes/gloves/rubber_gloves/rubber_gloves_black.clothing","LeftHand, RightHand","","Hands","Gloves"),
new("Mask","Hats","Hats","models/citizen_clothes/hat/balaclava/balaclava.clothing","HeadTop, HeadBottom","","","HatBeanie"),
new("Bandana Mask","Hats","Hats","models/citizen_clothes/hat/bandana_mask/bandana_mask.clothing","","","","HeadSpecial"),
new("Baseball Cap","Hats","Hats","models/citizen_clothes/hat/baseball_cap/baseball_cap.clothing","HeadTop","","","Hat"),
new("Baseball Cap Black","Hats","Hats","models/citizen_clothes/hat/baseball_cap/baseball_cap_black.clothing","HeadTop","","","Hat"),
new("Baseball Cap Blue White","Hats","Hats","models/citizen_clothes/hat/baseball_cap/baseball_cap_blue_white.clothing","HeadTop","","","Hat"),
new("Baseball Cap Brown","Hats","Hats","models/citizen_clothes/hat/baseball_cap/baseball_cap_brown.clothing","HeadTop","","","Hat"),
new("Beanie","Hats","Hats","models/citizen_clothes/hat/beanie/beanie.clothing","HeadTop","","","HatBeanie"),
new("Beanie Green","Hats","Hats","models/citizen_clothes/hat/beanie/beanie_green.clothing","HeadTop","","","HatBeanie"),
new("Beanie Red","Hats","Hats","models/citizen_clothes/hat/beanie/beanie_red.clothing","HeadTop","","","HatBeanie"),
new("Boxing Headgear","Hats","Hats","models/citizen_clothes/hat/boxing_headgear/boxing_headgear.clothing","HeadTop","","","HatCostume"),
new("Boxing Headgear Blue","Hats","Hats","models/citizen_clothes/hat/boxing_headgear/boxing_headgear_blue.clothing","HeadTop","","","HatCostume"),
new("Boxing Headgear Red","Hats","Hats","models/citizen_clothes/hat/boxing_headgear/boxing_headgear_red.clothing","HeadTop, HeadBottom","","","HatCostume"),
new("Bucket Hat","Hats","Hats","models/citizen_clothes/hat/bucket_hat/bucket_hat.clothing","HeadTop","","","Hat"),
new("Bucket Hat Black","Hats","Hats","models/citizen_clothes/hat/bucket_hat/bucket_hat_black.clothing","HeadTop","","","Hat"),
new("Chef's Hat","Hats","Hats","models/citizen_clothes/hat/chefs_hat/chefs_hat.clothing","HeadTop","","","HatUniform"),
new("Cowboy Hat","Hats","Hats","models/citizen_clothes/hat/cowboy_hat/cowboy_hat.clothing","HeadTop","","","HatCostume"),
new("Fedora","Hats","Hats","models/citizen_clothes/hat/fedora/fedora.clothing","HeadTop","","","HatFormal"),
new("Fedora Brown","Hats","Hats","models/citizen_clothes/hat/fedora/fedora_brown.clothing","HeadTop","","","HatFormal"),
new("Flatcap","Hats","Hats","models/citizen_clothes/hat/flatcap/flatcap.clothing","HeadTop","","","Hat"),
new("Gasmask","Hats","Hats","models/citizen_clothes/hat/gasmask/gasmask.clothing","HeadBottom","","","HatUniform"),
new("Hair Net","Hats","Hats","models/citizen_clothes/hat/hair_net/hair_net.clothing","HeadTop","","","HatUniform"),
new("Hair Net White","Hats","Hats","models/citizen_clothes/hat/hair_net/hair_net_white.clothing","HeadTop","","","HatUniform"),
new("Hard Hat","Hats","Hats","models/citizen_clothes/hat/hard_hat/hard_hat.clothing","HeadTop","","","HatUniform"),
new("Headphones","Hats","Hats","models/citizen_clothes/hat/headphones/models/headphones.clothing","","","","HeadTech"),
new("Police Hat","Hats","Hats","models/citizen_clothes/hat/police_hat/police_hat.clothing","HeadTop","","","HatUniform"),
new("Racer Helmet","Hats","Hats","models/citizen_clothes/hat/racer_helmet/racer_helmet.clothing","HeadTop","","","HatCostume"),
new("Scifi Trooper Helmet","Hats","Hats","models/citizen_clothes/hat/scifi_trooper_helmet/scifi_trooper_helmet.clothing","HeadTop","","","HatCostume"),
new("Skater Helmet","Hats","Hats","models/citizen_clothes/hat/skater_helmet/skater_helmet.clothing","HeadTop","","","HatUniform"),
new("Skater Helmet Camo","Hats","Hats","models/citizen_clothes/hat/skater_helmet/skater_helmet_camo.clothing","HeadTop","","","HatUniform"),
new("Skater Helmet White","Hats","Hats","models/citizen_clothes/hat/skater_helmet/skater_helmet_white.clothing","HeadTop","","","HatUniform"),
new("Tactical Helmet","Hats","Hats","models/citizen_clothes/hat/tactical_helmet/tactical_helmet.clothing","HeadTop, Glasses","","","HatUniform"),
new("Army Helmet","Hats","Hats","models/citizen_clothes/hat/tactical_helmet/tactical_helmet_army.clothing","HeadTop, Glasses","","","HatUniform"),
new("Trapper Hat","Hats","Hats","models/citizen_clothes/hat/trapper_hat/trapper_hat.clothing","HeadTop","","","HatBeanie"),
new("Trapper Hat Cloth","Hats","Hats","models/citizen_clothes/hat/trapper_hat/trapper_hat_cloth.clothing","HeadTop","","","HatBeanie"),
new("War Helmet","Hats","Hats","models/citizen_clothes/hat/war_helmet/war_helmet.clothing","HeadTop","","","HatUniform"),
new("Biker Jacket","Outerwear","Outerwear","models/citizen_clothes/jacket/biker_jacket/biker_jacket.clothing","","LeftArm, RightArm","","Jacket"),
new("Bomber Jacket","Outerwear","Outerwear","models/citizen_clothes/jacket/bomber_jacket/bomber_jacket.clothing","","LeftArm, RightArm","","Jacket"),
new("Black Leather Jacket","Outerwear","Outerwear","models/citizen_clothes/jacket/brown_leather_jacket/black_leather_jacket.clothing","","LeftArm, RightArm","","Jacket"),
new("Brown Leather Jacket","Outerwear","Outerwear","models/citizen_clothes/jacket/brown_leather_jacket/brown_leather_jacket.clothing","","LeftArm, RightArm","","Jacket"),
new("Hoodie","Tops","Tops","models/citizen_clothes/jacket/hoodie/hoodie.clothing","Chest","LeftArm, RightArm","Chest","Hoodie"),
new("Black Hoodie","Tops","Tops","models/citizen_clothes/jacket/hoodie/hoodie_black.clothing","LeftArm, RightArm, LeftWrist, RightWrist","Chest","Chest","Hoodie"),
new("Lab Coat","Outerwear","Outerwear","models/citizen_clothes/jacket/labcoat/labcoat.clothing","","LeftArm, RightArm","","Coat"),
new("Puffer Jacket","Outerwear","Outerwear","models/citizen_clothes/jacket/puffer_jacket/puffer_jacket.clothing","","LeftArm, RightArm","","Jacket"),
new("Puffer Jacket Blue","Outerwear","Outerwear","models/citizen_clothes/jacket/puffer_jacket/puffer_jacket_blue.clothing","","LeftArm, RightArm","","Jacket"),
new("Puffer Jacket Orange","Outerwear","Outerwear","models/citizen_clothes/jacket/puffer_jacket/puffer_jacket_orange.clothing","","LeftArm, RightArm","","Jacket"),
new("Rain Coat","Outerwear","Outerwear","models/citizen_clothes/jacket/raincoat/raincoat.clothing","","LeftArm, RightArm","","Coat"),
new("Sleeveless Jack","Outerwear","Outerwear","models/citizen_clothes/jacket/sleeveless_jacket/sleeveless_jacket.clothing","","LeftArm, RightArm","","Gilet"),
new("Wind Breaker","Outerwear","Outerwear","models/citizen_clothes/jacket/wind_breaker/wind_breaker.clothing","","LeftArm, RightArm","","Coat"),
new("Winter Coat","Outerwear","Outerwear","models/citizen_clothes/jacket/winter_coat/winter_coat.clothing","","LeftArm, RightArm","","Coat"),
new("Cardigan","Outerwear","Outerwear","models/citizen_clothes/jumper/cardigan/cardigan.clothing","","LeftArm, RightArm","","Cardigan"),
new("Cardigan Pink","Outerwear","Outerwear","models/citizen_clothes/jumper/cardigan/cardigan_colored.clothing","","LeftArm, RightArm","","Cardigan"),
new("Cardigan Dark","Outerwear","Outerwear","models/citizen_clothes/jumper/cardigan/cardigan_dark.clothing","","LeftArm, RightArm","","Cardigan"),
new("Cardigan Dirty","Outerwear","Outerwear","models/citizen_clothes/jumper/cardigan/cardigan_dirty.clothing","","LeftArm, RightArm","","Cardigan"),
new("Loose Jumper","Tops","Tops","models/citizen_clothes/jumper/loose_jumper/loose_jumper.clothing","Chest","LeftArm, RightArm","Chest","Knitwear"),
new("Goalie Jumper","Tops","Tops","models/citizen_clothes/jumper/loose_jumper/loose_jumper_goalie.clothing","Chest","LeftArm, RightArm","Chest","Knitwear"),
new("Loose Jumper Wooly","Tops","Tops","models/citizen_clothes/jumper/loose_jumper/loose_jumper_wooly.clothing","Chest","LeftArm, RightArm","Chest","Knitwear"),
new("Sleeveless Jumper","Tops","Tops","models/citizen_clothes/jumper/sleeveless_jumper/sleeveless_jumper.clothing","","LeftArm, RightArm","","Knitwear"),
new("Sleeveless Jumper Grey","Tops","Tops","models/citizen_clothes/jumper/sleeveless_jumper/sleeveless_jumper_grey.clothing","","LeftArm, RightArm","","Knitwear"),
new("Sleeveless Jumper Red","Tops","Tops","models/citizen_clothes/jumper/sleeveless_jumper/sleeveless_jumper_red.clothing","","LeftArm, RightArm","","Knitwear"),
new("Pearl Necklace","Accessories","necklace","models/citizen_clothes/necklace/pearl_necklace/pearl_necklace.clothing","Chin","","","NecklacePendant"),
new("Vintage Necklace","Accessories","necklace","models/citizen_clothes/necklace/vintage_necklace/vintage_necklace.clothing","Chin","","","NecklacePendant"),
new("Santa Hat 2024","Hats","Hats","models/citizen_clothes/seasonal/2024/santa/santahat.clothing","HeadTop","","","HatCostume"),
new("Santa Jacket 2024","Outerwear","Outerwear","models/citizen_clothes/seasonal/2024/santa/santajacket.clothing","LeftArm, RightArm","LeftArm, RightArm","Chest","Coat"),
new("Santa Trousers 2024","Bottoms","Bottoms","models/citizen_clothes/seasonal/2024/santa/santatrousers.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot","Legs, Feet","Trousers"),
new("Easter Bonnet","Hats","Hats","models/citizen_clothes/seasonal/2025/easter_farmer/easter_bonnet/easter_bonnet.clothing","HeadTop","","","HatCostume"),
new("Easter Dungarees","Outfits","Outfits","models/citizen_clothes/seasonal/2025/easter_farmer/easter_dungarees/models/easter_dungarees.clothing","Chest, LeftArm, RightArm, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","Chest, LeftArm, RightArm, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","Chest, Legs","Suit"),
new("Easter Slippers","Shoes","Shoes","models/citizen_clothes/seasonal/2025/easter_farmer/easter_slippers/models/easter_slippers.clothing","LeftFoot, RightFoot","","Feet","Footwear"),
new("Valentines Bear Head 2025","Hats","Hats","models/citizen_clothes/seasonal/2025/valentines_bear/bear_head.clothing","HeadTop, HeadBottom","HeadTop, HeadBottom","Head","HatCostume"),
new("Valentines Bear Top 2025","Outfits","Outfits","models/citizen_clothes/seasonal/2025/valentines_bear/bear_top.clothing","Chest, LeftArm, RightArm, LeftWrist, RightWrist, LeftHand, RightHand","LeftArm, RightArm, LeftWrist, RightWrist, LeftHand, RightHand","Chest, Hands","Costume"),
new("Valentines Bear Trousers 2025","Bottoms","Bottoms","models/citizen_clothes/seasonal/2025/valentines_bear/bear_trousers.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot","Legs, Feet","Trousers"),
new("Wizard Beard","Accessories","costume_beard","models/citizen_clothes/seasonal/2025/wizard_outfit/wizard_beard/wizard_beard.clothing","Face","","","FacialHairBeard"),
new("Wizard Gown","Outerwear","Outerwear","models/citizen_clothes/seasonal/2025/wizard_outfit/wizard_gown/wizard_gown.clothing","Chest, LeftArm, RightArm","Chest, LeftArm, RightArm, LeftWrist","Chest","Coat"),
new("Wizard Hat","Hats","Hats","models/citizen_clothes/seasonal/2025/wizard_outfit/wizard_hat/wizard_hat.clothing","HeadTop","","","HatCostume"),
new("Wizard Trousers","Bottoms","Bottoms","models/citizen_clothes/seasonal/2025/wizard_outfit/wizard_trousers/wizard_trousers.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot","","Legs, Feet","Trousers"),
new("Army Shirt","Tops","Tops","models/citizen_clothes/shirt/army_shirt/army_shirt.clothing","Chest, LeftArm, RightArm","","Chest","Shirt"),
new("Chef Shirt","Tops","Tops","models/citizen_clothes/shirt/chef_shirt/chef_shirt.clothing","Chest, LeftArm, RightArm","","Chest","Shirt"),
new("Fisherman Jumpsuit","Outfits","Outfits","models/citizen_clothes/shirt/fisherman_jumpsuit/fisherman_jumpsuit.clothing","Chest, LeftArm, RightArm, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot","","Chest, Legs, Feet","Suit"),
new("Flannel Shirt","Tops","Tops","models/citizen_clothes/shirt/flannel_shirt/flannel_shirt.clothing","Chest, LeftArm, RightArm","","Chest","Shirt"),
new("Blue Shirt","Tops","Tops","models/citizen_clothes/shirt/flannel_shirt/variations/blue_shirt/blue_shirt.clothing","Chest, LeftArm, RightArm","","Chest","Shirt"),
new("Hawaiian Shirt","Tops","Tops","models/citizen_clothes/shirt/hawaiian_shirt/hawaiian shirt.clothing","Chest, LeftArm, RightArm","","Chest","Shirt"),
new("Blue Jumpsuit","Outfits","Outfits","models/citizen_clothes/shirt/jumpsuit/blue_jumpsuit.clothing","Chest, LeftArm, RightArm, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Chest, Legs","Suit"),
new("Prison Jumpsuit","Outfits","Outfits","models/citizen_clothes/shirt/jumpsuit/prison_jumpsuit.clothing","Chest, LeftArm, RightArm, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Chest, Legs","Suit"),
new("Leotard","Tops","Tops","models/citizen_clothes/shirt/leotard/leotard.clothing","Chest, LeftArm, RightArm, LeftWrist, RightWrist, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","","TShirt"),
new("Leotard Blue","Tops","Tops","models/citizen_clothes/shirt/leotard/leotard_blue.clothing","Chest, LeftArm, RightArm, LeftWrist, RightWrist, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","","Shirt"),
new("Loose Crop Top","Tops","Tops","models/citizen_clothes/shirt/loose_crop_top/loose_crop_top.clothing","Chest, LeftArm, RightArm","","Chest","Shirt"),
new("Loose Crop Top Pattern","Tops","Tops","models/citizen_clothes/shirt/loose_crop_top/loose_crop_top_pattern.clothing","Chest, LeftArm, RightArm","","Chest","Shirt"),
new("Loose Crop Top Purple","Tops","Tops","models/citizen_clothes/shirt/loose_crop_top/loose_crop_top_purple.clothing","Chest, LeftArm, RightArm","","Chest","Shirt"),
new("Loose Crop Top White","Tops","Tops","models/citizen_clothes/shirt/loose_crop_top/loose_crop_top_white.clothing","Chest, LeftArm, RightArm","","Chest","Shirt"),
new("Loose Shirt","Tops","Tops","models/citizen_clothes/shirt/loose_shirt/loose_shirt.clothing","Chest, LeftArm, RightArm, LeftWrist, RightWrist","","Chest","TShirt"),
new("Polo Shirt","Tops","Tops","models/citizen_clothes/shirt/polo_shirt/polo_shirt.clothing","Chest, LeftArm, RightArm","","Chest","TShirt"),
new("Polo Shirt White","Tops","Tops","models/citizen_clothes/shirt/polo_shirt/polo_shirt_white.clothing","Chest, LeftArm, RightArm","","Chest","TShirt"),
new("Tank Top ","Tops","Tops","models/citizen_clothes/shirt/tanktop/tanktop.clothing","Chest","","","TShirt"),
new("Football Tshirt","Tops","Tops","models/citizen_clothes/shirt/tshirt/football_tshirt.clothing","Chest, LeftArm, RightArm","","Chest","TShirt"),
new("Tshirt","Tops","Tops","models/citizen_clothes/shirt/tshirt/tshirt.clothing","Chest, LeftArm, RightArm","","Chest","Tops"),
new("Waistcoat and Shirt","Tops","Tops","models/citizen_clothes/shirt/waistcoat_and_shirt/waistcoat_and_shirt.clothing","Chest, LeftArm, RightArm","","Chest","Shirt"),
new("Boot Heels Black","Shoes","Shoes","models/citizen_clothes/shoes/boot_heels/boot_heels_black.clothing","LeftFoot, RightFoot","","Feet","Heels"),
new("Boot Heels Brown","Shoes","Shoes","models/citizen_clothes/shoes/boot_heels/boot_heels_brown.clothing","LeftFoot, RightFoot","","Feet","Heels"),
new("Boot Heels Leopard","Shoes","Shoes","models/citizen_clothes/shoes/boot_heels/boot_heels_leopard.clothing","LeftFoot, RightFoot","","Feet","Heels"),
new("Boot Heels Red","Shoes","Shoes","models/citizen_clothes/shoes/boot_heels/boot_heels_red.clothing","LeftFoot, RightFoot","","Feet","Heels"),
new("Army Boots","Shoes","Shoes","models/citizen_clothes/shoes/boots/army_boots.clothing","LeftFoot, RightFoot","","Feet","Boots"),
new("Black Boots","Shoes","Shoes","models/citizen_clothes/shoes/boots/black_boots.clothing","LeftFoot, RightFoot","","Feet","Boots"),
new("Boxing Boots","Shoes","Shoes","models/citizen_clothes/shoes/boxing_boots/boxing_boots.clothing","LeftFoot, RightFoot","","Feet","Boots"),
new("Boxing Boots Blue","Shoes","Shoes","models/citizen_clothes/shoes/boxing_boots/boxing_boots_blue.clothing","LeftFoot, RightFoot","","Feet","Boots"),
new("Clogs and Socks","Shoes","Shoes","models/citizen_clothes/shoes/crocs_and_socks/clogs_and_socks.clothing","LeftFoot, RightFoot","","Feet","Slippers"),
new("Flat Shoes","Shoes","Shoes","models/citizen_clothes/shoes/flatshoes/textures/flat_shoes.clothing","LeftFoot, RightFoot","","","Shoes"),
new("Football Shoes","Shoes","Shoes","models/citizen_clothes/shoes/football_shoes/football_shoes.clothing","LeftFoot, RightFoot","","Feet","Trainers"),
new("Slippers","Shoes","Shoes","models/citizen_clothes/shoes/slippers/slippers.clothing","LeftFoot, RightFoot","","","Slippers"),
new("Blue Sneakers","Shoes","Shoes","models/citizen_clothes/shoes/sneakers/blue_sneakers.clothing","LeftFoot, RightFoot","","Feet","Shoes"),
new("Sneakers","Shoes","Shoes","models/citizen_clothes/shoes/sneakers/sneakers.clothing","LeftFoot, RightFoot","","Feet","Shoes"),
new("Trainers","Shoes","Shoes","models/citizen_clothes/shoes/trainers/trainers.clothing","LeftFoot, RightFoot","","Feet","Trainers"),
new("Boxing Shorts","Bottoms","Bottoms","models/citizen_clothes/shorts/boxing_shorts/boxing_shorts.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Shorts"),
new("Boxing Shorts Blue","Bottoms","Bottoms","models/citizen_clothes/shorts/boxing_shorts/boxing_shorts_blue.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Shorts"),
new("Boxing Shorts Red","Bottoms","Bottoms","models/citizen_clothes/shorts/boxing_shorts/boxing_shorts_red.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Shorts"),
new("Football Shorts","Bottoms","Bottoms","models/citizen_clothes/shorts/football_shorts/football_shorts.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Shorts"),
new("Shorts","Bottoms","Bottoms","models/citizen_clothes/shorts/shorts/shorts.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Shorts"),
new("Summer Shorts","Bottoms","Bottoms","models/citizen_clothes/shorts/summer_shorts/summer shorts.clothing","LeftThigh, RightThigh","","Legs","Shorts"),
new("Bin Man Trousers","Bottoms","Bottoms","models/citizen_clothes/trousers/cargopants/binman_trousers.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Trousers"),
new("Brown Cargo Pants","Bottoms","Bottoms","models/citizen_clothes/trousers/cargopants/brown_cargo_pants.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Trousers"),
new("Army Cargo Pants","Bottoms","Bottoms","models/citizen_clothes/trousers/cargopants/cargo_pants_army.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Trousers"),
new("Jeans","Bottoms","Bottoms","models/citizen_clothes/trousers/jeans/jeans.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Jeans"),
new("Black Jeans","Bottoms","Bottoms","models/citizen_clothes/trousers/jeans/jeans_black.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Jeans"),
new("Loose Jeans","Bottoms","Bottoms","models/citizen_clothes/trousers/loosejeans/loose_jeans.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Jeans"),
new("Loose Trousers","Bottoms","Bottoms","models/citizen_clothes/trousers/loosetrousers/loose_trousers.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Trousers"),
new("Smart Trousers","Bottoms","Bottoms","models/citizen_clothes/trousers/smarttrousers/trousers.smart.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Trousers"),
new("Trackie Bottoms","Bottoms","Bottoms","models/citizen_clothes/trousers/trackiebottoms/trackie_bottoms.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Trousers"),
new("Black Trackie Bottoms","Bottoms","Bottoms","models/citizen_clothes/trousers/trackiebottoms/trackie_bottoms_black.clothing","Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin","","Legs","Trousers"),
new("Chef's Apron","Outerwear","Outerwear","models/citizen_clothes/vest/chefs_apron/chefs_apron.clothing","","LeftArm, RightArm","","Vest"),
new("Tactical Vest","Outerwear","Outerwear","models/citizen_clothes/vest/tactical_vest/models/tactical_vest.clothing","","LeftArm, RightArm","","Vest"),
new("Army Vest","Outerwear","Outerwear","models/citizen_clothes/vest/tactical_vest/models/tactical_vest_army.clothing","LeftArm, RightArm","","","Vest"),
new("Modern Watch","Accessories","watch","models/citizen_clothes/watch/modern_watch/modern_watch.clothing","LeftWrist","","","WristWatch"),
new("Black Modern Watch","Accessories","watch","models/citizen_clothes/watch/modern_watch/modern_watch_black.clothing","LeftWrist","","","WristWatch")
};
}
Game
library
using System;
namespace CharacterEditor;
/// <summary>Changes only the iris albedo of the curated native human eyes.</summary>
public sealed class EyeColorPresentation : IDisposable
{
static ComputeShader compositor;
static Material carrier;
readonly Dictionary<int, Material> copies = new();
readonly Dictionary<int, Texture> textures = new();
SkinnedModelRenderer target;
Model model;
string group;
Color? applied;
CharacterAppearanceData requested;
DateTime started, nextRefresh;
public static async System.Threading.Tasks.Task EnsureLoadedAsync()
{
carrier ??= await Material.LoadAsync("materials/makeup/iris_compositor.vmat");
if (carrier?.ShaderName != "shaders/human_iris.shader" || !carrier.Shader.IsValid || !carrier.Shader.Schema.Variables.Any(x => x.Name == "IrisColor"))
{
carrier = null;
throw new InvalidOperationException("The human iris shader is unavailable.");
}
}
public void Apply(SkinnedModelRenderer body, CharacterAppearanceData data, bool refresh = false)
{
if (!body.IsValid() || body.Model is null)
return;
if (target != body || model != body.Model || group != body.MaterialGroup)
{
Dispose();
target = body;
model = body.Model;
group = body.MaterialGroup;
applied = null;
}
if (data?.CustomEyeColor != true)
{
foreach (var i in copies.Keys)
body.Materials.SetOverride(i, null);
Dispose();
applied = null;
return;
}
var color = data.EyeColor;
requested = data.DeepCopy();
if (applied != color)
{
started = DateTime.UtcNow;
nextRefresh = started.AddMilliseconds(250);
}
for (var i = 0; i < body.Materials.Count; i++)
{
var original = body.Materials.GetOriginal(i);
if (original?.ShaderName != "shaders/eyeball.shader")
continue;
if (!copies.TryGetValue(i, out var copy))
copies[i] = copy = original.CreateCopy($"fate-iris-{body.Id}-{i}.vmat");
if (refresh || applied != color || !textures.ContainsKey(i))
{
var source = original.GetTexture("g_tColor");
source.MarkUsed(Math.Max(source.Width, source.Height));
if (!textures.TryGetValue(i, out var texture))
{
texture = Texture.Create(source.Width, source.Height).WithFormat(ImageFormat.RGBA8888).WithUAVBinding().WithName("Local human iris").Finish();
textures[i] = texture;
}
var attributes = new RenderAttributes();
attributes.Set("BaseColor", source);
attributes.Set("IrisColor", new Vector4(color.r, color.g, color.b, 1));
attributes.Set("Result", texture);
compositor ??= new("shaders/human_iris.shader");
compositor.DispatchWithAttributes(attributes, source.Width, source.Height, 1);
copy.Set("g_tColor", texture);
}
body.Materials.SetOverride(i, copy);
}
applied = color;
}
public void Refresh()
{
if (!target.IsValid() || requested?.CustomEyeColor != true)
return;
for (var i = 0; i < target.Materials.Count; i++)
{
var material = target.Materials.GetOriginal(i);
if (material?.ShaderName == "shaders/eyeball.shader" && material.GetTexture("g_tColor")is Texture source)
source.MarkUsed(Math.Max(source.Width, source.Height));
}
var now = DateTime.UtcNow;
if ((now - started).TotalSeconds < 3 && now >= nextRefresh)
{
Apply(target, requested, true);
nextRefresh = now.AddMilliseconds(250);
}
}
public void Dispose()
{
foreach (var texture in textures.Values)
texture.Dispose();
textures.Clear();
copies.Clear();
applied = null;
requested = null;
}
}
Game
library
using System;
using System.Threading.Tasks;
namespace CharacterEditor;
/// <summary>Native paths and portable cloud identities. Assets remain owned by their original packages.</summary>
public static class ClothingResources
{
static readonly Dictionary<string, Clothing> loaded = new(StringComparer.OrdinalIgnoreCase);
static readonly Dictionary<string, Task<Clothing>> pending = new(StringComparer.OrdinalIgnoreCase);
static readonly Dictionary<string, Task> iconLoads = new(StringComparer.OrdinalIgnoreCase);
static readonly System.Threading.SemaphoreSlim iconRequests = new(3);
static readonly Dictionary<string, string> cloudIcons = new(StringComparer.OrdinalIgnoreCase);
public static int Revision { get; private set; }
public static bool IsCloud(string reference) => reference?.StartsWith("cloud:", StringComparison.OrdinalIgnoreCase) == true;
public static bool ValidReference(string reference)
{
if (string.IsNullOrWhiteSpace(reference) || reference.Length > 256 || reference.Contains("..")) return false;
if (!IsCloud(reference)) return reference.EndsWith(".clothing", StringComparison.OrdinalIgnoreCase);
var ident = reference[6..];
return ident.Split('.').Length == 2 && ident.Split('.').All(x => x.Length > 0) && ident.All(c => char.IsAsciiLetterOrDigit(c) || c is '.' or '_' or '-');
}
public static Clothing Get(string reference) => reference is null ? null : loaded.TryGetValue(reference, out var item) ? item : ResourceLibrary.Get<Clothing>(reference);
public static string Reference(Clothing item)
{
var cloud = loaded.FirstOrDefault(x => IsCloud(x.Key) && x.Value == item).Key;
if (cloud is not null) return cloud;
var definition = item.SteamItemDefinitionId is { } id ? Sandbox.Services.Inventory.FindDefinition(id) : null;
return !string.IsNullOrEmpty(definition?.PackageIdent) ? "cloud:" + definition.PackageIdent : item.ResourcePath;
}
public static Task<Clothing> Load(string reference)
{
if (!ValidReference(reference)) return Task.FromException<Clothing>(new InvalidOperationException("Use a .clothing asset path or cloud:creator.item reference."));
if (Get(reference) is { } existing)
{
if (!SupportsHuman(existing)) return Task.FromException<Clothing>(new InvalidOperationException("This item does not have a human model."));
return Finish(reference, existing);
}
if (pending.TryGetValue(reference, out var task) && !task.IsFaulted && !task.IsCanceled) return task;
return pending[reference] = LoadMissing(reference);
}
static async Task<Clothing> LoadMissing(string reference)
{
if (!ValidReference(reference)) throw new InvalidOperationException("Invalid clothing reference.");
var item = IsCloud(reference) ? await Cloud.Load<Clothing>(reference[6..]) : await ResourceLibrary.LoadAsync<Clothing>(reference);
if (item is null || item.IsError) throw new InvalidOperationException("Clothing unavailable: " + reference);
if (!SupportsHuman(item))
throw new InvalidOperationException("This item does not have a human model.");
loaded[reference] = item;
Revision++;
return await Finish(reference, item);
}
static async Task<Clothing> Finish(string reference, Clothing item)
{
if (IsCloud(reference) && !LocalIconExists(item) && !cloudIcons.ContainsKey(reference))
{
var definition = item.SteamItemDefinitionId is { } id ? Sandbox.Services.Inventory.FindDefinition(id) : null;
var icon = definition?.IconUrl;
try
{
if (string.IsNullOrEmpty(icon)) icon = (await Package.FetchAsync(reference[6..], partial: true))?.ThumbWide;
}
catch { /* A preview failure must not prevent wearing a downloaded item. */ }
cloudIcons[reference] = icon ?? "";
cloudIcons[item.ResourcePath] = icon ?? "";
Revision++;
}
return item;
}
static bool LocalIconExists(Clothing item) => !string.IsNullOrEmpty(item.Icon.Path) &&
(item.Icon.Path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) || FileSystem.Mounted.FileExists(item.Icon.Path));
public static string Icon(string reference)
{
var item = Get(reference);
if (item is not null && LocalIconExists(item)) return item.Icon.Path;
if (reference is not null && cloudIcons.TryGetValue(reference, out var icon)) return icon;
if (item is null) return "";
if (cloudIcons.TryGetValue(item.ResourcePath, out icon)) return icon;
if (!iconLoads.TryGetValue(item.ResourcePath, out var request) || request.IsCompleted)
iconLoads[item.ResourcePath] = RecoverIcon(item);
return "";
}
static async Task RecoverIcon(Clothing item)
{
await iconRequests.WaitAsync();
try
{
var reference = Reference(item);
if (IsCloud(reference))
{
await Finish(reference, item);
return;
}
// Previously mounted clothing can appear as a local path without a session
// alias. Match the package's primary asset, never just its display name.
var definition = Sandbox.Services.Inventory.Definitions.FirstOrDefault(d => string.Equals(d.Asset, item.ResourcePath, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(definition?.PackageIdent))
{
RegisterPreview(item, definition.PackageIdent, definition.IconUrl);
return;
}
var words = string.Join(" ", (item.Title ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(x => !x.Contains(':')));
if (words.Length == 0) return;
var found = await Package.FindAsync(words + " type:clothing", 16);
foreach (var candidate in found.Packages.OrderByDescending(p => string.Equals(p.Title, item.Title, StringComparison.OrdinalIgnoreCase)))
{
var package = await Package.FetchAsync(candidate.FullIdent, partial: false);
if (!string.Equals(package?.PrimaryAsset?.TrimStart('/'), item.ResourcePath.TrimStart('/'), StringComparison.OrdinalIgnoreCase)) continue;
RegisterPreview(item, package.FullIdent, package.ThumbWide);
return;
}
}
catch { /* Keep the picker usable when hosted previews are unavailable. */ }
finally
{
cloudIcons.TryAdd(item.ResourcePath, "");
Revision++;
iconRequests.Release();
}
}
static void RegisterPreview(Clothing item, string ident, string icon)
{
loaded["cloud:" + ident] = item;
cloudIcons[item.ResourcePath] = icon ?? "";
cloudIcons["cloud:" + ident] = icon ?? "";
}
public static bool IsHair(Clothing c) => c.Category.ToString().StartsWith("Hair");
public static bool IsFacialHair(Clothing c) => c.Category.ToString().StartsWith("FacialHair");
public static bool IsBaseSkin(Clothing c) => c.HasHumanSkin && string.IsNullOrEmpty(c.HumanAltModel) && string.IsNullOrEmpty(c.HumanAltFemaleModel);
static bool SupportsHuman(Clothing c) => c.HasHumanSkin || !string.IsNullOrEmpty(c.HumanAltModel) || !string.IsNullOrEmpty(c.HumanAltFemaleModel);
public static WardrobeItem Describe(string reference, Clothing item)
{
var category = Category(item.Category.ToString());
return new(item.Title, category, category, reference, item.SlotsUnder.ToString(), item.SlotsOver.ToString(), item.HideBody.ToString(), item.Category.ToString());
}
static string Category(string c) => c switch
{
"Tops" or "TShirt" or "Sweatshirt" or "Hoodie" or "Shirt" or "Knitwear" => "Tops",
"Bottoms" or "Shorts" or "Trousers" or "Jeans" or "Skirt" or "Underpants" => "Bottoms",
"Footwear" or "Heels" or "Sandals" or "Shoes" or "Trainers" or "Boots" or "Slippers" or "Socks" => "Shoes",
"Vest" or "Jacket" or "Cardigan" or "Coat" or "Gilet" => "Outerwear",
"Fullbody" or "Dress" or "Suit" or "Costume" or "Uniform" => "Outfits",
"Gloves" => "Gloves",
_ when c.StartsWith("Glasses") || c == "Eyewear" => "Glasses",
_ when c.StartsWith("Hat") || c == "Headwear" => "Hats",
_ when c.StartsWith("FacialHair") => "Facial hair",
_ when c.StartsWith("Hair") => "Hair",
_ => "Accessories"
};
/// <summary>Project clothing is discovered automatically; cloud choices are kept for the session.</summary>
public static IEnumerable<WardrobeItem> Available => WardrobeCatalog.Items
.Concat(loaded.Where(x => !IsBaseSkin(x.Value)).Select(x => Describe(x.Key, x.Value)))
.Concat(ResourceLibrary.GetAll<Clothing>().Where(c => !IsBaseSkin(c) && (!string.IsNullOrEmpty(c.HumanAltModel) || !string.IsNullOrEmpty(c.HumanAltFemaleModel)))
.Where(c => !loaded.Values.Contains(c)).Select(c => Describe(Reference(c), c)))
.DistinctBy(x => x.Path);
}
Game
library
namespace CharacterEditor;
// Generated by the companion authoring tools/face-transfer/calibrate_photo_fit.py; approximate rest-space anchors.
public static class PhotoFitAnchors
{
public static readonly string[] Keys={"faceHeight","jawWidth","eyeSeparation","leftEyeWidth","rightEyeWidth","noseWidth","noseLength","mouthWidth"};
public static readonly float[][] Ratios={
new float[]{1.44567f,0.8893f,0.18852f,0.27444f,0.25628f,0.26924f,0.26822f,0.50607f},
new float[]{1.39547f,0.87962f,0.16641f,0.28168f,0.26739f,0.22625f,0.27752f,0.47095f},
new float[]{1.43572f,0.89854f,0.17047f,0.25843f,0.25695f,0.28462f,0.27722f,0.45054f},
new float[]{1.4587f,0.8663f,0.16107f,0.30609f,0.27849f,0.22656f,0.30262f,0.43632f},
new float[]{1.44561f,0.89132f,0.16515f,0.25603f,0.25612f,0.19936f,0.30243f,0.43829f},
new float[]{1.41067f,0.89818f,0.17149f,0.26097f,0.25952f,0.26348f,0.25932f,0.4242f},
new float[]{1.41915f,0.89606f,0.16683f,0.25769f,0.25657f,0.22921f,0.28683f,0.42675f}
};
}
Game
library
using System;
namespace CharacterEditor;
/// <summary>Per-renderer complexion and cosmetics retain native shading; optional iris color applies separately.</summary>
public sealed class SkinColorPresentation
{
readonly Dictionary<int, Material> copies = new();
SkinnedModelRenderer target;
Model model;
string group;
Color? applied;
bool normalized;
string makeupSignature;
Texture appliedNeck;
readonly Dictionary<int, HumanMakeupPresentation> makeupCopies = new();
// These native materials contain a darker photographed base than the other curated faces.
// Correct their base when a palette/custom color is chosen, leaving Native/legacy looks exact.
static readonly HashSet<string> DarkBases = new(StringComparer.OrdinalIgnoreCase)
{
"models/citizen_human/bodies/male/body_mode_dark.vmat",
"models/citizen_human/bodies/female/female_body_mode_dark.vmat",
"models/citizen_human/heads/citizen_human_malehead01/citizen_human_malehead01.vmat",
"models/citizen_human/heads/citizen_human_malehead04/citizen_human_malehead04.vmat",
"models/citizen_human/heads/citizen_human_femalehead01/citizen_human_femalehead01.vmat"
};
public static Color MaterialTint(Material original, Color color, bool normalize) => original.GetColor("g_vColorTint") * color * (normalize && DarkBases.Contains(original.Name) ? new Color(1.7f, 1.91f, 2.155f) : Color.White);
readonly EyeColorPresentation eyes = new();
public void Apply(SkinnedModelRenderer body, Color color, bool normalize = false, CharacterMakeupData makeup = null, string skinPath = "", CharacterAppearanceData appearance = null)
{
ApplySkin(body, color, normalize, makeup, skinPath, false, appearance?.BaseShape is not null, appearance?.BaseShape is not null);
eyes.Apply(body, appearance);
}
public void ApplyEmbeddedSkin(SkinnedModelRenderer body, Color color, bool normalize = false) => ApplySkin(body, color, normalize, null, "", true);
public void ApplyEmbeddedSkin(SkinnedModelRenderer body, CharacterAppearanceData appearance) => ApplySkin(body, appearance.SkinColor, appearance.NormalizeSkinTone, null, appearance.SkinPath, true, true, appearance.BaseShape is not null);
void ApplySkin(SkinnedModelRenderer body, Color color, bool normalize, CharacterMakeupData makeup, string skinPath, bool preserveNonSkin = false, bool unified = false, bool unifiedBody = false)
{
if (!body.IsValid() || body.Model is null)
return;
if (target != body || model != body.Model || group != body.MaterialGroup)
{
target = body;
model = body.Model;
group = body.MaterialGroup;
copies.Clear();
applied = null;
foreach (var item in makeupCopies.Values)
item.Dispose();
makeupCopies.Clear();
makeupSignature = null;
Clear(body, preserveNonSkin);
}
var cosmetics = "matched-neck-albedo" + makeup?.PresentationKey() + skinPath + unified + unifiedBody + (unifiedBody ? UnifiedHuman.BodyToneTint(skinPath) : Color.White).ToString();
var neck = unifiedBody ? UnifiedNeckAssets.ColorTexture(skinPath) : null;
if (applied == color && normalized == normalize && makeupSignature == cosmetics && appliedNeck == neck)
{
RefreshMakeup();
return;
}
appliedNeck = neck;
makeupSignature = cosmetics;
if (makeup?.Active != true)
{
foreach (var item in makeupCopies.Values)
item.Dispose();
makeupCopies.Clear();
}
if (color == Color.White && !normalize && makeup?.Active != true && !unified)
{
Clear(body, preserveNonSkin);
applied = color;
normalized = false;
return;
}
// MaterialAccessor uses original model slots, including alternatives. GetMaterials(group)
// returns a compact group list and cannot be used as renderer override indices.
for (var i = 0; i < body.Materials.Count; i++)
{
var slot = body.Materials.GetOriginal(i);
var original = unified ? UnifiedHuman.SkinMaterial(slot, skinPath, unifiedBody) : HumanMarkingModels.NativeMaterial(slot);
var armSide = HumanMarkingModels.ArmSide(slot);
if (original?.ShaderName != "shaders/skin.shader" || original.Name.EndsWith("/mouth.vmat", StringComparison.OrdinalIgnoreCase))
continue;
if (!copies.TryGetValue(i, out var copy) || unified)
copies[i] = copy = original.CreateCopy($"fate-skin-{body.Id}-{i}.vmat");
var tint = MaterialTint(original, color, normalize) * (unifiedBody ? UnifiedHuman.BodyToneTint(original, skinPath) : Color.White);
var selectedHead = IsSelectedHead(original, skinPath);
var sourceColor = unifiedBody && selectedHead ? UnifiedNeckAssets.ColorTexture(skinPath) : original.GetTexture("g_tColor");
var bodyInk = original.Name.Contains("/bodies/", StringComparison.OrdinalIgnoreCase) && makeup?.Markings.Any(x => x.Style != "none" && x.Ink.Amount > 0 && MarkingCatalog.OnMaterial(x, armSide)) == true;
if (makeup?.Active == true && (selectedHead || bodyInk))
{
if (!makeupCopies.TryGetValue(i, out var presentation))
makeupCopies[i] = presentation = new();
// Keep complexion correction in the native skin shader on every surface.
// Baking it into an 8-bit makeup texture changes color-space behavior and
// clips bright corrections, creating a visible head/body seam.
copy.Set("g_tColor", presentation.Apply(original, makeup, skinPath, selectedHead, armSide, sourceColor));
copy.Set("g_vColorTint", tint);
}
else
{
copy.Set("g_tColor", sourceColor);
copy.Set("g_vColorTint", tint);
if (makeupCopies.Remove(i, out var unused))
unused.Dispose();
}
body.Materials.SetOverride(i, copy);
}
applied = color;
normalized = normalize;
}
public static bool IsSelectedHead(Material original, string skinPath)
{
var id = System.IO.Path.GetFileNameWithoutExtension(skinPath);
var head = id switch
{
"human01" => "malehead01",
"human02" => "femalehead01",
"human03" => "malehead02",
"human04" => "femalehead02",
"human05" => "malehead03",
"human06" => "malehead04",
"human07" => "malehead05",
_ => ""
};
return head != "" && original.Name.EndsWith($"citizen_human_{head}.vmat", StringComparison.OrdinalIgnoreCase);
}
public void Dispose()
{
eyes.Dispose();
foreach (var item in makeupCopies.Values)
item.Dispose();
makeupCopies.Clear();
}
void RefreshMakeup()
{
foreach (var pair in makeupCopies)
{
var texture = pair.Value.Refresh();
if (copies.TryGetValue(pair.Key, out var copy) && copy.GetTexture("g_tColor") != texture)
copy.Set("g_tColor", texture);
}
}
public void Refresh()
{
eyes.Refresh();
RefreshMakeup();
}
public void Invalidate()
{
applied = null;
}
static void Clear(SkinnedModelRenderer body, bool preserveNonSkin = false)
{
for (var i = 0; i < body.Materials.Count; i++)
{
var slot = body.Materials.GetOriginal(i);
if (preserveNonSkin && (HumanMarkingModels.NativeMaterial(slot)?.ShaderName != "shaders/skin.shader" || HumanMarkingModels.NativeMaterial(slot).Name.EndsWith("/mouth.vmat", StringComparison.OrdinalIgnoreCase)))
continue;
if (HumanMarkingModels.ArmSide(slot) != 0)
body.Materials.SetOverride(i, HumanMarkingModels.NativeMaterial(slot));
else if (body.Materials.HasOverride(i))
body.Materials.SetOverride(i, null);
}
}
}
Game
library
@using System
@using System.Text.Json
@using Sandbox
@using Sandbox.UI
@namespace CharacterEditor
@inherits PanelComponent
<root>
@if(Session is not null && Session.View!="hidden")
{
@if(Session.View=="editor")
{
<CharacterCreatorPanel [email protected] @ref=Creator Saved=@Save Closed=@RequestClose SaveLabel=@(Session.IsNew?"Save character":"Save changes") />
@if(!Session.IsNew){<button class="savecopy" onclick=@SaveCopy>Save as a copy</button>}
@if(ConfirmClose){<div class="shade"><div class="dialog"><h2>Discard your edits?</h2><p>Your saved character will stay as it was.</p><div class="actions"><button onclick=@(()=>ConfirmClose=false)>Keep editing</button><button class="primary" onclick=@Discard>Discard edits</button></div></div></div>}
}
else if(Session.View=="plaza")
{
<div class="plazahead"><div><div class="eyebrow">CHARACTER EDITOR</div><h1>The gathering place</h1><p>Your characters, a little more alive.</p></div><button onclick=@(()=>Session.View="library")>Back to collection</button></div>
<div class="residents">
<div class="eyebrow">IN THE PLAZA · @Session.Plaza?.Residents.Count</div>
@foreach(var resident in Session.Plaza?.Residents??new List<CharacterAvatar>())
{<div class="resident"><button onclick=@(()=>Session.Plaza.Focus(resident.Preset.Id))>@resident.GameObject.Name</button><button onclick=@(()=>Session.Edit(resident.Preset,resident))>Edit</button></div>}
@if(Session.Library.Items.Count==0){<p>Save your first character to bring this place to life.</p><button class="primary" onclick=@(()=>Session.Edit())>Create a character</button>}
@if(Session.Plaza?.PageCount>1){<div class="actions"><button onclick=@(()=>Session.Plaza.ChangePage(-1))>Previous</button><span>@(Session.Plaza.Page+1) / @Session.Plaza.PageCount</span><button onclick=@(()=>Session.Plaza.ChangePage(1))>Next</button></div>}
</div>
<div class="plazatools"><button onclick=@(()=>Session.Plaza.Yaw-=25)>Turn left</button><button onclick=@(()=>Session.Plaza.Yaw+=25)>Turn right</button><button onclick=@(()=>Session.Plaza.Distance-=80)>Closer</button><button onclick=@(()=>Session.Plaza.Distance+=80)>Further</button><button onclick=@(()=>Session.Plaza.Overview())>Overview</button><button onclick=@(()=>Session.Plaza.Paused=!Session.Plaza.Paused)>@(Session.Plaza?.Paused==true?"Let them wander":"Pause wandering")</button></div>
}
else
{
<div class="collection">
<div class="topbar"><div class="brand"><span class="brandmark">C</span><span>CHARACTER EDITOR</span></div><div class="actions"><button onclick=@(()=>ImportOpen=true)>Import preset</button><button class="primary" onclick=@(()=>Session.Edit())>+ Create character</button></div></div>
<div class="intro"><div><div class="eyebrow">A PLACE FOR EVERY VERSION OF YOU</div><h1>Meet your characters.</h1><p>Shape a face. Find a personality. Build a little world of your own.</p></div>@if(Session.Plaza.IsValid()){<button class="plazalink" [email protected]><span>Visit the plaza</span><span class="arrow">↗</span></button>}</div>
<div class="librarybar"><div class="actions"><button class=@(!Favorites?"chosen":"") onclick=@(()=>Favorites=false)>All characters · @Session.Library.Items.Count</button><button class=@(Favorites?"chosen":"") onclick=@(()=>Favorites=true)>Favorites</button></div><TextEntry class="search" Value=@Search OnTextEdited=@((string value)=>{Search=value;}) Placeholder="Find a character" /></div>
<div class="cards">
@foreach(var preset in Visible)
{
<div class="card"><button class="portrait" onclick=@(()=>Session.Edit(preset))><CharacterCreatorPreview [email protected] Portrait=@true /></button><div class="cardtitle"><h2>@preset.Name</h2><button class=@($"favorite {(preset.Favorite?"chosen":"")}") onclick=@(()=>Session.Favorite(preset))>@(preset.Favorite?"★":"☆")</button></div><div class="cardactions"><button onclick=@(()=>Session.Edit(preset))>Edit</button><button onclick=@(()=>Export(preset))>Export</button><button class="delete" onclick=@(()=>PendingDelete=preset)>Delete</button></div></div>
}
@if(!Visible.Any())
{<div class="empty"><div class="emptyicon">✦</div><h2>@(Session.Library.Items.Count==0?"Your cast starts here.":"No characters found.")</h2><p>@(Session.Library.Items.Count==0?"Start with a familiar face, or make someone entirely new. Every saved character gets a place in your plaza.":"Try another search or switch back to all characters.")</p><button class="primary" onclick=@(()=>Session.Edit())>Create your first look</button></div>}
</div>
<div class="bottomline"><span>7 blendable faces · Body shaping · Your own wardrobe</span><span>Made to mix, match & make your own</span></div>
</div>
}
@if(!string.IsNullOrWhiteSpace(Session.Message)){<div class="toast" onclick=@(()=>Session.Message=null)>@Session.Message</div>}
@if(Session.Library.Warnings.Count>0 && !DismissWarnings){<div class="warning" onclick=@(()=>DismissWarnings=true)>@string.Join(" · ",Session.Library.Warnings) · Click to dismiss</div>}
@if(PendingDelete is not null){<div class="shade"><div class="dialog"><h2>Delete @PendingDelete.Name?</h2><p>This removes the saved character and its backup from your collection.</p><div class="actions"><button onclick=@(()=>PendingDelete=null)>Keep character</button><button class="primary" onclick=@Delete>Delete character</button></div></div></div>}
@if(ImportOpen || ExportJson is not null)
{<div class="shade"><div class="dialog transfer"><div class="eyebrow">PORTABLE PRESETS</div><h2>@(ImportOpen?"Bring a character along.":"Take this character with you.")</h2><p>@(ImportOpen?"Paste a Character Editor preset or raw Fate Pending appearance JSON. Import always creates a new copy.":"Copy this preset into another project using Character Kit. A copy is also saved in character_editor/exports in this game's data folder.")</p><TextEntry class="json" Multiline=@true Value=@(ImportOpen?ImportJson:ExportJson) OnTextEdited=@((string value)=>{ImportJson=value;}) /><div class="actions"><button onclick=@CloseTransfer>Close</button>@if(ImportOpen){<button class="primary" onclick=@Import>Import character</button>}else{<button class="primary" onclick=@(()=>Clipboard.SetText(ExportJson))>Copy preset</button>}</div></div></div>}
@if(Session.EnableEscapeMenu && !Session.Embedded){<StudioEscapeMenu @ref=EscapeMenu MainMenu=@ReturnToMainMenu OpenLab=@OpenFaceLab EnableExperiments=@(Session.EnableFaceLab && Session.View!="editor") HasUnsavedEdits=@HasUnsavedEdits />}
}
</root>
@code {
public StudioSession Session => Components.Get<StudioSession>();
public CharacterCreatorPanel Creator { get; set; }
public StudioEscapeMenu EscapeMenu { get; set; }
string Search = "", ImportJson = "", ExportJson;
bool Favorites, ImportOpen, ConfirmClose, DismissWarnings;
CharacterPreset PendingDelete;
IEnumerable<CharacterPreset> Visible => Session.Library.Items.Where(x => (!Favorites || x.Favorite) && x.Name.Contains(Search ?? "", StringComparison.OrdinalIgnoreCase)).OrderByDescending(x => x.Favorite).ThenBy(x => x.Name);
protected override void OnStart()
{
Session?.EnsureLoaded();
}
protected override void OnUpdate()
{
base.OnUpdate();
if (Session?.EnableEscapeMenu == true && !Session.Embedded && Session.View != "hidden" && EscapeMenu is not null && Input.EscapePressed)
{
Input.EscapePressed = false;
EscapeMenu.Toggle();
}
}
bool HasUnsavedEdits() => Session.View == "editor" && Creator is not null && Session.Draft is not null && (Creator.DraftCharacterName != Session.Draft.Name || JsonSerializer.Serialize(Creator.DraftAppearance) != JsonSerializer.Serialize(Session.Draft.Appearance));
void ReturnToMainMenu() => StudioSession.OpenScene("scenes/main_menu.scene");
void OpenFaceLab() => StudioSession.OpenScene("scenes/face_lab.scene");
string Save(string name, CharacterAppearanceData data) => Session.Save(name, data);
void SaveCopy()
{
if (Creator?.AppearanceReady == true)
{
var error = Session.Save(Creator.DraftCharacterName, Creator.DraftAppearance, true);
if (error is not null)
Session.Message = error;
}
}
void RequestClose()
{
if (HasUnsavedEdits())
ConfirmClose = true;
else
Discard();
}
void Discard()
{
ConfirmClose = false;
Session.CloseEditor();
Creator = null;
}
void Delete()
{
Session.Delete(PendingDelete);
PendingDelete = null;
}
void Import()
{
Session.Import(ImportJson);
if (Session.Message.StartsWith("Imported "))
CloseTransfer();
}
void Export(CharacterPreset preset)
{
try
{
ExportJson = preset.ToJson();
FileSystem.Data.CreateDirectory("character_editor/exports");
FileSystem.Data.WriteAllText($"character_editor/exports/{preset.Id}.json", ExportJson);
}
catch (Exception e)
{
Session.Message = e.Message;
}
}
void CloseTransfer()
{
ImportOpen = false;
ExportJson = null;
ImportJson = "";
}
protected override int BuildHash() => HashCode.Combine(Session?.View, Session?.Revision, Session?.Library.Revision, Search, Favorites, HashCode.Combine(ImportOpen, ExportJson, PendingDelete, ConfirmClose, DismissWarnings, Session?.Message), Session?.Plaza?.Paused, HashCode.Combine(Session?.Plaza?.Page, Session?.Plaza?.Residents.Count, Session?.EnableEscapeMenu, Session?.EnableFaceLab, Session?.Embedded));
}
Game
library
StudioPanel {
position: absolute; width: 100%; height: 100%; font-family: Inter; color: #f0eee6; pointer-events: none;
button { pointer-events: all; cursor: pointer; padding: 11px 17px; border-radius: 6px; background-color: #ffffff0a; border: 1px solid #ffffff20; color: #e9ebe6; font-size: 14px; flex-shrink: 0; align-items: center; justify-content: center; gap: 10px; }
button:hover { background-color: #ffffff20; border-color: #c5e4a7; }
button.primary { background-color: #d6ebaf; border-color: #d6ebaf; color: #263425; font-weight: 600; }
button.primary:hover { background-color: #eaf9cc; }
button.chosen { color: #d6ebaf; border-color: #d6ebaf88; }
h1, h2, p { margin: 0; flex-shrink: 0; }
h1 { font-size: 48px; font-weight: 500; letter-spacing: -1.5px; }
h2 { font-size: 22px; font-weight: 500; }
p { font-size: 15px; line-height: 25px; color: #aeb8ab; white-space: normal; }
.eyebrow { font-size: 11px; letter-spacing: 2px; color: #c6d9b2; margin-bottom: 13px; }
.collection { position: absolute; width: 100%; height: 100%; background-color: #17221ff9; padding: 30px 54px 20px; flex-direction: column; pointer-events: all; }
.topbar { justify-content: space-between; align-items: center; flex-shrink: 0; }
.brand { gap: 13px; align-items: center; letter-spacing: 2px; font-size: 12px; font-weight: 600; }
.brandmark { width: 37px; height: 37px; border-radius: 50%; border: 2px solid #d6ebaf; color: #d6ebaf; font-size: 25px; align-items: center; justify-content: center; }
.actions { gap: 10px; align-items: center; flex-shrink: 0; }
.intro { margin: 42px 0 30px; justify-content: space-between; align-items: center; flex-shrink: 0; }
.intro > div { flex-direction: column; gap: 9px; }
.intro .eyebrow { margin-bottom: 0; }
.plazalink { width: 235px; height: 100px; background-color: #354b3e; justify-content: space-between; padding: 25px; font-size: 19px; border-color: #a5c08466; }
.arrow { font-size: 36px; color: #d6ebaf; }
.librarybar { justify-content: space-between; align-items: center; border-bottom: 1px solid #ffffff20; padding-bottom: 17px; flex-shrink: 0; }
TextEntry.search { width: 240px; padding: 12px 15px; background-color: #0e1916; border: 1px solid #ffffff20; border-radius: 6px; color: #edf2e5; font-size: 14px; }
.cards { flex-grow: 1; min-height: 0; overflow-y: auto; flex-wrap: wrap; align-content: flex-start; gap: 20px; padding: 22px 0; }
.card { width: 220px; flex-shrink: 0; flex-direction: column; background-color: #25312c; border: 1px solid #ffffff18; border-radius: 9px; overflow: hidden; }
button.portrait { width: 100%; height: 190px; padding: 0; border: 0; border-radius: 0; background-color: #435146; position: relative; }
.portrait CharacterCreatorPreview { position: absolute; width: 100%; height: 100%; pointer-events: none; }
.cardtitle { align-items: center; justify-content: space-between; padding: 10px 14px 4px; }
.cardtitle h2 { font-size: 17px; max-width: 153px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
button.favorite { padding: 0; border: 0; background-color: transparent; font-size: 26px; }
.cardactions { gap: 5px; padding: 8px 12px 14px; }
.cardactions button { font-size: 11px; padding: 7px 10px; }
button.delete { color: #d5aaa1; }
.empty { width: 100%; padding: 45px 0; flex-direction: column; align-items: center; gap: 17px; }
.empty p { max-width: 450px; text-align: center; }
.emptyicon { font-size: 54px; color: #d6ebaf; }
.bottomline { justify-content: space-between; color: #809487; font-size: 11px; padding-top: 14px; border-top: 1px solid #ffffff18; flex-shrink: 0; }
.plazahead { position: absolute; left: 38px; right: 38px; top: 30px; align-items: flex-start; justify-content: space-between; text-shadow: 0 2px 4px #000a; }
.plazahead > div { flex-direction: column; gap: 5px; }
.plazahead h1 { font-size: 34px; }
.plazahead button { background-color: #17221fee; }
.plazahead p { color: #edf2e5; }
.residents { position: absolute; left: 30px; top: 190px; width: 245px; max-height: 65%; overflow-y: auto; flex-direction: column; padding: 20px; background-color: #17221fe8; border: 1px solid #ffffff25; border-radius: 8px; pointer-events: all; gap: 8px; }
.resident { gap: 6px; flex-shrink: 0; }
.resident button { font-size: 12px; padding: 8px; }
.resident button:first-child { flex-grow: 1; justify-content: flex-start; }
.plazatools { position: absolute; bottom: 25px; right: 30px; gap: 6px; padding: 10px; background-color: #17221fe8; border-radius: 8px; }
.plazatools button { font-size: 12px; padding: 10px; }
.shade { position: absolute; width: 100%; height: 100%; background-color: #07140eea; align-items: center; justify-content: center; pointer-events: all; z-index: 50; }
.dialog { width: 490px; padding: 35px; background-color: #25352b; border: 1px solid #acc79666; border-radius: 12px; flex-direction: column; gap: 22px; }
.dialog .actions { justify-content: flex-end; }
.transfer { width: 700px; }
TextEntry.json { height: 260px; padding: 15px; background-color: #121f18; color: #d6ebaf; font-size: 12px; overflow-y: auto; white-space: pre-wrap; }
.toast { position: absolute; bottom: 65px; left: 35%; max-width: 60%; padding: 14px 22px; background-color: #d6ebaf; color: #263425; border-radius: 8px; pointer-events: all; z-index: 100; font-size: 14px; }
.warning { position: absolute; bottom: 110px; left: 30px; max-width: 70%; background-color: #633f24; padding: 15px; pointer-events: all; z-index: 101; }
button.savecopy { position: absolute; right: 250px; bottom: 27px; z-index: 6; background-color: #25352b; }
}
Game
library
using System;
using System.IO;
using System.Text;
namespace CharacterEditor;
/// <summary>Reads the mesh-local bone order in the engine's SaveToVmdl metadata.</summary>
internal static class SerializedMeshBones
{
// The engine's own serializer currently emits KV3 v2 with LZ4. This reader
// consumes that output, never arbitrary on-disk compressed clothing files.
public static string[] Read(byte[] block)
{
using var r = new BinaryReader(new MemoryStream(block));
if (r.ReadUInt32() != 0x4b563302)
throw new InvalidDataException("Unsupported serialized mesh metadata version.");
r.ReadBytes(16);
int compression = r.ReadInt32();
r.ReadInt32();
int bytes = r.ReadInt32(), ints = r.ReadInt32(), doubles = r.ReadInt32();
r.ReadInt32();
r.ReadInt32();
int size = r.ReadInt32(), packed = r.ReadInt32();
r.ReadInt32();
r.ReadInt32();
if (size < 0 || size > 32000000)
throw new InvalidDataException("Invalid mesh metadata size.");
var payload = r.ReadBytes(packed);
var data = compression == 0 ? payload : compression == 1 ? Expand(payload, size) : throw new InvalidDataException("Unsupported mesh metadata compression.");
var p4 = (bytes + 3) & ~3;
var p8 = (p4 + ints * 4 + 7) & ~7;
var stringsAt = p8 + doubles * 8;
int strings = BitConverter.ToInt32(data, p4);
var text = new string[strings];
int cursor = stringsAt;
for (int i = 0; i < strings; i++)
{
var end = Array.IndexOf(data, (byte)0, cursor);
if (end < 0)
throw new InvalidDataException("Missing metadata string terminator.");
text[i] = Encoding.UTF8.GetString(data, cursor, end - cursor);
cursor = end + 1;
}
var reader = new Values(data, text, 0, p4 + 4, p8, cursor);
var root = reader.Value(reader.Type(), 0) as Dictionary<string, object>;
if (root?.TryGetValue("m_skeleton", out var skeleton) == true)
root = skeleton as Dictionary<string, object>;
if (root is null || !root.TryGetValue("m_bones", out var boneValue) || boneValue is not List<object> bones)
throw new InvalidDataException("Serialized garment has no mesh bone table.");
return bones.Cast<Dictionary<string, object>>().Select(b => (string)b["m_boneName"]).ToArray();
}
static byte[] Expand(byte[] input, int length)
{
var output = new byte[length];
int i = 0, o = 0;
int Extended(int n)
{
if (n != 15)
return n;
int b;
do
{
b = input[i++];
n += b;
}
while (b == 255);
return n;
}
while (i < input.Length)
{
int token = input[i++];
int literal = Extended(token >> 4);
if (i + literal > input.Length || o + literal > length)
throw new InvalidDataException("Invalid metadata literal.");
Array.Copy(input, i, output, o, literal);
i += literal;
o += literal;
if (i == input.Length)
break;
int offset = input[i++] | input[i++] << 8;
int match = Extended(token & 15) + 4;
if (offset <= 0 || offset > o || o + match > length)
throw new InvalidDataException("Invalid metadata match.");
for (int j = 0; j < match; j++)
output[o + j] = output[o + j - offset];
o += match;
}
if (o != length)
throw new InvalidDataException("Truncated mesh metadata.");
return output;
}
sealed class Values
{
readonly byte[] data;
readonly string[] strings;
int p1, p4, p8, types;
public Values(byte[] data, string[] strings, int p1, int p4, int p8, int types)
{
this.data = data;
this.strings = strings;
this.p1 = p1;
this.p4 = p4;
this.p8 = p8;
this.types = types;
}
int I()
{
var value = BitConverter.ToInt32(data, p4);
p4 += 4;
return value;
}
long L()
{
var value = BitConverter.ToInt64(data, p8);
p8 += 8;
return value;
}
string S(int i) => i < 0 ? "" : strings[i];
public int Type()
{
int t = data[types++];
if ((t & 128) != 0)
{
t &= 127;
types++;
}
return t;
}
public object Value(int type, int depth)
{
if (depth > 64)
throw new InvalidDataException("Excessive mesh metadata nesting.");
switch (type)
{
case 1:
return null;
case 2:
return data[p1++] != 0;
case 3:
case 4:
return L();
case 5:
var number = BitConverter.Int64BitsToDouble(L());
return number;
case 6:
return S(I());
case 8:
case 10:
case 24:
int count = type == 24 ? data[p1++] : I();
if (count < 0 || count > 2000000)
throw new InvalidDataException("Invalid metadata array.");
int element = type == 8 ? 0 : Type();
var array = new List<object>(count);
for (int i = 0; i < count; i++)
array.Add(Value(element == 0 ? Type() : element, depth + 1));
return array;
case 9:
int members = I();
if (members < 0 || members > 100000)
throw new InvalidDataException("Invalid metadata object.");
var obj = new Dictionary<string, object>();
for (int i = 0; i < members; i++)
{
int t = Type();
string name = S(I());
obj[name] = Value(t, depth + 1);
}
return obj;
case 11:
case 12:
return I();
case 13:
return true;
case 14:
return false;
case 15:
return 0L;
case 16:
return 1L;
case 17:
return 0d;
case 18:
return 1d;
default:
throw new InvalidDataException("Unsupported mesh metadata node " + type);
}
}
}
}
Game
library
using System;
using System.Threading.Tasks;
namespace CharacterEditor;
/// <summary>Drop-in human prefab. The same portable JSON drives editor and world rendering.</summary>
public sealed class CharacterAvatar : Component, Component.ExecuteInEditor
{
[Property, TextArea]
public string PresetJson { get; set; } = "";
[Property]
public bool ShowClothing { get; set; } = true;
[Property]
public bool AllowEditing { get; set; } = true;
[Property]
public bool ShowEditButton { get; set; }
[Property]
public SkinnedModelRenderer BodyTarget { get; set; }
[Property]
public Dresser Wardrobe { get; set; }
public CharacterPreset Preset { get; private set; } = new();
[Property, ReadOnly]
public string Error { get; private set; }
public SkinnedModelRenderer Body => BodyTarget.IsValid() ? BodyTarget : visual?.Body;
CharacterVisual visual;
string received;
GameObject editPrompt;
Task wardrobeLoad;
int wardrobeRevision;
public void Apply(CharacterPreset preset)
{
if (preset.Validate()is string error)
throw new InvalidOperationException(error);
Preset = preset.DeepCopy();
if (BodyTarget.IsValid() && Preset.Appearance.BaseShape is null)
UnifiedHuman.Enable(Preset.Appearance);
PresetJson = Preset.ToJson();
received = PresetJson;
SyncWardrobe();
}
[Button]
public void Randomize()
{
var previous = Preset.Appearance;
var a = AppearanceCatalog.Randomize(blendHeads: true);
a.VoiceId = previous.VoiceId;
if (previous.BaseShape is { } shape)
{
a.BaseShape.ClothingVariant = shape.ClothingVariant;
a.BaseShape.PhotoFaces = shape.PhotoFaces.Select(f => f.DeepCopy()).ToList();
foreach (var face in a.BaseShape.PhotoFaces)
face.Influence = 0;
}
var name = GameObject.Name;
Apply(new CharacterPreset { Name = string.IsNullOrWhiteSpace(name) ? "New character" : name[..Math.Min(name.Length, 40)], Appearance = a });
}
[Button]
public void LoadJson()
{
try
{
Apply(CharacterPreset.FromJson(PresetJson));
Error = null;
}
catch (Exception e)
{
Error = e.Message;
}
}
[Button]
public void RefreshAppearance()
{
visual?.Invalidate();
OnUpdate();
}
[Button]
public void UpdateJson()
{
if (Wardrobe.IsValid())
{
var items = Wardrobe.Clothing.Where(e => e.Clothing is not null).Select(e => e.Clothing).ToArray();
var a = Preset.Appearance;
a.HairPath = items.FirstOrDefault(ClothingResources.IsHair) is { } hair ? ClothingResources.Reference(hair) : "";
a.FacialHairPath = items.FirstOrDefault(ClothingResources.IsFacialHair) is { } facial ? ClothingResources.Reference(facial) : "";
a.ClothingPaths = items.Where(c => !ClothingResources.IsBaseSkin(c) && !ClothingResources.IsHair(c) && !ClothingResources.IsFacialHair(c)).Select(ClothingResources.Reference).ToArray();
}
if (Preset.Validate()is string error)
{
Error = error;
return;
}
PresetJson = Preset.ToJson();
received = PresetJson;
Error = null;
}
void SyncWardrobe()
{
if (!Wardrobe.IsValid())
return;
wardrobeLoad = SyncWardrobeAsync(Preset.Appearance.DeepCopy(), ++wardrobeRevision);
}
async Task SyncWardrobeAsync(CharacterAppearanceData a, int revision)
{
await CharacterVisual.Load(a);
if (!IsValid || revision != wardrobeRevision || !Wardrobe.IsValid())
return;
var container = new ClothingContainer
{
PrefersHuman = true
};
var paths = new[]
{
UnifiedHuman.PresentationSkin(a),
a.HairPath,
a.FacialHairPath
}.Concat(WardrobeCatalog.Paths(a)).Where(p => !string.IsNullOrEmpty(p));
foreach (var path in paths)
{
var item = ClothingResources.Get(path);
if (item is not null)
container.Add(item);
}
Wardrobe.Source = Dresser.ClothingSource.Manual;
Wardrobe.BodyTarget = Body;
Wardrobe.ApplyHeightScale = false;
Wardrobe.Clothing = container.Clothing;
Wardrobe.Apply(container);
}
protected override void OnUpdate()
{
if (received != PresetJson)
{
try
{
bool initial = received is null;
Preset = string.IsNullOrWhiteSpace(PresetJson) ? new CharacterPreset() : CharacterPreset.FromJson(PresetJson);
if (BodyTarget.IsValid() && Preset.Appearance.BaseShape is null)
UnifiedHuman.Enable(Preset.Appearance);
received = PresetJson;
if (!string.IsNullOrWhiteSpace(PresetJson) && (!initial || !Wardrobe.IsValid() || Wardrobe.Clothing.Count == 0))
SyncWardrobe();
Error = null;
}
catch (Exception e)
{
Error = e.Message;
return;
}
}
if (wardrobeLoad?.IsFaulted == true)
{
Error = wardrobeLoad.Exception.GetBaseException().Message;
return;
}
if (wardrobeLoad?.IsCompleted == false)
return;
visual ??= new CharacterVisual(GameObject, BodyTarget, Wardrobe);
visual.Update(Preset.Appearance, ShowClothing);
Error = visual.Error;
if (!Scene.IsEditor && ShowEditButton && AllowEditing && !editPrompt.IsValid())
{
editPrompt = new GameObject
{
Parent = GameObject,
Name = "Edit character button",
NetworkMode = NetworkMode.Never
};
editPrompt.Components.Create<ScreenPanel>();
editPrompt.Components.Create<AvatarEditButton>().Avatar = this;
}
if (editPrompt.IsValid())
editPrompt.Enabled = !Scene.IsEditor && ShowEditButton && AllowEditing;
}
public void OpenEditor()
{
if (!AllowEditing)
return;
var session = Scene.GetAllComponents<StudioSession>().FirstOrDefault();
if (!session.IsValid())
{
using var scope = Scene.Push();
var ui = new GameObject
{
Name = "Character editor",
NetworkMode = NetworkMode.Never
};
session = ui.Components.Create<StudioSession>();
session.Embedded = true;
ui.Components.Create<ScreenPanel>();
ui.Components.Create<StudioPanel>();
}
session.Edit(Preset, this);
}
protected override void OnDisabled()
{
visual?.Dispose();
visual = null;
}
protected override void OnDestroy()
{
visual?.Dispose();
}
}
Game
library
@using Sandbox
@using Sandbox.UI
@using System
@using System.Text.Json
@using System.Threading.Tasks
@namespace CharacterEditor
@inherits Panel
<root class="interactive creatoroverlay">
@if(true)
{
<div class="creatorwindow">
<div class="creatorheader"><div><div class="creatorbreadcrumb"><span>@ProductLabel</span><span class="material-icons">chevron_right</span><span>@LocationLabel</span></div><div class="creatortitle">@Title</div></div><button class="creatorbutton quiet cancelcreation" onclick=@Back><span class="material-icons">arrow_back</span>Back</button></div>
<div class="creatorbody">
<div class="creatorcontrols">
<div class="creatortabs">@foreach(var tab in Tabs){<button class=@($"creatorbutton creatortab {(Tab==tab?"chosen":"")}") onclick=@(()=>ChangeTab(tab))>@tab</button>}</div>
<div class=@($"creatoroptions {(Tab=="Voice"?"voiceoptions":"")}")>
<ReferenceFacePanel style=@(Tab=="Photos"?"display: flex;":"display: none;") Appearance=@Appearance Generated=@AddPhotoFace />
@if(Tab=="Presets")
{
<div class="optionheading">Choose a starting look</div><div class="optiondescription">Start with a preset, then make it your own.</div><div class="presetgrid">
@foreach(var preset in AppearanceCatalog.Presets){<button class=@($"creatorbutton presetchoice {(ChosenPreset==preset.Id?"chosen":"")}") onclick=@(()=>ApplyPreset(preset.Id))><div class="presetportrait"><CharacterCreatorPreview [email protected] Portrait=@true /></div><div class="presetlabel">@preset.Label</div></button>}
</div>
}
else if(Tab=="Face & body")
{
@if(Appearance.BaseShape is not null)
{
<div class="optionheading">Blend your base face</div><div class="optiondescription">Overall sets the whole face. In regional tabs, blend that result with individual faces for that area. Refine the result in Shape; choose textures in Skin.</div>
<div class="blendtabs">@foreach(var region in new[]{"overall"}.Concat(FaceRegions.Ids)){<button class=@($"creatorbutton blendtab blend-tab-{region} {(BlendRegion==region?"chosen":"")}") onclick=@(()=>BlendRegion=region)>@FaceRegions.Label(region)</button>}</div>
@if(BlendRegion!="overall")
{
<div class="regionstatus"><span>Mix the live Overall blend with individual faces. Relative weights determine each share.</span><button class="creatorbutton quiet reset-region" onclick=@ResetRegion>Reset region</button></div>
<div class="overallblendrow"><div class="shapelabel"><span>Overall blend</span><span>@($"{MathF.Round(OverallWeight*100)} influence · {MathF.Round(FaceRegions.OverallShare(Appearance.BaseShape,BlendRegion)*100)}% of region")</span></div><SliderControl class="shapeslider region-overall-weight" Value=@OverallWeight OnValueChanged=@SetOverallWeight Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /><div class="optiondescription">Follows changes made in the Overall tab.</div></div>
}
@for(var i=0;i<7;i++)
{
var index=i;
<div class="shaperow"><div class="shapelabel"><span>Face @(index+1)</span><span>@BlendWeightLabel(index)</span></div><SliderControl class=@($"shapeslider head-weight-{index+1}") Value=@HeadWeight(index) OnValueChanged=@((float value)=>SetHeadWeight(index,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
}
@foreach(var face in Appearance.BaseShape.PhotoFaces)
{
<div class="photoblendcard">
<div class="photoblendheader"><span class="photoblendname">@face.Name</span><button class="creatorbutton quiet delete-photo-face" tooltip="Delete this custom face from the character and all regions" onclick=@(()=>FaceRegions.RemovePhoto(Appearance.BaseShape,face))><span class="material-icons">delete_outline</span></button></div>
<div class="shapelabel"><span>Blend influence</span><span>@($"{MathF.Round(PhotoWeight(face)*100)} influence · {MathF.Round(FaceRegions.PhotoEntryShare(Appearance.BaseShape,face,BlendRegion)*100)}% of blend")</span></div>
<SliderControl class="shapeslider photo-face-weight" Value=@PhotoWeight(face) OnValueChanged=@((float value)=>SetPhotoWeight(face,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false />
@if(BlendRegion=="overall")
{
<div class="shapelabel"><span>Photo detail strength</span><span>@MathF.Round(face.Strength*100)%</span></div>
<SliderControl class="shapeslider photo-face-strength" [email protected] OnValueChanged=@((float value)=>{face.Strength=Math.Clamp(value,0,2);}) Min=@(0f) Max=@(2f) Step=@(.01f) ShowTextEntry=@false />
<div class="optiondescription">100% is the fitted shape. Higher values exaggerate its slider changes, within normal Shape limits. This also affects regions using this face.</div>
}
</div>
}
@if(Appearance.BaseShape.TotalInfluence==0){<div class="optiondescription">All influences are zero: using Face 3 as the neutral base.</div>}
<div class="optionheading">Base body</div><div class="shapelabel"><span>@MathF.Round((1-Appearance.BaseShape.MaleBody)*100)% female</span><span>@MathF.Round(Appearance.BaseShape.MaleBody*100)% male</span></div><SliderControl class="shapeslider base-body" [email protected] OnValueChanged=@SetBodyBlend Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false />
@foreach(var variant in new[]{"male","female"})
{
<button class=@($"creatorbutton quiet clothing-prefer-{variant}") role="checkbox" aria-checked=@(Appearance.BaseShape.ClothingVariant==variant?"true":"false") onclick=@(()=>ToggleClothingVariant(variant))><span class="material-icons">@(Appearance.BaseShape.ClothingVariant==variant?"check_box":"check_box_outline_blank")</span>Prefer @variant clothing version</button>
}
<div class="optiondescription">Neither selected: use the body blend. Available versions still fit your body shape.</div>
<div class="optiondescription">Body blend is independent of face and skin. Breast adjustments fade with the female amount; bulge adjustments fade with the male amount.</div>
}
else
{
<div class="optionheading">Native base</div><div class="optiondescription">This saved character uses its original native face and body.</div><button class="creatorbutton" onclick=@EnableBlending>Enable weighted base blending</button>
<div class="bodychoices"><button class="creatorbutton" onclick=@(()=>ChooseBody("male"))>Masculine</button><button class="creatorbutton" onclick=@(()=>ChooseBody("female"))>Feminine</button></div><div class="skingrid">@foreach(var skin in AppearanceCatalog.Skins.Where(x=>x.BodyType==BodyType)){<button class="creatorbutton skinchoice" onclick=@(()=>ChooseSkin(skin))>@FaceLabel(skin)</button>}</div>
}
<div class="optionheading">Eye color</div><div class="optiondescription">Color the iris while keeping the pupil and whites of the eyes.</div>
<div class="colorchoices"><button class=@($"creatorbutton colorchoice eyecolorchoice {(!Appearance.CustomEyeColor?"chosen":"")}") onclick=@(()=>ChooseEyeColor(null))>Native</button>@foreach(var color in EyeColors){<button class=@($"creatorbutton colorchoice eyecolorchoice {(Appearance.CustomEyeColor && Appearance.EyeColor==color.Color?"chosen":"")}") onclick=@(()=>ChooseEyeColor(color.Color))><div class="colorswatch" style=@($"background-color: {ColorCss(color.Color)};")></div><span>@color.Label</span></button>}</div>
<button class="creatorbutton quiet eyecustomtoggle" onclick=@(()=>ChooseEyeColor(Appearance.EyeColor))><span class="material-icons">palette</span>Custom eye color</button>
@if(Appearance.CustomEyeColor){@foreach(var channel in new[]{"Red","Green","Blue"}){<div class="skinchannel"><div class="shapelabel">@channel</div><SliderControl class=@($"shapeslider eye-{channel.ToLowerInvariant()}") Value=@EyeChannel(channel) OnValueChanged=@((float value)=>SetEyeChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}}
}
else if(Tab=="Skin")
{
@if(Appearance.BaseShape is not null){<div class="optionheading">Skin details</div><div class="optiondescription">Choose the native texture set: pigment, pores, freckles and other details. This does not change your face weights or body shape.</div><div class="skingrid">@foreach(var skin in AppearanceCatalog.Skins){<button class=@($"creatorbutton skinchoice {(Appearance.SkinPath==skin.ResourcePath?"chosen":"")}") onclick=@(()=>ChooseSkin(skin))>Skin @skin.Id.Replace("human","")</button>}</div>}
<div class="optionheading">Skin color</div><div class="optiondescription">Choose a complexion independently of face and body. Native restores the selected skin's original color.</div>
<div class="shapecategories"><button class=@($"creatorbutton skinpalette {(SkinPalette=="Human"?"chosen":"")}") onclick=@(()=>SkinPalette="Human")>Human</button><button class=@($"creatorbutton skinpalette {(SkinPalette=="Fantasy"?"chosen":"")}") onclick=@(()=>SkinPalette="Fantasy")>Fantasy & custom</button></div>
<div class="colorchoices">@foreach(var color in SkinPalette=="Human"?AppearanceCatalog.HumanSkinColors:AppearanceCatalog.FantasySkinColors){<button class=@($"creatorbutton colorchoice skincolorchoice skincolor-{color.Id} {(Appearance.SkinColor==color.Color && Appearance.NormalizeSkinTone==color.NormalizeSkinTone?"chosen":"")}") onclick=@(()=>ChooseSkinColor(color.Color,color.NormalizeSkinTone)) [email protected]><div class="colorswatch" style=@($"background-color: {ColorCss(color.Color)};")></div><span>@color.Label</span></button>}</div>
@if(SkinPalette=="Fantasy")
{
<div class="fieldlabel colorlabel">Custom color</div><div class="skincolorpreview"><div class="colorswatch" style=@($"background-color: {ColorCss(Appearance.SkinColor)};")></div><span>@($"RGB {MathF.Round(Appearance.SkinColorR*255)} · {MathF.Round(Appearance.SkinColorG*255)} · {MathF.Round(Appearance.SkinColorB*255)}")</span></div>
@foreach(var channel in new[]{"Red","Green","Blue"}){<div class="skinchannel"><div class="shapelabel">@channel</div><SliderControl class=@($"shapeslider skin-{channel.ToLowerInvariant()}") Value=@SkinChannel(channel) OnValueChanged=@((float value)=>SetSkinChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}
}
<button class="creatorbutton quiet resetshape resetskincolor" onclick=@(()=>ChooseSkinColor(Color.White))><span class="material-icons">restart_alt</span>Reset skin color</button>
}
else if(Tab=="Makeup")
{
<div class="optionheading">Makeup & markings</div><div class="optiondescription">Make it yours. Choose None to leave any makeup layer bare.</div>
<div class="shapecategories">@foreach(var layer in MakeupCatalog.Layers){<button class=@($"creatorbutton makeuplayer {(MakeupLayer==layer?"chosen":"")}") onclick=@(()=>ChooseMakeupLayer(layer))>@(layer=="Tattoo"?"Markings":layer)</button>}</div>
@if(MakeupLayer=="Tattoo")
{
<div class="markinglayers"><div class="markinglist">@for(var i=0;i<Appearance.Makeup.Markings.Count;i++){var index=i;var mark=Appearance.Makeup.Markings[i];<button class=@($"creatorbutton markinglayer {(SelectedMarking==i?"chosen":"")}") onclick=@(()=>SelectMarking(index))>@($"{i+1} · {MarkingCatalog.Style(mark.Style).Label}")</button>}</div><button class="creatorbutton addmarking" onclick=@AddMarking tooltip="Add an independent marking layer"><span class="material-icons">add</span>Add marking</button></div>
@if(CurrentMarking is null)
{<div class="markingempty"><span class="material-icons">brush</span><div class="fieldlabel">None</div><div class="optiondescription">Add a marking to choose its design, location and ink. Combine up to six.</div></div>}
else
{
<div class="markingeditorheading"><button class="creatorbutton quiet inspectmarking" onclick=@InspectMarking tooltip="Focus the preview on this marking"><span class="material-icons">center_focus_strong</span>@MarkingCatalog.Region(CurrentMarking.Region).Label</button><button class="creatorbutton quiet deletemarking" onclick=@DeleteMarking tooltip="Delete this marking layer"><span class="material-icons">delete_outline</span>Delete layer</button></div>
<div class="markingsections">@foreach(var section in new[]{"Design","Placement","Ink"}){<button class=@($"creatorbutton markingsection {(MarkingSection==section?"chosen":"")}") onclick=@(()=>MarkingSection=section)>@section</button>}</div>
@if(MarkingSection=="Design")
{
<div class="markingdesigns">@foreach(var style in MarkingCatalog.Styles){<button class=@($"creatorbutton markingstyle style-{style.Id} {(CurrentMarking.Style==style.Id?"chosen":"")}") onclick=@(()=>ChooseMarkingStyle(style.Id))><span class="material-icons">@MarkingSymbol(style.Id)</span><span>@style.Label</span></button>}</div>
<div class="fieldlabel markinglocationlabel">Location</div><div class="markinglocations">@foreach(var region in MarkingCatalog.Locations(CurrentMarking.Style)){<button class=@($"creatorbutton markinglocation location-{region.Id} {(CurrentMarking.Region==region.Id?"chosen":"")}") onclick=@(()=>ChooseMarkingRegion(region.Id))>@region.Label</button>}</div>
}
else if(MarkingSection=="Placement")
{
<div class="optiondescription">@(MarkingCatalog.Style(CurrentMarking.Style).Band?"This band wraps around the selected region. Slide it along the body and adjust its size and barb spacing.":"Move, turn and resize this marking within the selected region.")</div>
<div class="markingplacement">@foreach(var control in MarkingCatalog.Style(CurrentMarking.Style).Band?new[]{"Vertical","Scale","Spacing"}:new[]{"Horizontal","Vertical","Scale","Rotation"}){<div class="skinchannel"><div class="shapelabel">@(control=="Vertical" && MarkingCatalog.Style(CurrentMarking.Style).Band?"Position along region":control=="Scale"?"Size":control=="Spacing"?"Barb spacing":control)</div><SliderControl class=@($"shapeslider marking-{control.ToLowerInvariant()}") Value=@MarkingValue(control) OnValueChanged=@((float value)=>SetMarkingValue(control,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>}</div><button class="creatorbutton quiet resetmarkingplacement" onclick=@ResetMarkingPlacement><span class="material-icons">restart_alt</span>Reset placement</button>
}
}
}
@if(MakeupLayer!="Tattoo" || CurrentMarking is not null && MarkingSection=="Ink")
{
<div class="colorchoices">
@if(MakeupLayer!="Tattoo"){<button class=@($"creatorbutton colorchoice makeupnone {(!CurrentMakeupActive?"chosen":"")}") onclick=@ChooseNoMakeup><span class="material-icons">block</span><span>None</span></button>}
@foreach(var color in MakeupCatalog.Colors(MakeupLayer)){<button class=@($"creatorbutton colorchoice makeupcolor makeup-{color.Id} {(CurrentMakeupActive && CurrentMakeup.Color==color.Color?"chosen":"")}") onclick=@(()=>ChooseMakeupColor(color.Color)) [email protected]><div class="colorswatch" style=@($"background-color: {ColorCss(color.Color)};")></div><span>@color.Label</span></button>}
</div>
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>@(MakeupLayer=="Eyeliner"?"Upper eyelid strength":"Strength")</span><span class="shapevalue">@(MathF.Round(CurrentMakeup.Amount*100))%</span></div><SliderControl class="shapeslider makeupamount" [email protected] OnValueChanged=@SetMakeupAmount Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
@if(MakeupLayer=="Eyeliner")
{
<div class="optiondescription">Upper and lower liner share one color. Wings follow the upper liner strength; inner-corner liner is optional.</div>
@foreach(var part in new[]{"Lower eyelid","Inner corner"})
{
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>@part strength</span><span class="shapevalue">@(MathF.Round(EyelinerStrength(part)*100))%</span></div><SliderControl class=@($"shapeslider eyeliner-{(part=="Lower eyelid"?"lower":"inner")}") Value=@EyelinerStrength(part) OnValueChanged=@((float value)=>SetEyelinerStrength(part,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
}
<div class="fieldlabel colorlabel">Wing</div>
<div class="choicegrid">@foreach(var wing in new[]{"none","wing"}){<button class=@($"creatorbutton eyelinerwing {((wing=="none")== (Appearance.Makeup.EyelinerWing=="none")?"chosen":"")}") onclick=@(()=>ChooseEyelinerWing(wing))>@(wing=="none"?"No wing":"Wing")</button>}</div>
@if(Appearance.Makeup.EyelinerWing!="none")
{
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>Wing strength</span><span class="shapevalue">@(MathF.Round(Appearance.Makeup.EyelinerWingStrength*100))%</span></div><SliderControl class="shapeslider eyeliner-wing" [email protected] OnValueChanged=@((float value)=>SetEyelinerStrength("Wing",value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>Wing tip horizontal</span><span class="shapevalue">@(MathF.Round(Appearance.Makeup.WingTipHorizontal*100))%</span></div><SliderControl class="shapeslider eyeliner-wing-horizontal" [email protected] OnValueChanged=@((float value)=>SetWingTip(false,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>Wing tip vertical</span><span class="shapevalue">@(Appearance.Makeup.WingTipVertical==0?"Level":$"{MathF.Round(MathF.Abs(Appearance.Makeup.WingTipVertical)*100)}% {(Appearance.Makeup.WingTipVertical>0?"up":"down")}")</span></div><SliderControl class="shapeslider eyeliner-wing-vertical" [email protected] OnValueChanged=@((float value)=>SetWingTip(true,value)) Min=@(-1f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
<div class="skinchannel makeupstrength"><div class="shapelabel"><span>Wing thickness</span><span class="shapevalue">@(MathF.Round(Appearance.Makeup.EyelinerWingThickness*100))%</span></div><SliderControl class="shapeslider eyeliner-wing-thickness" [email protected] OnValueChanged=@SetWingThickness Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>
}
}
<button class="creatorbutton quiet makeupcustomtoggle" onclick=@(()=>CustomMakeup=!CustomMakeup)><span class="material-icons">palette</span>@(CustomMakeup?"Hide custom color":"Custom color")</button>
@if(CustomMakeup){<div class="makeupcustomgrid">@foreach(var channel in new[]{"Red","Green","Blue"}){<div class="skinchannel"><div class="shapelabel">@channel</div><SliderControl class=@($"shapeslider makeup-{channel.ToLowerInvariant()}") Value=@MakeupChannel(channel) OnValueChanged=@((float value)=>SetMakeupChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}</div>}
}
<div class="makeupresets"><button class="creatorbutton quiet clearmakeup" onclick=@ClearMakeup>Clear all makeup & markings</button></div>
}
else if(Tab=="Clothing")
{
<div class="optionheading">Your wardrobe</div><div class="optiondescription">Click an item to wear it, click again to remove it. Outfits replace tops and bottoms; jackets and accessories can be layered.</div>
<button class="creatorbutton quiet browsecloud" onclick=@(()=>CloudWardrobeOpen=!CloudWardrobeOpen)>@(CloudWardrobeOpen?"Close cloud browser":"Browse cloud clothing & purchased cosmetics")</button>
@if(CloudWardrobeOpen){<CloudClothingBrowser Selected=@ChooseCloudClothing Appearance=@Appearance />}
<TextEntry class="wardrobesearch" Value=@WardrobeSearch OnTextEdited=@((string value)=>{WardrobeSearch=value;}) Placeholder="Search clothing" />
<div class="shapecategories">@foreach(var category in WardrobeCatalog.Categories){<button class=@($"creatorbutton {(WardrobeCategory==category?"chosen":"")}") onclick=@(()=>WardrobeCategory=category)>@category</button>}</div>
<div class="fieldlabel">Wearing @WardrobeCatalog.Paths(Appearance).Count() items</div><div class="choicegrid">@foreach(var path in WardrobeCatalog.Paths(Appearance).ToArray()){var worn=ClothingResources.Available.FirstOrDefault(x=>x.Path==path);@if(worn is not null){<button class="creatorbutton chosen" onclick=@(()=>ChooseClothing(worn))>@($"{worn.Name} ×")</button>}}</div>
<div class="fieldlabel">@($"{WardrobeCategory} · {VisibleWardrobe.Count()} choices")</div><div class="choicegrid thumbnailgrid">@foreach(var item in VisibleWardrobe){<button class=@($"creatorbutton wardrobechoice {((WardrobeCatalog.Paths(Appearance).Contains(item.Path)||Appearance.HairPath==item.Path||Appearance.FacialHairPath==item.Path)?"chosen":"")}") onclick=@(()=>ChooseClothing(item))><div class="thumbnailframe">@if(ClothingThumbnails.For(item.Path) is { Length: > 0 } icon){<div class="itemthumbnail" style=@($"background-image: url('{icon}');")></div>}else{<span class="material-icons thumbnailplaceholder">block</span>}</div><span class="itemlabel">@item.Name</span></button>}</div>
@if(!VisibleWardrobe.Any()){<div class="optiondescription">No clothing matches your search in this category.</div>}
<div class="choicegrid"><button class="creatorbutton quiet" onclick=@ClearClothing>Remove clothing</button><button class="creatorbutton quiet" onclick=@ResetClothing>Default outfit</button></div>
}
else if(Tab=="Hair")
{
<div class="optionheading">Hair</div><div class="choicegrid thumbnailgrid">@foreach(var hair in AvailableHair){<button class=@($"creatorbutton hairchoice {(Appearance.HairPath==hair.ResourcePath?"chosen":"")}") onclick=@(()=>ChooseHair(hair))><div class="thumbnailframe">@if(ClothingThumbnails.For(hair.ResourcePath) is { Length: > 0 } icon){<div class="itemthumbnail" style=@($"background-image: url('{icon}');")></div>}else{<span class="material-icons thumbnailplaceholder">block</span>}</div><span class="itemlabel">@hair.Label</span></button>}</div><div class="fieldlabel colorlabel">Hair color</div><div class="colorchoices">@foreach(var color in AppearanceCatalog.Colors){<button class=@($"creatorbutton colorchoice {(SameColor(color.Color)?"chosen":"")}") onclick=@(()=>ChooseColor(color)) [email protected]><div class="colorswatch" style=@($"background-color: {ColorCss(color.Color)};")></div><span>@color.Label</span></button>}</div><div class="fieldlabel colorlabel">Facial hair</div><div class="choicegrid thumbnailgrid">@foreach(var facial in AvailableFacialHair.Where(x=>x.BodyType==""||x.BodyType==BodyType)){<button class=@($"creatorbutton facialchoice {(Appearance.FacialHairPath==facial.ResourcePath?"chosen":"")}") onclick=@(()=>ChooseFacial(facial))><div class="thumbnailframe">@if(ClothingThumbnails.For(facial.ResourcePath) is { Length: > 0 } icon){<div class="itemthumbnail" style=@($"background-image: url('{icon}');")></div>}else{<span class="material-icons thumbnailplaceholder">block</span>}</div><span class="itemlabel">@facial.Label</span></button>}</div>
}
else if(Tab=="Shape")
{
<div class="optionheading">Refine your look</div><div class="optiondescription">Neutral is 50%; ear taper and belly start at 0%.</div>
<div class="shapecategories">@foreach(var category in AppearanceCatalog.Sliders.Select(x=>x.Group).Distinct()){<button class=@($"creatorbutton shapecategory {(ShapeCategory==category?"chosen":"")}") onclick=@(()=>ChangeShapeCategory(category))>@category</button>}</div>
@if(ShapeCategory=="Body" && Appearance.BaseShape is not null)
{
@foreach(var breast in new[]{true,false}){<div class="shaperow"><div class="shapelabel"><span>@(breast?"Breast size":"Bulge size")</span><span>@MathF.Round((breast?1-Appearance.BaseShape.MaleBody:Appearance.BaseShape.MaleBody)*100)% effect</span><button class="creatorbutton quiet" onclick=@(()=>SetBodyFeature(breast,.5f))>Reset</button></div><SliderControl class=@($"shapeslider {(breast?"breast":"bulge")}-size") Value=@(breast?Appearance.BaseShape.BreastSize:Appearance.BaseShape.BulgeSize) OnValueChanged=@((float value)=>SetBodyFeature(breast,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>}
}
<div class="shapegrid">@foreach(var slider in AppearanceCatalog.Sliders.Where(x=>x.Group==ShapeCategory && (Appearance.BaseShape is null || x.Id!="bodyfeature")))
{
<div class=@($"shaperow shape-{slider.Id}")><div class="shapelabel"><span>@slider.LabelFor(Appearance)</span><div class="shapeactions"><span class="shapevalue">@(MathF.Round(slider.Get(Appearance)*100))%</span><button class="creatorbutton quiet resetslider" onclick=@(()=>SetShape(slider,slider.Neutral)) tooltip=@($"Reset {slider.LabelFor(Appearance).ToLowerInvariant()} to {MathF.Round(slider.Neutral*100)}%")><span class="material-icons">restart_alt</span></button></div></div><SliderControl class="shapeslider" [email protected](Appearance) OnValueChanged=@((float value)=>SetShape(slider,value)) [email protected] [email protected] Step=@(0.01f) ShowTextEntry=@false />@if(slider.Id=="columella"){<div class="fitnesshint">Width between the nostrils</div>}@if(slider.Id=="philtrum"){<div class="fitnesshint">Width between nose and upper lip</div>}@if(slider.Id=="eartaper"){<div class="fitnesshint">Normal · Pointed</div>}@if(slider.Id=="fitness"){<div class="fitnesshint">Soft · Neutral · Muscular</div>}@if(slider.Id=="nostrils"){<div class="fitnesshint">Width of the nostril wings</div>}@if(slider.Id=="nostrilfullness"){<div class="fitnesshint">Smaller · Neutral · Fuller wings</div>}@if(slider.Id=="noseheight" || slider.Id=="chinheight"){<div class="fitnesshint">Lower · Neutral · Higher</div>}</div>
}</div>
<button class="creatorbutton quiet resetshape" onclick=@ResetShapes>Reset body & face</button>
}
@if(ExtraTabs.Contains(Tab) && ExtraContent is not null){@ExtraContent(this)}
</div>
<div class="creatorutility"><button class="creatorbutton quiet" onclick=@Randomize><span class="material-icons">shuffle</span>Randomize</button><button class="creatorbutton quiet" onclick=@Reset><span class="material-icons">restart_alt</span>Reset appearance</button></div>
</div>
<div class="creatorpresentation"><div class="previewheading"><div class="previewname">@(string.IsNullOrWhiteSpace(CharacterName)?"Your character":CharacterName.Trim())</div><div class="previewcaption">@(PreviewPan.Length>0.01f || MathF.Abs(PreviewPitch)>0.01f?"Free inspection":Zoom>CharacterCreatorPreview.FaceZoom+0.001f?"Face detail":Portrait?"Face preview":"Character preview")</div></div><CharacterCreatorPreview @ref=Preview class="mainpreview" Hero=@true Appearance=@Appearance Portrait=@Portrait Zoom=@Zoom Yaw=@Yaw Pitch=@PreviewPitch PitchChanged=@SetPitch PanOffset=@PreviewPan PanChanged=@SetPan Interactive=@(!LoadingCatalog) OrbitChanged=@SetYaw ZoomChanged=@SetZoom ShowClothing=@ShowClothing /><div class="previewcontrols"><div class="previewviewrow"><button class="creatorbutton quiet rotateleft" onclick=@(()=>Rotate(-30)) tooltip="Turn left"><span class="material-icons">rotate_left</span></button><button class=@($"creatorbutton bodyview {(Zoom==0?"chosen":"")}") onclick=@(()=>SetView(0))>Full body</button><button class=@($"creatorbutton previewzoom {(MathF.Abs(Zoom-CharacterCreatorPreview.FaceZoom)<0.001f?"chosen":"")}") onclick=@(()=>SetView(CharacterCreatorPreview.FaceZoom))>Face view</button><button class="creatorbutton quiet rotateright" onclick=@(()=>Rotate(30)) tooltip="Turn right"><span class="material-icons">rotate_right</span></button></div><button class="creatorbutton quiet clothingtoggle" onclick=@ToggleClothing><span class="material-icons">@(ShowClothing?"visibility_off":"visibility")</span>@(ShowClothing?"Hide clothing":"Show clothing")</button></div><div class="previewzoomrow"><span>Zoom</span><SliderControl class="customzoom" Value=@Zoom OnValueChanged=@SetZoom Min=@(0f) [email protected] Step=@(0.005f) ShowTextEntry=@false /></div><div class="previewrecenter"><button class="creatorbutton quiet recenterpreview" onclick=@RecenterPreview><span class="material-icons">center_focus_strong</span>Recenter preview</button></div><div class="previewhint">Left drag: turn · Right: tilt · Middle: pan · Scroll: zoom</div></div>
</div>
<div class="creatorfooter"><div class="creatoridentity"><div class="identityfields"><div class="namefield"><div class="fieldlabel">Character name</div><TextEntry class="charactername" Value=@CharacterName OnTextEdited=@SetCharacterName MaxLength=@(40) Placeholder="Your character's name" /></div>@FooterContent</div></div><div class="creatorfeedback">@(Feedback??(LoadingCatalog?"Loading character options…":Preview?.Error)??(""))</div><button class="creatorbutton primary startplaythrough" disabled=@Disabled onclick=@Start><span class="material-icons">play_arrow</span>@(SaveLabel)</button></div>
</div>
}
</root>
@code {
/// <summary>Consumer-specific pages; built-in categories remain owned by Character Kit.</summary>
public IReadOnlyList<string> ExtraTabs { get; set; } = Array.Empty<string>();
public IReadOnlyList<string> HiddenTabs { get; set; } = Array.Empty<string>();
public RenderFragment<CharacterCreatorPanel> ExtraContent { get; set; }
public RenderFragment FooterContent { get; set; }
public Action StopPreview { get; set; }
public bool Disabled { get; set; }
public string ProductLabel { get; set; } = "Character Editor";
public string LocationLabel { get; set; } = "Studio";
public string Title { get; set; } = "Create your character";
public CharacterAppearanceData EditableAppearance => Appearance;
public SkinnedModelRenderer PreviewRenderer => Preview?.Body;
public string SelectedTab => Tab;
public string SaveLabel { get; set; } = "Save character";
public Func<string, CharacterAppearanceData, string> Saved { get; set; }
public Action Closed { get; set; }
public CharacterPreset InitialPreset { get; set; }
CharacterPreset loadedPreset;
public CharacterAppearanceData DraftAppearance => Appearance.DeepCopy();
public string DraftCharacterName => CharacterName;
public void LoadDraft(string name, CharacterAppearanceData data)
{
CharacterName = name;
Appearance = data.DeepCopy();
ChosenPreset = null;
Tab = "Face & body";
}
public bool AppearanceReady => CatalogReady && Preview?.Loading != true && CharacterAppearance.ValidateForApply(Appearance)is null;
IReadOnlyList<string> Tabs => new[]
{
"Presets",
"Skin",
"Face & body",
"Photos",
"Hair",
"Clothing",
"Shape",
"Makeup"
}.Where(x => !HiddenTabs.Contains(x)).Concat(ExtraTabs).Distinct().ToArray();
bool CloudWardrobeOpen;
string WardrobeSearch = "", WardrobeCategory = "Tops";
IEnumerable<WardrobeItem> VisibleWardrobe => ClothingResources.Available.Where(x => x.Category == WardrobeCategory && (string.IsNullOrWhiteSpace(WardrobeSearch) || x.Name.Contains(WardrobeSearch, StringComparison.OrdinalIgnoreCase))).OrderBy(x => x.Name);
void ChooseCloudClothing(WardrobeItem item)
{
WardrobeCategory = item.Category;
WardrobeSearch = "";
ChooseClothing(item);
Appearance.Version = 2;
ChosenPreset = null;
Feedback = null;
StateHasChanged();
}
void ChooseClothing(WardrobeItem item)
{
var clothing = ClothingResources.Get(item.Path);
if (clothing is not null && ClothingResources.IsHair(clothing))
Appearance.HairPath = Appearance.HairPath == item.Path || ClothingResources.Get(Appearance.HairPath) == clothing ? "" : item.Path;
else if (clothing is not null && ClothingResources.IsFacialHair(clothing))
Appearance.FacialHairPath = Appearance.FacialHairPath == item.Path || ClothingResources.Get(Appearance.FacialHairPath) == clothing ? "" : item.Path;
else
WardrobeCatalog.Toggle(Appearance, item);
Appearance.Version = 2;
ShowClothing = true;
ChosenPreset = null;
Feedback = null;
}
void ClearClothing()
{
Appearance.ClothingPaths = Array.Empty<string>();
Appearance.Version = 2;
ShowClothing = true;
ChosenPreset = null;
}
void ResetClothing()
{
Appearance.ClothingPaths = WardrobeCatalog.DefaultPaths.ToArray();
Appearance.Version = 2;
ShowClothing = true;
ChosenPreset = null;
}
CharacterAppearanceData Appearance = AppearanceCatalog.CreateDefault();
string CharacterName = "Rowan", PlaythroughName = "A new adventure", Tab = "Presets", ShapeCategory = "Body", ChosenPreset = "traveller", Feedback;
string BlendRegion = "overall";
bool CatalogReady, LoadingCatalog;
string SkinPalette = "Human";
string MakeupLayer = "Lipstick";
bool CustomMakeup;
int SelectedMarking;
string MarkingSection = "Design";
CharacterMarkingData CurrentMarking => Appearance.Makeup.Markings.Count == 0 ? null : Appearance.Makeup.Markings[Math.Clamp(SelectedMarking, 0, Appearance.Makeup.Markings.Count - 1)];
MakeupLayerData CurrentMakeup => MakeupLayer == "Tattoo" ? CurrentMarking?.Ink ?? Appearance.Makeup.Tattoo : MakeupCatalog.Layer(Appearance.Makeup, MakeupLayer);
void ChooseMakeupLayer(string layer)
{
if (LoadingCatalog)
return;
MakeupLayer = layer;
if (layer == "Tattoo")
Appearance.Makeup.EditMarkings();
}
void SelectMarking(int index)
{
if (LoadingCatalog || index < 0 || index >= Appearance.Makeup.Markings.Count)
return;
SelectedMarking = index;
}
void AddMarking()
{
if (LoadingCatalog || Appearance.Makeup.Markings.Count >= MarkingCatalog.Maximum)
{
Feedback = "You can combine up to six marking layers.";
return;
}
var marking = new CharacterMarkingData();
MarkingCatalog.ResetPosition(marking);
Appearance.Makeup.Markings.Add(marking);
SelectedMarking = Appearance.Makeup.Markings.Count - 1;
MarkingSection = "Design";
ChangedCosmetics();
}
void DeleteMarking()
{
if (LoadingCatalog || CurrentMarking is null)
return;
Appearance.Makeup.Markings.Remove(CurrentMarking);
SelectedMarking = Math.Clamp(SelectedMarking, 0, Math.Max(0, Appearance.Makeup.Markings.Count - 1));
ChangedCosmetics();
}
string MarkingSymbol(string style) => style switch
{
"crescent" => "brightness_2",
"teardrop" => "water_drop",
"star" => "star_outline",
"rune" => "gesture",
"barbed-band" => "link",
_ => "block"
};
void ChooseMarkingStyle(string style)
{
if (LoadingCatalog || CurrentMarking is null)
return;
MarkingCatalog.ChangeStyle(CurrentMarking, style);
ChangedCosmetics();
}
void ChooseMarkingRegion(string region)
{
if (LoadingCatalog || CurrentMarking is null)
return;
MarkingCatalog.ChangeRegion(CurrentMarking, region);
ChangedCosmetics();
}
float MarkingValue(string control) => control switch
{
"Horizontal" => CurrentMarking.Horizontal,
"Vertical" => CurrentMarking.Vertical,
"Scale" => CurrentMarking.Scale,
"Spacing" => CurrentMarking.Spacing,
_ => CurrentMarking.Rotation
};
void SetMarkingValue(string control, float value)
{
if (LoadingCatalog || CurrentMarking is null || !float.IsFinite(value))
return;
value = Math.Clamp(value, 0, 1);
if (MarkingCatalog.Style(CurrentMarking.Style).Band && control is not "Vertical" and not "Scale" and not "Spacing")
return;
if (control == "Horizontal")
CurrentMarking.Horizontal = value;
else if (control == "Vertical")
CurrentMarking.Vertical = value;
else if (control == "Scale")
CurrentMarking.Scale = value;
else if (control == "Spacing")
CurrentMarking.Spacing = value;
else if (control == "Rotation")
CurrentMarking.Rotation = value;
ChangedCosmetics();
}
void ResetMarkingPlacement()
{
if (LoadingCatalog || CurrentMarking is null)
return;
MarkingCatalog.ResetPosition(CurrentMarking);
CurrentMarking.Scale = .5f;
CurrentMarking.Rotation = .5f;
CurrentMarking.Spacing = .5f;
ChangedCosmetics();
}
void InspectMarking()
{
if (LoadingCatalog || CurrentMarking is null)
return;
var region = MarkingCatalog.Region(CurrentMarking.Region);
var center = MarkingCatalog.Focus(CurrentMarking);
var outward = MarkingCatalog.Outward(CurrentMarking);
Yaw = -MathF.Atan2(outward.y, outward.x) * 180 / MathF.PI;
SetZoom(region.Head ? 1.55f : 1.15f);
var rotated = Rotation.FromYaw(Yaw) * center;
var targetHeight = 62 + 1.5f * MathF.Max(0, Zoom - 1);
PreviewPan = new(rotated.x, rotated.y, rotated.z - targetHeight);
PreviewPitch = 0;
}
void ChangedCosmetics()
{
ChosenPreset = null;
Feedback = null;
}
void ChooseNoMakeup()
{
if (LoadingCatalog)
return;
CurrentMakeup.Amount = 0;
if (MakeupLayer == "Eyeliner") MakeupCatalog.Reset(Appearance.Makeup, "Eyeliner");
ChangedCosmetics();
}
void ChooseMakeupColor(Color color)
{
if (LoadingCatalog)
return;
CurrentMakeup.Color = color;
if (!CurrentMakeupActive)
CurrentMakeup.Amount = MakeupLayer == "Eyeliner" ? 1f : .65f;
ChangedCosmetics();
}
void SetMakeupAmount(float value)
{
if (LoadingCatalog || !float.IsFinite(value))
return;
CurrentMakeup.Amount = Math.Clamp(value, 0, 1);
ChangedCosmetics();
}
bool CurrentMakeupActive => MakeupLayer == "Eyeliner" ? Appearance.Makeup.EyelinerActive : CurrentMakeup.Amount > 0;
float EyelinerStrength(string part) => part == "Lower eyelid" ? Appearance.Makeup.EyelinerLowerStrength : part == "Wing" ? Appearance.Makeup.EyelinerWingStrength : Appearance.Makeup.EyelinerInnerStrength;
void SetEyelinerStrength(string part, float value)
{
if (LoadingCatalog || !float.IsFinite(value)) return;
value = Math.Clamp(value, 0, 1);
if (part == "Lower eyelid") Appearance.Makeup.EyelinerLowerStrength = value;
else if (part == "Wing") Appearance.Makeup.EyelinerWingStrength = value;
else Appearance.Makeup.EyelinerInnerStrength = value;
ChangedCosmetics();
}
void PrepareAdjustableWing()
{
var makeup = Appearance.Makeup;
makeup.EyelinerWingHorizontal = makeup.WingTipHorizontal;
makeup.EyelinerWingVertical = makeup.WingTipVertical;
makeup.EyelinerWing = "wing";
}
void SetWingTip(bool vertical, float value)
{
if (LoadingCatalog || !float.IsFinite(value)) return;
PrepareAdjustableWing();
if (vertical) Appearance.Makeup.EyelinerWingVertical = Math.Clamp(value, -1, 1);
else Appearance.Makeup.EyelinerWingHorizontal = Math.Clamp(value, 0, 1);
ChangedCosmetics();
}
void SetWingThickness(float value)
{
if (LoadingCatalog || !float.IsFinite(value)) return;
PrepareAdjustableWing();
Appearance.Makeup.EyelinerWingThickness = Math.Clamp(value, 0, 1);
ChangedCosmetics();
}
void ChooseEyelinerWing(string wing)
{
if (LoadingCatalog) return;
if (wing == "wing") PrepareAdjustableWing();
else Appearance.Makeup.EyelinerWing = "none";
if (wing != "none" && CurrentMakeup.Amount == 0) CurrentMakeup.Amount = 1;
ChangedCosmetics();
}
float MakeupChannel(string channel) => channel == "Red" ? CurrentMakeup.Red : channel == "Green" ? CurrentMakeup.Green : CurrentMakeup.Blue;
void SetMakeupChannel(string channel, float value)
{
if (LoadingCatalog || !float.IsFinite(value))
return;
value = Math.Clamp(value, 0, 1);
if (channel == "Red")
CurrentMakeup.Red = value;
else if (channel == "Green")
CurrentMakeup.Green = value;
else
CurrentMakeup.Blue = value;
ChangedCosmetics();
}
void ClearMakeup()
{
if (LoadingCatalog)
return;
Appearance.Makeup = new();
SelectedMarking = 0;
ChangedCosmetics();
}
bool Portrait = true, ShowClothing = true;
float Yaw, Zoom = CharacterCreatorPreview.FaceZoom;
CharacterCreatorPreview Preview;
Vector3 PreviewPan;
float PreviewPitch;
string BodyType => AppearanceCatalog.IsFemale(Appearance) ? "female" : "male";
IEnumerable<AppearanceOption> AvailableHair => AppearanceCatalog.Hair.Concat(ClothingResources.Available.Where(x=>x.Category=="Hair").Select(x=>new AppearanceOption(x.Path,x.Name,x.Path))).DistinctBy(x=>x.ResourcePath);
IEnumerable<AppearanceOption> AvailableFacialHair => AppearanceCatalog.FacialHair.Concat(ClothingResources.Available.Where(x=>x.Category=="Facial hair").Select(x=>new AppearanceOption(x.Path,x.Name,x.Path))).DistinctBy(x=>x.ResourcePath);
string HairLabel => AppearanceCatalog.Hair.FirstOrDefault(x => x.ResourcePath == Appearance.HairPath)?.Label ?? "Hair";
string FaceLabel(AppearanceOption option) => option.Label.Replace(" · male", "").Replace(" · female", "");
string ColorCss(Color color) => $"rgb({(int)(color.r * 255)},{(int)(color.g * 255)},{(int)(color.b * 255)})";
void SetCharacterName(string value)
{
CharacterName = value;
Feedback = null;
}
void SetPlaythroughName(string value)
{
PlaythroughName = value;
Feedback = null;
}
bool SameColor(Color color) => MathF.Abs(Appearance.HairColorR - color.r) < 0.001f && MathF.Abs(Appearance.HairColorG - color.g) < 0.001f && MathF.Abs(Appearance.HairColorB - color.b) < 0.001f;
void ChangeTab(string tab)
{
if (tab != Tab)
StopVoicePreview();
Tab = tab;
if (tab == "Clothing")
SetZoom(0);
Feedback = null;
}
void StopVoicePreview()
{
StopPreview?.Invoke();
}
void ChangeShapeCategory(string category)
{
ShapeCategory = category;
Feedback = null;
}
void ApplyPreset(string id)
{
if (LoadingCatalog)
return;
var voice = Appearance.VoiceId;
bool unified = Appearance.BaseShape is not null;
Appearance = AppearanceCatalog.ApplyPreset(id);
if (unified)
UnifiedHuman.Enable(Appearance);
Appearance.VoiceId = voice;
ChosenPreset = id;
Feedback = null;
}
void EnableBlending()
{
UnifiedHuman.Enable(Appearance);
ChosenPreset = null;
Feedback = null;
}
void AddPhotoFace(PhotoFace face)
{
if (!face.Valid)
return;
UnifiedHuman.Enable(Appearance);
if (Appearance.BaseShape.PhotoFaces.Count >= 4)
return;
Array.Fill(Appearance.BaseShape.HeadWeights, 0f);
foreach (var old in Appearance.BaseShape.PhotoFaces)
old.Influence = 0;
Appearance.BaseShape.PhotoFaces.Add(face);
ChosenPreset = null;
Tab = "Face & body";
BlendRegion = "overall";
SetView(CharacterCreatorPreview.FaceZoom);
Feedback = "Custom base added. Blend it here or refine it in Shape, then save your character.";
}
FaceRegionBlend Region(bool create = false)
{
if (BlendRegion == "overall")
return null;
if (!Appearance.BaseShape.Regions.TryGetValue(BlendRegion, out var value) && create)
{
value = new()
{
OverallInfluence = .5f
};
Appearance.BaseShape.Regions.Add(BlendRegion, value);
}
return value;
}
float OverallWeight => Region()?.OverallInfluence ?? .5f;
void SetOverallWeight(float value)
{
if (!float.IsFinite(value) || BlendRegion == "overall")
return;
Region(true).OverallInfluence = Math.Clamp(value, 0, 1);
ChosenPreset = null;
}
float HeadWeight(int index) => BlendRegion == "overall" ? Appearance.BaseShape.HeadWeights[index] : Region()?.HeadWeights[index] ?? 0;
float PhotoWeight(PhotoFace face) => BlendRegion == "overall" ? face.Influence : Region()?.PhotoWeights.GetValueOrDefault(face.Id) ?? 0;
string BlendWeightLabel(int index)
{
float share = FaceRegions.NativeShare(Appearance.BaseShape, index, BlendRegion);
return $"{MathF.Round(HeadWeight(index) * 100)} influence · {MathF.Round(share * 100)}% of blend";
}
void SetHeadWeight(int index, float value)
{
if (Appearance.BaseShape is null || !float.IsFinite(value))
return;
(BlendRegion == "overall" ? Appearance.BaseShape.HeadWeights : Region(true).HeadWeights)[index] = Math.Clamp(value, 0, 1);
ChosenPreset = null;
}
void SetPhotoWeight(PhotoFace face, float value)
{
if (!float.IsFinite(value))
return;
if (BlendRegion == "overall")
face.Influence = Math.Clamp(value, 0, 1);
else
Region(true).PhotoWeights[face.Id] = Math.Clamp(value, 0, 1);
ChosenPreset = null;
}
void ResetRegion()
{
if (BlendRegion != "overall")
Appearance.BaseShape.Regions.Remove(BlendRegion);
ChosenPreset = null;
}
void SetBodyBlend(float value)
{
if (Appearance.BaseShape is null || !float.IsFinite(value))
return;
Appearance.BaseShape.MaleBody = Math.Clamp(value, 0, 1);
ChosenPreset = null;
}
void ToggleClothingVariant(string variant)
{
if (Appearance.BaseShape is null)
return;
Appearance.BaseShape.ClothingVariant = Appearance.BaseShape.ClothingVariant == variant ? null : variant;
ChosenPreset = null;
}
void SetBodyFeature(bool breast, float value)
{
if (Appearance.BaseShape is null || !float.IsFinite(value))
return;
if (breast)
Appearance.BaseShape.BreastSize = Math.Clamp(value, 0, 1);
else
Appearance.BaseShape.BulgeSize = Math.Clamp(value, 0, 1);
ChosenPreset = null;
}
void ChooseBody(string body)
{
var skin = AppearanceCatalog.Skins.FirstOrDefault(x => x.BodyType == body);
if (skin is not null)
ChooseSkin(skin);
}
void ChooseSkin(AppearanceOption skin)
{
if (LoadingCatalog)
return;
Appearance.SkinPath = skin.ResourcePath;
if (Appearance.BaseShape is null && !AppearanceCatalog.FacialHair.Any(x => x.ResourcePath == Appearance.FacialHairPath && (x.BodyType == "" || x.BodyType == skin.BodyType)))
Appearance.FacialHairPath = "";
ChosenPreset = null;
Feedback = null;
}
void ChooseHair(AppearanceOption hair)
{
if (LoadingCatalog)
return;
Appearance.HairPath = hair.ResourcePath;
ChosenPreset = null;
Feedback = null;
}
void ChooseFacial(AppearanceOption facial)
{
if (LoadingCatalog)
return;
Appearance.FacialHairPath = facial.ResourcePath;
ChosenPreset = null;
Feedback = null;
}
void ChooseColor(AppearanceColorOption color)
{
if (LoadingCatalog)
return;
Appearance.HairColor = color.Color;
ChosenPreset = null;
Feedback = null;
}
void ChooseSkinColor(Color color, bool normalize = false)
{
if (LoadingCatalog)
return;
Appearance.SkinColor = color;
Appearance.NormalizeSkinTone = normalize;
ChosenPreset = null;
Feedback = null;
}
static IReadOnlyList<AppearanceColorOption> EyeColors => new[]
{
new AppearanceColorOption("brown", "Brown", new(.24f, .12f, .055f)),
new("hazel", "Hazel", new(.34f, .3f, .11f)),
new("amber", "Amber", new(.58f, .34f, .075f)),
new("green", "Green", new(.13f, .34f, .19f)),
new("blue", "Blue", new(.09f, .3f, .55f)),
new("grey", "Grey", new(.32f, .36f, .4f)),
new("violet", "Violet", new(.32f, .16f, .46f))
};
void ChooseEyeColor(Color? color)
{
if (LoadingCatalog || LoadingCatalog)
return;
Appearance.CustomEyeColor = color.HasValue;
if (color.HasValue)
Appearance.EyeColor = color.Value;
}
float EyeChannel(string channel) => channel == "Red" ? Appearance.EyeColorR : channel == "Green" ? Appearance.EyeColorG : Appearance.EyeColorB;
void SetEyeChannel(string channel, float value)
{
if (LoadingCatalog || LoadingCatalog)
return;
Appearance.CustomEyeColor = true;
value = Math.Clamp(value, 0, 1);
if (channel == "Red")
Appearance.EyeColorR = value;
else if (channel == "Green")
Appearance.EyeColorG = value;
else
Appearance.EyeColorB = value;
}
float SkinChannel(string channel) => channel == "Red" ? Appearance.SkinColorR : channel == "Green" ? Appearance.SkinColorG : Appearance.SkinColorB;
void SetSkinChannel(string channel, float value)
{
if (LoadingCatalog || !float.IsFinite(value))
return;
value = Math.Clamp(value, 0, 1);
Appearance.NormalizeSkinTone = true;
if (channel == "Red")
Appearance.SkinColorR = value;
else if (channel == "Green")
Appearance.SkinColorG = value;
else
Appearance.SkinColorB = value;
ChosenPreset = null;
Feedback = null;
}
void SetShape(AppearanceSlider slider, float value)
{
if (LoadingCatalog)
return;
slider.Set(Appearance, value);
ChosenPreset = null;
Feedback = null;
}
void ResetShapes()
{
if (LoadingCatalog)
return;
AppearanceCatalog.ResetShapes(Appearance);
ChosenPreset = null;
Feedback = null;
}
void Randomize()
{
if (LoadingCatalog)
return;
var voice = Appearance.VoiceId;
var variant = Appearance.BaseShape?.ClothingVariant;
var photos = Appearance.BaseShape?.PhotoFaces.Select(x => x.DeepCopy()).ToList();
Appearance = AppearanceCatalog.Randomize(blendHeads: Appearance.BaseShape is not null);
Appearance.VoiceId = voice;
if (Appearance.BaseShape is not null)
{
Appearance.BaseShape.ClothingVariant = variant;
if (photos is not null)
{
foreach (var face in photos)
face.Influence = 0;
Appearance.BaseShape.PhotoFaces = photos;
}
}
ChosenPreset = null;
Feedback = null;
}
void Reset()
{
if (LoadingCatalog)
return;
var voice = Appearance.VoiceId;
bool unified = Appearance.BaseShape is not null;
Appearance = AppearanceCatalog.CreateDefault();
if (unified)
UnifiedHuman.Enable(Appearance);
Appearance.VoiceId = voice;
ChosenPreset = "traveller";
Yaw = 0;
RecenterPreview();
SetZoom(CharacterCreatorPreview.FaceZoom);
Feedback = null;
}
void Rotate(float delta)
{
Yaw = (Yaw + delta) % 360;
}
void SetYaw(float value)
{
Yaw = value;
}
void SetView(float value)
{
RecenterPreview();
SetZoom(value);
}
void SetZoom(float value)
{
if (float.IsFinite(value))
{
Zoom = Math.Clamp(value, 0, CharacterCreatorPreview.MaxZoom);
Portrait = Zoom >= CharacterCreatorPreview.FaceZoom - 0.001f;
}
}
void SetPitch(float value)
{
PreviewPitch = value;
}
void SetPan(Vector3 value)
{
PreviewPan = value;
}
void RecenterPreview()
{
Yaw = 0;
PreviewPan = Vector3.Zero;
PreviewPitch = 0;
Preview?.Recenter();
}
void ToggleClothing()
{
ShowClothing = !ShowClothing;
}
void Back()
{
if (!LoadingCatalog)
{
StopVoicePreview();
Closed?.Invoke();
}
}
void Start()
{
if (Disabled || LoadingCatalog || Preview?.Loading == true)
return;
if (!CatalogReady)
{
Feedback = LoadingCatalog ? "Loading character options…" : "Character options could not be loaded.";
return;
}
if (CharacterAppearance.ValidateForApply(Appearance)is string error)
{
Feedback = error;
return;
}
StopVoicePreview();
Feedback = Saved?.Invoke(CharacterName, Appearance.DeepCopy());
}
public override void Delete(bool immediate = false)
{
StopVoicePreview();
base.Delete(immediate);
}
protected override async Task OnParametersSetAsync()
{
if (InitialPreset is not null && !ReferenceEquals(loadedPreset, InitialPreset))
{
LoadDraft(InitialPreset.Name, InitialPreset.Appearance);
loadedPreset = InitialPreset;
}
if (CatalogReady || LoadingCatalog)
return;
LoadingCatalog = true;
try
{
await AppearanceCatalog.EnsureLoadedAsync();
CatalogReady = true;
}
catch (Exception)
{
Feedback = "Character options could not be loaded. Please try again.";
}
finally
{
LoadingCatalog = false;
}
}
protected override int BuildHash() => HashCode.Combine(CharacterName, PlaythroughName, HashCode.Combine(Tab, ShapeCategory, SkinPalette, MakeupLayer, CustomMakeup, SelectedMarking, MarkingSection, BlendRegion), ChosenPreset, JsonSerializer.Serialize(Appearance) + Appearance.Makeup.PresentationKey(), HashCode.Combine(Portrait, Yaw, Feedback, LoadingCatalog, SaveLabel, Preview?.Error, CatalogReady, HashCode.Combine(LoadingCatalog, Disabled, ProductLabel, LocationLabel, Title)), Zoom, HashCode.Combine(ShowClothing, PreviewPan, PreviewPitch, WardrobeSearch, WardrobeCategory, ClothingResources.Revision, CloudWardrobeOpen));
}
Game
library
using System;
namespace CharacterEditor;
/// <summary>Local GPU albedo composition retains the native skin shader and geometry.</summary>
public sealed class HumanMakeupPresentation : IDisposable
{
static ComputeShader compositor;
Texture result;
Material original;
CharacterMakeupData makeup;
string skinPath;
bool head;
int armSide;
readonly MakeupRefreshWindow refresh = new();
Texture source;
Texture baseColorOverride;
int sourceWidth, sourceHeight;
Texture[] inputs = new Texture[8];
double lastRefreshTime = double.NaN;
public Texture Apply(Material original, CharacterMakeupData makeup, string skinPath, bool head = true, int armSide = 0, Texture baseColorOverride = null)
{
this.original = original;
this.makeup = makeup.DeepCopy();
this.skinPath = skinPath;
this.head = head;
this.armSide = armSide;
this.baseColorOverride = baseColorOverride;
source = baseColorOverride ?? original.GetTexture("g_tColor");
refresh.Reset();
lastRefreshTime = double.NaN;
// A material can exist while its native albedo is still loading. Keep that
// original bound until the compute inputs are ready instead of baking its fallback.
return Refresh();
}
public Texture Refresh()
{
var input = baseColorOverride ?? original.GetTexture("g_tColor");
if (input != source || input?.Width != sourceWidth || input?.Height != sourceHeight)
{
source = input;
sourceWidth = input?.Width ?? 0;
sourceHeight = input?.Height ?? 0;
refresh.Reset();
}
var inputs = Inputs();
foreach (var texture in inputs)
if (texture is not null)
texture.MarkUsed(Math.Max(texture.Width, texture.Height));
bool ready = true;
foreach (var texture in inputs)
if (texture is not { IsValid: true, IsError: false, IsLoaded: true })
{
ready = false;
break;
}
var now = Time.NowDouble;
var delta = now == lastRefreshTime ? 0 : Math.Clamp(Time.Delta, 0, .1f);
lastRefreshTime = now;
// Count actual presentation updates, not wall time spent in the client's load screen.
// Streaming refresh is finite once the real inputs become ready.
if (refresh.Step(ready, delta))
Compose();
return ready && result is not null ? result : source;
}
Texture[] Inputs()
{
// Live hotload can retain an older presentation's input array.
if (inputs is null || inputs.Length != 8) inputs = new Texture[8];
inputs[0] = source;
inputs[1] = HumanMakeupAssets.Mask(skinPath);
inputs[2] = HumanMakeupAssets.Marking(skinPath);
inputs[3] = HumanMakeupAssets.EyeDetail(skinPath);
inputs[4] = HumanMakeupAssets.Surface(skinPath, "position_hi", head);
inputs[5] = HumanMakeupAssets.Surface(skinPath, "position_lo", head);
inputs[6] = HumanMakeupAssets.Surface(skinPath, "normal", head);
inputs[7] = HumanMakeupAssets.LinerDetail;
return inputs;
}
Texture Compose()
{
source.MarkUsed(Math.Max(source.Width, source.Height));
if (result is null || result.Width != source.Width || result.Height != source.Height)
{
result?.Dispose();
result = Texture.Create(source.Width, source.Height).WithFormat(ImageFormat.RGBA8888).WithUAVBinding().WithName("Local human makeup").Finish();
}
var mask = HumanMakeupAssets.Mask(skinPath);
var marking = HumanMakeupAssets.Marking(skinPath);
mask.MarkUsed(Math.Max(mask.Width, mask.Height));
marking.MarkUsed(Math.Max(marking.Width, marking.Height));
var attributes = new RenderAttributes();
attributes.Set("BaseColor", source);
attributes.Set("Coverage", mask);
attributes.Set("Marking", marking);
attributes.Set("Result", result);
var eyeDetail = HumanMakeupAssets.EyeDetail(skinPath);
eyeDetail.MarkUsed(Math.Max(eyeDetail.Width, eyeDetail.Height));
attributes.Set("EyeDetail", eyeDetail);
attributes.Set("LinerDetail", HumanMakeupAssets.LinerDetail);
attributes.Set("LinerOptions", new Vector4(makeup.EyelinerLowerStrength, makeup.EyelinerInnerStrength, makeup.EyelinerWing == "wing" ? 4f : makeup.EyelinerWing == "downward" ? 3f : makeup.EyelinerWing == "cat-eye" ? 2f : makeup.EyelinerWing == "flick" ? 1f : 0f, makeup.EyelinerWingStrength));
attributes.Set("WingTip", new Vector4(makeup.WingTipHorizontal, makeup.WingTipVertical, makeup.EyelinerWingThickness, 0));
attributes.Set("Liner", new Vector4(makeup.Eyeliner.Red, makeup.Eyeliner.Green, makeup.Eyeliner.Blue, makeup.Eyeliner.Amount));
attributes.Set("LowerEye", new Vector4(makeup.LowerEyeshadow.Red, makeup.LowerEyeshadow.Green, makeup.LowerEyeshadow.Blue, makeup.LowerEyeshadow.Amount));
attributes.Set("Lip", new Vector4(makeup.Lipstick.Red, makeup.Lipstick.Green, makeup.Lipstick.Blue, makeup.Lipstick.Amount));
attributes.Set("Eye", new Vector4(makeup.Eyeshadow.Red, makeup.Eyeshadow.Green, makeup.Eyeshadow.Blue, makeup.Eyeshadow.Amount));
attributes.Set("Blush", new Vector4(makeup.Blush.Red, makeup.Blush.Green, makeup.Blush.Blue, makeup.Blush.Amount));
attributes.Set("Tattoo", new Vector4(makeup.Tattoo.Red, makeup.Tattoo.Green, makeup.Tattoo.Blue, makeup.TattooStyle == "none" ? 0 : makeup.Tattoo.Amount));
attributes.Set("IsHead", head ? 1f : 0f);
attributes.Set("ArmSide", (float)armSide);
foreach (var kind in new[]
{
"position_hi",
"position_lo",
"normal"
}
)
{
var texture = HumanMakeupAssets.Surface(skinPath, kind, head);
texture.MarkUsed(Math.Max(texture.Width, texture.Height));
attributes.Set(kind == "position_hi" ? "PositionHigh" : kind == "position_lo" ? "PositionLow" : "SurfaceNormal", texture);
}
var layers = makeup.Markings.Where(x => head ? MarkingCatalog.OnHead(x) : MarkingCatalog.OnMaterial(x, armSide)).ToArray();
for (var i = 0; i < MarkingCatalog.Maximum; i++)
{
var name = i.ToString();
if (i >= layers.Length)
{
attributes.Set("Ink" + name, new Vector4(0, 0, 0, 0));
continue;
}
var layer = layers[i];
var region = MarkingCatalog.Region(layer.Region);
var style = MarkingCatalog.Style(layer.Style);
var frame = MarkingCatalog.Frame(layer);
var center = frame.Center;
var scale = region.Size * MarkingCatalog.StampScale(layer);
var width = style.Band ? MarkingCatalog.BandScale(layer) : .38f * scale;
var height = style.Band ? MarkingCatalog.BandSpacing(layer) : .45f * scale;
attributes.Set("Center" + name, new Vector4(center.x, center.y, center.z, style.ShaderId));
attributes.Set("Right" + name, new Vector4(frame.Right.x, frame.Right.y, frame.Right.z, width));
attributes.Set("Up" + name, new Vector4(frame.Up.x, frame.Up.y, frame.Up.z, height));
attributes.Set("Normal" + name, new Vector4(frame.Normal.x, frame.Normal.y, frame.Normal.z, (layer.Rotation - .5f) * MathF.PI * 2));
attributes.Set("Min" + name, new Vector4(region.Min.x, region.Min.y, region.Min.z, frame.Projection));
attributes.Set("Max" + name, new Vector4(region.Max.x, region.Max.y, region.Max.z, style.Band ? Math.Clamp(MathF.Round(MathF.PI * 2 * region.Radius / (1.6f * height)), 3, 36) : frame.Phase));
attributes.Set("Ink" + name, new Vector4(layer.Ink.Red, layer.Ink.Green, layer.Ink.Blue, style.ShaderId == 0 ? 0 : layer.Ink.Amount));
}
compositor ??= new(HumanMakeupAssets.ShaderPath);
compositor.DispatchWithAttributes(attributes, source.Width, source.Height, 1);
return result;
}
public void Dispose()
{
result?.Dispose();
result = null;
}
}
/// <summary>Finite mip refresh starts when native input textures are ready, measured in presentation updates.</summary>
public sealed class MakeupRefreshWindow
{
bool ready;
float elapsed, next;
public void Reset()
{
ready = false;
elapsed = 0;
next = 0;
}
public bool Step(bool inputsReady, float delta)
{
if (!inputsReady)
{
Reset();
return false;
}
if (!ready)
{
ready = true;
elapsed = 0;
next = 0;
}
elapsed += Math.Clamp(delta, 0, .1f);
if (elapsed > 3 || elapsed < next)
return false;
next = elapsed + .25f;
return true;
}
}
Game
library
namespace CharacterEditor;
/// <summary>Authored human cuts fitted automatically to both unified body endpoints.</summary>
public static partial class UnifiedWardrobe
{
/// <summary>Head replacements can retain skin or deliberately replace the entire neck.</summary>
public static bool RetainsHumanNeck(Clothing item)
{
if (string.IsNullOrEmpty(item.HumanSkinModel)) return true;
var skin = Model.Load(item.HumanSkinModel);
// Use the replacement's native skin presentation. A transparent/non-skin
// head material must not be replaced with the unified human's skin neck.
return skin.MeshInfo.Meshes.Where(m => m.Name == "head_01")
.SelectMany(m => m.DrawCalls).Any(d => d.Material?.ShaderName == "shaders/skin.shader");
}
public static string ModelPath(Clothing item, IEnumerable<Clothing> selected, TagSet tags, CharacterAppearanceData data)
{
var native = item.GetModel(selected.ToArray(), tags);
if (data.BaseShape is null)
return native;
// Preserve conditional model overrides selected by native clothing metadata.
if (native != item.Model && native != item.HumanAltModel && native != item.HumanAltFemaleModel)
return native;
// Explicit preference selects only the authored cut; the morph still uses
// the actual body blend. Single-variant items retain their usual fallback.
if (data.BaseShape.ClothingVariant is not null && !string.IsNullOrEmpty(item.HumanAltModel) && !string.IsNullOrEmpty(item.HumanAltFemaleModel))
{
var choice = data.BaseShape.ClothingVariant == "male" ? item.HumanAltModel : item.HumanAltFemaleModel;
return choice;
}
// A single authored human variant is usable across the full body range.
var preferred = data.BaseShape.MaleBody >= .5f ? item.HumanAltModel : item.HumanAltFemaleModel;
if (string.IsNullOrEmpty(preferred))
preferred = string.IsNullOrEmpty(item.HumanAltModel) ? item.HumanAltFemaleModel : item.HumanAltModel;
return !string.IsNullOrEmpty(preferred) ? preferred : native;
}
public static Model Resolve(Clothing item, IEnumerable<Clothing> selected, TagSet tags, CharacterAppearanceData data)
{
var path = ModelPath(item, selected, tags, data);
var native = Model.Load(path);
if (data.BaseShape is null || string.IsNullOrEmpty(path) || (string.IsNullOrEmpty(item.HumanAltModel) && string.IsNullOrEmpty(item.HumanAltFemaleModel)))
return native;
if (item.Category.ToString().StartsWith("FacialHair") || item.Category.ToString().StartsWith("Hair"))
return native;
bool female = path == item.HumanAltFemaleModel && path != item.HumanAltModel;
if (path != item.HumanAltModel && path != item.HumanAltFemaleModel)
female = tags.Has("female");
return AutomaticWardrobeFit.Fit(native, female, fitFace: (item.HideBody & Clothing.BodyGroups.Head) == 0);
}
public static void Apply(SkinnedModelRenderer renderer, CharacterAppearanceData data)
{
if (data.BaseShape is not null && AutomaticWardrobeFit.IsFitted(renderer.Model))
renderer.Morphs.Set("ce_body_female", 1 - data.BaseShape.MaleBody);
}
}// Keep hair outside body fitting.
Debug: View Raw JSON Response
{
"TotalCount": 94,
"Files": [
{
"Ident": "vendetta.characterkit",
"Path": "CharacterKit/AutomaticWardrobeFit.cs",
"FileName": "AutomaticWardrobeFit.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\nusing System.IO;\r\nusing System.Text;\r\nusing System.Runtime.InteropServices;\r\n\r\nnamespace CharacterEditor;\r\n/// <summary>Fits loaded human clothes against shared body surfaces, without garment authoring files.</summary>\r\npublic static class AutomaticWardrobeFit\r\n{\r\n\tconst string ReferencePath = \"models/characterkit/wardrobe-reference.bin\";\r\n\tpublic const string Morph = \"ce_body_female\";\r\n\t// Cache by resource identity and authored cut, never by the current slider value.\r\n\tstatic readonly Dictionary<(Model, bool, bool), Model> fittedModels = new();\r\n\tstatic readonly Dictionary<(Model, bool, bool), string> fittingFailures = new();\r\n\tstatic Surface originalMale, unifiedMale, unifiedFemale;\r\n\tstatic Surface headSurface;\r\n\tstatic Model replacementNeck;\r\n\tpublic static int CachedModels => fittedModels.Count;\r\n\r\n\tpublic static void ClearCache()\r\n\t{\r\n\t\tfittedModels.Clear();\r\n\t\tfittingFailures.Clear();\r\n\t\treplacementNeck = null;\r\n\t\theadSurface = null;\r\n\t}\r\n\r\n\t/// <summary>Keep the lower neck when clothing replaces the head bodygroup.</summary>\r\n\tpublic static Model ReplacementNeck()\r\n\t{\r\n\t\tif (replacementNeck is not null) return replacementNeck;\r\n\t\tReferences();\r\n\t\treturn replacementNeck = Build(Model.Load(UnifiedHuman.ModelPath), false, neckOnly: true);\r\n\t}\r\n\r\n\tpublic static bool IsFitted(Model model) => model?.Name.StartsWith(\"automatic-human-fit:\", StringComparison.Ordinal) == true;\r\n\tpublic static Model Fit(Model source, bool female, bool fitFace = true)\r\n\t{\r\n\t\tif (source is null || source.IsError || IsFitted(source) || source.BoneCount == 0 || source.Bounds.Mins.z > 60)\r\n\t\t\treturn source;\r\n\t\tif (fittedModels.TryGetValue((source, female, fitFace), out var ready))\r\n\t\t\treturn ready;\r\n\t\tif (fittingFailures.TryGetValue((source, female, fitFace), out var problem))\r\n\t\t\tthrow new InvalidOperationException(problem);\r\n\t\ttry\r\n\t\t{\r\n\t\t\tReferences();\r\n\t\t\tvar fitted = Build(source, female, fitFace: fitFace);\r\n\t\t\tfittedModels[(source, female, fitFace)] = fitted;\r\n\t\t\treturn fitted;\r\n\t\t}\r\n\t\tcatch (Exception error)\r\n\t\t{\r\n\t\t\tvar message = \"Cannot automatically fit \" + source.Name + \": \" + error.Message;\r\n\t\t\tfittingFailures[(source, female, fitFace)] = message;\r\n\t\t\tthrow new InvalidOperationException(message, error);\r\n\t\t}\r\n\t}\r\n\r\n\tstatic void References()\r\n\t{\r\n\t\tif (unifiedMale is not null)\r\n\t\t\treturn;\r\n\t\tusing var stream = new MemoryStream(FileSystem.Mounted.ReadAllBytes(ReferencePath).ToArray());\r\n\t\tusing var r = new BinaryReader(stream);\r\n\t\tif (r.ReadInt32() != 1)\r\n\t\t\tthrow new InvalidOperationException(\"Unsupported human fitting reference.\");\r\n\t\toriginalMale = ReadSurface(r);\r\n\t\tunifiedMale = ReadSurface(r);\r\n\t\tunifiedFemale = new(unifiedMale.Female, unifiedMale.Female, unifiedMale.Triangles, unifiedMale.Names, unifiedMale.Weights);\r\n\t}\r\n\r\n\tstatic Surface HeadSurface()\r\n\t{\r\n\t\tif (headSurface is not null) return headSurface;\r\n\t\tvar source = Model.Load(UnifiedHuman.ModelPath);\r\n\t\tvar buffers = ReadBuffers(source);\r\n\t\tvar points = new List<Vector3>();\r\n\t\tvar triangles = new List<int>();\r\n\t\tfor (int mi = 0; mi < buffers.Count; mi++)\r\n\t\t{\r\n\t\t\tvar info = source.MeshInfo.Meshes[mi];\r\n\t\t\tif (!info.Name.StartsWith(\"head\", StringComparison.OrdinalIgnoreCase)) continue;\r\n\t\t\tvar buffer = buffers[mi];\r\n\t\t\tint callIndex = 0;\r\n\t\t\tforeach (var call in info.DrawCalls)\r\n\t\t\t{\r\n\t\t\t\tint vb = buffer.Vertices.Length == 1 ? 0 : callIndex;\r\n\t\t\t\tint ib = buffer.Indices.Length == 1 ? 0 : callIndex;\r\n\t\t\t\tvar vertices = Vertices(buffer.Vertices[vb], null, buffer.Bones.Length);\r\n\t\t\t\tvar indices = Indices(buffer.Indices[ib]);\r\n\t\t\t\tint offset = points.Count;\r\n\t\t\t\tpoints.AddRange(vertices.Select(v => v.Position));\r\n\t\t\t\ttriangles.AddRange(indices.Skip(call.StartIndex).Take(call.Indices).Select(i => i + call.BaseVertex + offset));\r\n\t\t\t\tcallIndex++;\r\n\t\t\t}\r\n\t\t}\r\n\t\tvar p = points.ToArray();\r\n\t\treturn headSurface = new(p, p, triangles.ToArray(), Array.Empty<string>(), Array.Empty<(int, float)[]>());\r\n\t}\r\n\r\n\tstatic Vector3 V(BinaryReader r) => new(r.ReadSingle(), r.ReadSingle(), r.ReadSingle());\r\n\tstatic Surface ReadSurface(BinaryReader r)\r\n\t{\r\n\t\tvar names = new string[r.ReadInt32()];\r\n\t\tfor (int i = 0; i < names.Length; i++)\r\n\t\t\tnames[i] = Encoding.UTF8.GetString(r.ReadBytes(r.ReadInt32()));\r\n\t\tvar count = r.ReadInt32();\r\n\t\tvar triangles = new int[r.ReadInt32() * 3];\r\n\t\tvar p = new Vector3[count];\r\n\t\tvar f = new Vector3[count];\r\n\t\tvar weights = new (int, float)[count][];\r\n\t\tfor (int i = 0; i < count; i++)\r\n\t\t{\r\n\t\t\tp[i] = V(r);\r\n\t\t\tf[i] = V(r);\r\n\t\t\tweights[i] = new (int, float)[r.ReadInt32()];\r\n\t\t\tfor (int j = 0; j < weights[i].Length; j++)\r\n\t\t\t\tweights[i][j] = (r.ReadInt32(), r.ReadSingle());\r\n\t\t}\r\n\r\n\t\tfor (int i = 0; i < triangles.Length; i++)\r\n\t\t\ttriangles[i] = r.ReadInt32();\r\n\t\treturn new(p, f, triangles, names, weights);\r\n\t}\r\n\r\n\t[StructLayout(LayoutKind.Sequential)]\r\n\tstruct SkinVertex\r\n\t{\r\n\t\t[VertexLayout.Position]\r\n\t\tpublic Vector3 Position;\r\n\t\t[VertexLayout.Normal]\r\n\t\tpublic Vector3 Normal;\r\n\t\t[VertexLayout.Tangent]\r\n\t\tpublic Vector4 Tangent;\r\n\t\t[VertexLayout.TexCoord(0)]\r\n\t\tpublic Vector4 UV;\r\n\t\t[VertexLayout.TexCoord(1)]\r\n\t\tpublic Vector4 UV2;\r\n\t\t[VertexLayout.Color]\r\n\t\tpublic Color32 Color;\r\n\t\t[VertexLayout.BlendIndices]\r\n\t\tpublic Color32 Indices;\r\n\t\t[VertexLayout.BlendWeight]\r\n\t\tpublic Color32 Weights;\r\n\t}\r\n\r\n\treadonly record struct Attribute(string Name, int Index, int Format, int Offset);\r\n\tsealed record Buffer(int Count, int Stride, Attribute[] Attributes, byte[] Data);\r\n\tsealed record Buffers(Buffer[] Vertices, Buffer[] Indices, string[] Bones);\r\n\t// SaveToVmdl emits unpacked MBUF data, even when the installed resource uses\r\n\t// compressed buffers. Reading that public serialization retains authored weights.\r\n\tstatic List<Buffers> ReadBuffers(Model source)\r\n\t{\r\n\t\tvar data = source.SaveToVmdl();\r\n\t\tusing var r = new BinaryReader(new MemoryStream(data));\r\n\t\tr.BaseStream.Position = 8;\r\n\t\tvar table = 8 + r.ReadInt32();\r\n\t\tvar blocks = r.ReadInt32();\r\n\t\tvar result = new List<Buffers>();\r\n\t\tfor (int i = 0; i < blocks; i++)\r\n\t\t{\r\n\t\t\tr.BaseStream.Position = table + i * 12;\r\n\t\t\tvar tag = Encoding.ASCII.GetString(r.ReadBytes(4));\r\n\t\t\tvar anchor = (int)r.BaseStream.Position;\r\n\t\t\tvar offset = r.ReadInt32();\r\n\t\t\tvar size = r.ReadInt32();\r\n\t\t\tif (tag == \"MDAT\")\r\n\t\t\t{\r\n\t\t\t\tr.BaseStream.Position = anchor + offset;\r\n\t\t\t\tvar bones = SerializedMeshBones.Read(r.ReadBytes(size));\r\n\t\t\t\tresult[^1] = result[^1] with\r\n\t\t\t\t{\r\n\t\t\t\t\tBones = bones\r\n\t\t\t\t};\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (tag != \"MBUF\")\r\n\t\t\t\tcontinue;\r\n\t\t\tvar start = anchor + offset;\r\n\t\t\tr.BaseStream.Position = start;\r\n\t\t\tvar vo = r.ReadInt32();\r\n\t\t\tvar vc = r.ReadInt32();\r\n\t\t\tvar io = r.ReadInt32();\r\n\t\t\tvar ic = r.ReadInt32();\r\n\t\t\tresult.Add(new(Enumerable.Range(0, vc).Select(j => ReadBuffer(r, start + vo + j * 24)).ToArray(), Enumerable.Range(0, ic).Select(j => ReadBuffer(r, start + 8 + io + j * 24)).ToArray(), null));\r\n\t\t}\r\n\r\n\t\tif (result.Count != source.MeshCount)\r\n\t\t\tthrow new InvalidOperationException($\"{source.Name}: missing serialized mesh buffers.\");\r\n\t\treturn result;\r\n\t}\r\n\r\n\tstatic Buffer ReadBuffer(BinaryReader r, int position)\r\n\t{\r\n\t\tr.BaseStream.Position = position;\r\n\t\tvar count = r.ReadInt32();\r\n\t\tvar stride = r.ReadInt32() & 0x3ffffff;\r\n\t\tvar ap = (int)r.BaseStream.Position;\r\n\t\tvar ao = r.ReadInt32();\r\n\t\tvar ac = r.ReadInt32();\r\n\t\tvar dp = (int)r.BaseStream.Position;\r\n\t\tvar offset = r.ReadInt32();\r\n\t\tvar bytes = r.ReadInt32();\r\n\t\tif (count < 0 || count > 2000000 || stride < 1 || stride > 1024 || bytes != count * stride)\r\n\t\t\tthrow new InvalidOperationException(\"Unexpected serialized human clothing buffer.\");\r\n\t\tvar attributes = new Attribute[ac];\r\n\t\tfor (int i = 0; i < ac; i++)\r\n\t\t{\r\n\t\t\tr.BaseStream.Position = ap + ao + i * 56;\r\n\t\t\tvar name = Encoding.ASCII.GetString(r.ReadBytes(32)).TrimEnd('\\0');\r\n\t\t\tattributes[i] = new(name, r.ReadInt32(), r.ReadInt32(), r.ReadInt32());\r\n\t\t}\r\n\r\n\t\tr.BaseStream.Position = dp + offset;\r\n\t\treturn new(count, stride, attributes, r.ReadBytes(bytes));\r\n\t}\r\n\r\n\tstatic float F(byte[] b, int p) => BitConverter.ToSingle(b, p);\r\n\tstatic ushort U16(byte[] b, int p) => BitConverter.ToUInt16(b, p);\r\n\tstatic uint U32(byte[] b, int p) => BitConverter.ToUInt32(b, p);\r\n\tstatic Color32 Bytes(byte[] b, int p) => new(b[p], b[p + 1], b[p + 2], b[p + 3]);\r\n\tstatic Vector4 Values(byte[] b, int p, int format)\r\n\t{\r\n\t\tfloat HalfAt(int i) => (float)BitConverter.UInt16BitsToHalf(U16(b, p + i * 2));\r\n\t\treturn format switch\r\n\t\t{\r\n\t\t\t2 => new(F(b, p), F(b, p + 4), F(b, p + 8), F(b, p + 12)),\r\n\t\t\t6 => new(F(b, p), F(b, p + 4), F(b, p + 8), 0),\r\n\t\t\t16 => new(F(b, p), F(b, p + 4), 0, 0),\r\n\t\t\t10 => new(HalfAt(0), HalfAt(1), HalfAt(2), HalfAt(3)),\r\n\t\t\t34 => new(HalfAt(0), HalfAt(1), 0, 0),\r\n\t\t\t28 => new(b[p] / 255f, b[p + 1] / 255f, b[p + 2] / 255f, b[p + 3] / 255f),\r\n\t\t\t_ => throw new InvalidOperationException(\"Unsupported clothing vertex format \" + format)};\r\n\t}\r\n\r\n\tstatic SkinVertex[] Vertices(Buffer b, Vertex[] native, int boneCount)\r\n\t{\r\n\t\tvar vertices = new SkinVertex[b.Count];\r\n\t\tfor (int i = 0; i < b.Count; i++)\r\n\t\t{\r\n\t\t\tvar v = new SkinVertex\r\n\t\t\t{\r\n\t\t\t\tColor = new(255, 255, 255, 255),\r\n\t\t\t\tWeights = new(255, 0, 0, 0)\r\n\t\t\t};\r\n\t\t\tforeach (var a in b.Attributes)\r\n\t\t\t{\r\n\t\t\t\tvar p = i * b.Stride + a.Offset;\r\n\t\t\t\tswitch (a.Name)\r\n\t\t\t\t{\r\n\t\t\t\t\tcase \"POSITION\":\r\n\t\t\t\t\t\tvar xyz = Values(b.Data, p, a.Format);\r\n\t\t\t\t\t\tv.Position = new(xyz.x, xyz.y, xyz.z);\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase \"TEXCOORD\":\r\n\t\t\t\t\t\tif (a.Index == 0)\r\n\t\t\t\t\t\t\tv.UV = Values(b.Data, p, a.Format);\r\n\t\t\t\t\t\telse if (a.Index == 1)\r\n\t\t\t\t\t\t\tv.UV2 = Values(b.Data, p, a.Format);\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase \"BLENDINDICES\":\r\n\t\t\t\t\t\tif (a.Format == 30)\r\n\t\t\t\t\t\t\tv.Indices = Bytes(b.Data, p);\r\n\t\t\t\t\t\telse if (a.Format == 12)\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tvar ids = Enumerable.Range(0, 4).Select(j => (int)U16(b.Data, p + j * 2)).ToArray();\r\n\t\t\t\t\t\t\tif (ids.Any(j => j >= boneCount || j > 255))\r\n\t\t\t\t\t\t\t\tthrow new InvalidOperationException(\"Clothing bone index exceeds fitting layout.\");\r\n\t\t\t\t\t\t\tv.Indices = new((byte)ids[0], (byte)ids[1], (byte)ids[2], (byte)ids[3]);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\telse\r\n\t\t\t\t\t\t\tthrow new InvalidOperationException(\"Unsupported clothing bone-index format \" + a.Format);\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase \"BLENDWEIGHT\":\r\n\t\t\t\t\t\tif (a.Format != 28)\r\n\t\t\t\t\t\t\tthrow new InvalidOperationException(\"Unsupported clothing weight format \" + a.Format);\r\n\t\t\t\t\t\tv.Weights = Bytes(b.Data, p);\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase \"COLOR\":\r\n\t\t\t\t\t\tif (a.Format == 28)\r\n\t\t\t\t\t\t\tv.Color = Bytes(b.Data, p);\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase \"NORMAL\":\r\n\t\t\t\t\t\tif (a.Format == 6)\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tvar n = Values(b.Data, p, a.Format);\r\n\t\t\t\t\t\t\tv.Normal = new(n.x, n.y, n.z);\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t\tcase \"TANGENT\":\r\n\t\t\t\t\t\tv.Tangent = Values(b.Data, p, a.Format);\r\n\t\t\t\t\t\tbreak;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tif (v.Normal.LengthSquared < .01f && native is not null && native.Length == b.Count && (native[i].Position - v.Position).Length < .001f)\r\n\t\t\t{\r\n\t\t\t\tv.Normal = native[i].Normal;\r\n\t\t\t\tv.Tangent = native[i].Tangent;\r\n\t\t\t}\r\n\r\n\t\t\tif (new[]\r\n\t\t\t{\r\n\t\t\t\tv.Indices.r,\r\n\t\t\t\tv.Indices.g,\r\n\t\t\t\tv.Indices.b,\r\n\t\t\t\tv.Indices.a\r\n\t\t\t}.Any(j => j >= boneCount))\r\n\t\t\t\tthrow new InvalidOperationException(\"Serialized garment bone map does not match its skeleton.\");\r\n\t\t\tvertices[i] = v;\r\n\t\t}\r\n\r\n\t\treturn vertices;\r\n\t}\r\n\r\n\tstatic int[] Indices(Buffer b) => Enumerable.Range(0, b.Count).Select(i => b.Stride == 2 ? (int)U16(b.Data, i * 2) : checked((int)U32(b.Data, i * 4))).ToArray();\r\n\tstatic Vector3[] Normals(Vector3[] p, int[] indices)\r\n\t{\r\n\t\tvar n = new Vector3[p.Length];\r\n\t\tfor (int i = 0; i + 2 < indices.Length; i += 3)\r\n\t\t{\r\n\t\t\tvar a = indices[i];\r\n\t\t\tvar b = indices[i + 1];\r\n\t\t\tvar c = indices[i + 2];\r\n\t\t\tvar cross = Vector3.Cross(p[b] - p[a], p[c] - p[a]);\r\n\t\t\tn[a] += cross;\r\n\t\t\tn[b] += cross;\r\n\t\t\tn[c] += cross;\r\n\t\t}\r\n\r\n\t\treturn n.Select(v => v.Normal).ToArray();\r\n\t}\r\n\r\n\tstatic Vector4[] Tangents(SkinVertex[] vertices, Vector3[] p, Vector3[] normals, int[] indices)\r\n\t{\r\n\t\tvar t = new Vector3[p.Length];\r\n\t\tvar b = new Vector3[p.Length];\r\n\t\tfor (int i = 0; i + 2 < indices.Length; i += 3)\r\n\t\t{\r\n\t\t\tint a = indices[i], c = indices[i + 1], d = indices[i + 2];\r\n\t\t\tvar e = p[c] - p[a];\r\n\t\t\tvar f = p[d] - p[a];\r\n\t\t\tvar uv = vertices[c].UV - vertices[a].UV;\r\n\t\t\tvar uw = vertices[d].UV - vertices[a].UV;\r\n\t\t\tfloat determinant = uv.x * uw.y - uv.y * uw.x;\r\n\t\t\tif (MathF.Abs(determinant) < 1e-8f)\r\n\t\t\t\tcontinue;\r\n\t\t\tvar tangent = (e * uw.y - f * uv.y) / determinant;\r\n\t\t\tvar bitangent = (f * uv.x - e * uw.x) / determinant;\r\n\t\t\tforeach (int id in new[]\r\n\t\t\t{\r\n\t\t\t\ta,\r\n\t\t\t\tc,\r\n\t\t\t\td\r\n\t\t\t}\r\n\r\n\t\t\t)\r\n\t\t\t{\r\n\t\t\t\tt[id] += tangent;\r\n\t\t\t\tb[id] += bitangent;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn Enumerable.Range(0, p.Length).Select(i =>\r\n\t\t{\r\n\t\t\tvar n = normals[i];\r\n\t\t\tvar tangent = (t[i] - n * Vector3.Dot(n, t[i])).Normal;\r\n\t\t\treturn new Vector4(tangent, Vector3.Dot(Vector3.Cross(n, tangent), b[i]) < 0 ? -1 : 1);\r\n\t\t}).ToArray();\r\n\t}\r\n\r\n\tstatic Model Build(Model source, bool female, bool neckOnly = false, bool fitFace = true)\r\n\t{\r\n\t\tvar buffers = ReadBuffers(source);\r\n\t\tvar builder = Model.Builder.WithName(\"automatic-human-fit:\" + source.Name + \":\" + (neckOnly ? \"replacement-neck\" : female ? \"female\" : \"male\"));\r\n\t\tfor (int i = 0; i < source.BoneCount; i++)\r\n\t\t{\r\n\t\t\tvar transform = source.GetBoneTransform(i);\r\n\t\t\tvar parent = source.GetBoneParent(i);\r\n\t\t\tbuilder.AddBone(source.GetBoneName(i), transform.Position, transform.Rotation, parent < 0 ? null : source.GetBoneName(parent));\r\n\t\t}\r\n\r\n\t\tvar native = source.GetVertices();\r\n\t\tvar baseMaterials = (source.MaterialGroupCount > 0 ? source.GetMaterials(0) : source.Materials).ToArray();\r\n\t\tvar meshMaterials = new List<Material>();\r\n\t\tvar work = new List<Action>();\r\n\t\tvar clothMale = new List<Vector3>();\r\n\t\tvar clothFemale = new List<Vector3>();\r\n\t\tvar clothIndices = new List<int>();\r\n\t\tvar head = neckOnly || !fitFace ? null : HeadSurface();\r\n\t\tfor (int mi = 0; mi < buffers.Count; mi++)\r\n\t\t{\r\n\t\t\tvar info = source.MeshInfo.Meshes[mi];\r\n\t\t\tif (neckOnly && !info.Name.StartsWith(\"head\", StringComparison.OrdinalIgnoreCase)) continue;\r\n\t\t\tvar buffer = buffers[mi];\r\n\t\t\tvar boneMap = buffer.Bones.Select(name => Enumerable.Range(0, source.BoneCount).First(i => source.GetBoneName(i) == name)).ToArray();\r\n\t\t\tint callIndex = 0;\r\n\t\t\tforeach (var call in info.DrawCalls)\r\n\t\t\t{\r\n\t\t\t\t// SaveToVmdl emits individual buffers in draw-call order when a mesh uses\r\n\t\t\t\t// separate streams (its metadata currently labels each stream as zero).\r\n\t\t\t\tint vb = buffer.Vertices.Length == 1 ? 0 : callIndex;\r\n\t\t\t\tint ib = buffer.Indices.Length == 1 ? 0 : callIndex;\r\n\t\t\t\tif (vb >= buffer.Vertices.Length || ib >= buffer.Indices.Length)\r\n\t\t\t\t\tthrow new InvalidOperationException(\"Unexpected garment draw-call buffer mapping.\");\r\n\t\t\t\tvar verts = Vertices(buffer.Vertices[vb], mi == 0 && buffer.Vertices.Length == 1 ? native : null, buffer.Bones.Length);\r\n\t\t\t\tvar indices = Indices(buffer.Indices[ib]);\r\n\t\t\t\tfor (int i = 0; i < verts.Length; i++)\r\n\t\t\t\t{\r\n\t\t\t\t\tvar ids = verts[i].Indices;\r\n\t\t\t\t\tverts[i].Indices = new((byte)boneMap[ids.r], (byte)boneMap[ids.g], (byte)boneMap[ids.b], (byte)boneMap[ids.a]);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tcallIndex++;\r\n\t\t\t\tvar selectedIndices = indices.Skip(call.StartIndex).Take(call.Indices).Select(i => i + call.BaseVertex).ToArray();\r\n\t\t\t\tif (neckOnly)\r\n\t\t\t\t\tselectedIndices = Enumerable.Range(0, selectedIndices.Length / 3)\r\n\t\t\t\t\t\t.Where(t => Enumerable.Range(0, 3).All(j => verts[selectedIndices[t * 3 + j]].Position.z <= 60))\r\n\t\t\t\t\t\t.SelectMany(t => selectedIndices.Skip(t * 3).Take(3)).ToArray();\r\n\t\t\t\tif (selectedIndices.Length == 0) continue;\r\n\t\t\t\tvar used = selectedIndices.Distinct().ToArray();\r\n\t\t\t\tvar lookup = used.Select((id, i) => (id, i)).ToDictionary(x => x.id, x => x.i);\r\n\t\t\t\tvar localIndices = selectedIndices.Select(i => lookup[i]).ToArray();\r\n\t\t\t\tvar v = used.Select(i => verts[i]).ToArray();\r\n\t\t\t\tvar original = v.Select(x => x.Position).ToArray();\r\n\t\t\t\tvar male = new Vector3[v.Length];\r\n\t\t\t\tvar target = new Vector3[v.Length];\r\n\t\t\t\tbool skin = neckOnly || call.Material?.Name.Contains(\"/bodies/\", StringComparison.OrdinalIgnoreCase) == true;\r\n\t\t\t\tfor (int i = 0; i < v.Length; i++)\r\n\t\t\t\t{\r\n\t\t\t\t\tvar p = original[i];\r\n\t\t\t\t\tif (!skin && !neckOnly && fitFace && p.z >= 57 && p.z <= 73)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tvar hit = head.Nearest(p);\r\n\t\t\t\t\t\tif (hit.Distance < 1.2f && hit.Distance < originalMale.Nearest(p).Distance)\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tvar normal = head.FaceNormal(hit.Triangle);\r\n\t\t\t\t\t\t\tfloat clearance = Vector3.Dot(p - hit.Point, normal);\r\n\t\t\t\t\t\t\t// Close face coverings need space for the difference in falloff\r\n\t\t\t\t\t\t\t// between the skin and fabric under inherited facial deformers.\r\n\t\t\t\t\t\t\tfloat margin = .60f * Math.Clamp((62.2f - p.z) / 1.0f, 0, 1);\r\n\t\t\t\t\t\t\tif (margin > 0) p += normal * Math.Clamp(margin - clearance, 0, .85f);\r\n\t\t\t\t\t\t\tmale[i] = target[i] = p;\r\n\t\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif (p.z > 60)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tmale[i] = p;\r\n\t\t\t\t\t\ttarget[i] = p;\r\n\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tvar from = (neckOnly ? unifiedMale : female ? unifiedFemale : originalMale).Nearest(p);\r\n\t\t\t\t\tvar mapped = female || neckOnly ? from : unifiedMale.Nearest(from.Point);\r\n\t\t\t\t\tvar m = unifiedMale.Interpolate(mapped.Triangle, mapped.Barycentric);\r\n\t\t\t\t\tvar f = unifiedFemale.Interpolate(mapped.Triangle, mapped.Barycentric);\r\n\t\t\t\t\t// Preserve authored clearance, including compressed skin under clothing.\r\n\t\t\t\t\tvar offset = p - from.Point;\r\n\t\t\t\t\tbool snap = skin && offset.Length < .12f;\r\n\t\t\t\t\tmale[i] = m + (snap ? Vector3.Zero : offset);\r\n\t\t\t\t\ttarget[i] = f + (snap ? Vector3.Zero : offset);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tint clothOffset = clothMale.Count;\r\n\t\t\t\tif (!skin)\r\n\t\t\t\t{\r\n\t\t\t\t\tclothIndices.AddRange(localIndices.Select(i => i + clothOffset));\r\n\t\t\t\t\tclothMale.AddRange(male);\r\n\t\t\t\t\tclothFemale.AddRange(target);\r\n\t\t\t\t}\r\n\r\n\t\t\t\twork.Add(() =>\r\n\t\t\t\t{\r\n\t\t\t\t\tif (!skin)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tfor (int i = 0; i < v.Length; i++)\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tmale[i] = clothMale[clothOffset + i];\r\n\t\t\t\t\t\t\ttarget[i] = clothFemale[clothOffset + i];\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tvar originalNormals = Normals(original, localIndices);\r\n\t\t\t\t\tvar maleNormals = Normals(male, localIndices);\r\n\t\t\t\t\tvar femaleNormals = Normals(target, localIndices);\r\n\t\t\t\t\tvar tangents = Tangents(v, male, maleNormals, localIndices);\r\n\t\t\t\t\tvar morph = new MorphDelta[v.Length];\r\n\t\t\t\t\tfor (int i = 0; i < v.Length; i++)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif (v[i].Normal.LengthSquared < .01f)\r\n\t\t\t\t\t\t\tv[i].Normal = originalNormals[i];\r\n\t\t\t\t\t\tv[i].Normal = (v[i].Normal + maleNormals[i] - originalNormals[i]).Normal;\r\n\t\t\t\t\t\tv[i].Tangent = tangents[i];\r\n\t\t\t\t\t\tv[i].Position = male[i];\r\n\t\t\t\t\t\tmorph[i] = new(i, target[i] - male[i], femaleNormals[i] - maleNormals[i]);\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tvar mesh = new Mesh(call.Material, (MeshPrimitiveType)call.PrimitiveType);\r\n\t\t\t\t\tmesh.UvDensity = call.UvDensity;\r\n\t\t\t\t\tmesh.CreateVertexBuffer(v.Length, v.ToList());\r\n\t\t\t\t\tmesh.CreateIndexBuffer(localIndices.Length, localIndices.ToList());\r\n\t\t\t\t\tmesh.AddMorph(Morph, morph);\r\n\t\t\t\t\tmesh.Bounds = new BBox(male.Concat(target).Aggregate((a, b) => Vector3.Min(a, b)), male.Concat(target).Aggregate((a, b) => Vector3.Max(a, b)));\r\n\t\t\t\t\tvar lods = info.LodMask;\r\n\t\t\t\t\tif (source.MeshInfo.LodCount == 0)\r\n\t\t\t\t\t\tbuilder.AddMesh(mesh);\r\n\t\t\t\t\telse\r\n\t\t\t\t\t\tfor (int lod = 0; lod < source.MeshInfo.LodCount; lod++)\r\n\t\t\t\t\t\t\tif ((lods & (1 << lod)) != 0)\r\n\t\t\t\t\t\t\t\tbuilder.AddMesh(mesh, lod);\r\n\t\t\t\t\tmeshMaterials.Add(call.Material);\r\n\t\t\t\t});\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (clothMale.Count > 0)\r\n\t\t{\r\n\t\t\tvar m = clothMale.ToArray();\r\n\t\t\tvar f = clothFemale.ToArray();\r\n\t\t\tvar t = clothIndices.ToArray();\r\n\t\t\tClearTriangles(unifiedMale, m, t);\r\n\t\t\tClearTriangles(unifiedFemale, f, t);\r\n\t\t\tclothMale = m.ToList();\r\n\t\t\tclothFemale = f.ToList();\r\n\t\t}\r\n\r\n\t\tforeach (var build in work)\r\n\t\t\tbuild();\r\n\t\tfor (int group = 0; group < source.MaterialGroupCount; group++)\r\n\t\t{\r\n\t\t\tvar materials = source.GetMaterials(group).ToArray();\r\n\t\t\tvar g = builder.AddMaterialGroup(source.GetMaterialGroupName(group));\r\n\t\t\tforeach (var material in meshMaterials)\r\n\t\t\t{\r\n\t\t\t\tvar index = Array.IndexOf(baseMaterials, material);\r\n\t\t\t\tg.AddMaterial(index >= 0 && index < materials.Length ? materials[index] : material);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tfor (int lod = 1; lod < source.MeshInfo.LodSwitchDistances.Length; lod++)\r\n\t\t\tbuilder.WithLodDistance(lod, source.MeshInfo.LodSwitchDistances[lod]);\r\n\t\treturn builder.Create();\r\n\t}\r\n\r\n\tstatic void ClearTriangles(Surface body, Vector3[] points, int[] triangles)\r\n\t{\r\n\t\t// A smooth spatial clearance field moves folded/layered fabric together.\r\n\t\t// Local per-triangle pushes can pull an inner layer through its outer flap.\r\n\t\tvar source = points.ToArray();\r\n\t\tvar min = points.Aggregate((a, b) => Vector3.Min(a, b)) - Vector3.One * 1.2f;\r\n\t\tvar max = points.Aggregate((a, b) => Vector3.Max(a, b)) + Vector3.One * 1.2f;\r\n\t\tvar candidates = body.Points.Where(p => p.x >= min.x && p.x <= max.x && p.y >= min.y && p.y <= max.y && p.z >= min.z && p.z <= max.z).ToArray();\r\n\t\tvar pushes = new List<(Vector3 Point, Vector3 Push)>();\r\n\t\tvar fabric = new Surface(points, points, triangles, Array.Empty<string>(), Array.Empty<(int, float)[]>());\r\n\t\tforeach (var p in candidates)\r\n\t\t{\r\n\t\t\tvar hit = fabric.Nearest(p);\r\n\t\t\tif (hit.Distance > 1.2f)\r\n\t\t\t\tcontinue;\r\n\t\t\tvar w = hit.Barycentric;\r\n\t\t\tif (MathF.Min(w.x, MathF.Min(w.y, w.z)) < .015f)\r\n\t\t\t\tcontinue;\r\n\t\t\tvar n = body.FaceNormal(body.Nearest(p).Triangle);\r\n\t\t\tif (MathF.Abs(Vector3.Dot(n, fabric.FaceNormal(hit.Triangle))) < .25f)\r\n\t\t\t\tcontinue;\r\n\t\t\tvar penetration = Vector3.Dot(p - hit.Point, n) + .14f;\r\n\t\t\tif (penetration <= 0 || penetration > 1.2f)\r\n\t\t\t\tcontinue;\r\n\t\t\tpushes.Add((hit.Point, n * MathF.Min(.7f, penetration + .04f)));\r\n\t\t}\r\n\r\n\t\t// Spatial buckets bound the search and apply identical displacement to UV\r\n\t\t// duplicates, overlapping material sections, and neighbouring LOD vertices.\r\n\t\tconst float radius = 2.5f;\r\n\t\t(int, int, int) Cell(Vector3 p) => ((int)MathF.Floor(p.x / radius), (int)MathF.Floor(p.y / radius), (int)MathF.Floor(p.z / radius));\r\n\t\tvar buckets = pushes.GroupBy(p => Cell(p.Point)).ToDictionary(g => g.Key, g => g.ToArray());\r\n\t\tfor (int i = 0; i < points.Length; i++)\r\n\t\t{\r\n\t\t\tvar cell = Cell(source[i]);\r\n\t\t\tVector3 best = default;\r\n\t\t\tfor (int x = -1; x <= 1; x++)\r\n\t\t\t\tfor (int y = -1; y <= 1; y++)\r\n\t\t\t\t\tfor (int z = -1; z <= 1; z++)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tif (!buckets.TryGetValue((cell.Item1 + x, cell.Item2 + y, cell.Item3 + z), out var values))\r\n\t\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t\tforeach (var p in values)\r\n\t\t\t\t\t\t{\r\n\t\t\t\t\t\t\tvar d = (source[i] - p.Point).Length / radius;\r\n\t\t\t\t\t\t\tif (d >= 1)\r\n\t\t\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t\t\tvar falloff = 1 - d * d * (3 - 2 * d);\r\n\t\t\t\t\t\t\tvar push = p.Push * falloff;\r\n\t\t\t\t\t\t\tif (push.LengthSquared > best.LengthSquared)\r\n\t\t\t\t\t\t\t\tbest = push;\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\tpoints[i] += best;\r\n\t\t}\r\n\t}\r\n\r\n\treadonly record struct Hit(int Triangle, Vector3 Barycentric, Vector3 Point, float Distance);\r\n\tsealed class Surface\r\n\t{\r\n\t\tpublic readonly Vector3[] Points, Female;\r\n\t\tpublic readonly int[] Triangles;\r\n\t\tpublic readonly string[] Names;\r\n\t\tpublic readonly (int, float)[][] Weights;\r\n\t\treadonly Node tree;\r\n\t\tsealed class Node\r\n\t\t{\r\n\t\t\tpublic Vector3 Min, Max;\r\n\t\t\tpublic Node Left, Right;\r\n\t\t\tpublic int[] Faces;\r\n\t\t}\r\n\r\n\t\tpublic Surface(Vector3[] p, Vector3[] f, int[] t, string[] names, (int, float)[][] weights)\r\n\t\t{\r\n\t\t\tPoints = p;\r\n\t\t\tFemale = f;\r\n\t\t\tTriangles = t;\r\n\t\t\tNames = names;\r\n\t\t\tWeights = weights;\r\n\t\t\ttree = BuildTree(Enumerable.Range(0, t.Length / 3).ToArray());\r\n\t\t}\r\n\r\n\t\tVector3 A(int t, int corner) => Points[Triangles[t * 3 + corner]];\r\n\t\tNode BuildTree(int[] faces)\r\n\t\t{\r\n\t\t\tvar p = faces.SelectMany(i => new[] { A(i, 0), A(i, 1), A(i, 2) }).ToArray();\r\n\t\t\tvar node = new Node\r\n\t\t\t{\r\n\t\t\t\tMin = p.Aggregate((a, b) => Vector3.Min(a, b)),\r\n\t\t\t\tMax = p.Aggregate((a, b) => Vector3.Max(a, b))\r\n\t\t\t};\r\n\t\t\tif (faces.Length <= 12)\r\n\t\t\t{\r\n\t\t\t\tnode.Faces = faces;\r\n\t\t\t\treturn node;\r\n\t\t\t}\r\n\r\n\t\t\tvar extent = node.Max - node.Min;\r\n\t\t\tint axis = extent.x > extent.y ? (extent.x > extent.z ? 0 : 2) : (extent.y > extent.z ? 1 : 2);\r\n\t\t\tfloat Coord(Vector3 v) => axis == 0 ? v.x : axis == 1 ? v.y : v.z;\r\n\t\t\tvar sorted = faces.OrderBy(i => Coord(A(i, 0) + A(i, 1) + A(i, 2))).ToArray();\r\n\t\t\tint half = sorted.Length / 2;\r\n\t\t\tnode.Left = BuildTree(sorted.Take(half).ToArray());\r\n\t\t\tnode.Right = BuildTree(sorted.Skip(half).ToArray());\r\n\t\t\treturn node;\r\n\t\t}\r\n\r\n\t\tpublic Vector3 Interpolate(int t, Vector3 w) => A(t, 0) * w.x + A(t, 1) * w.y + A(t, 2) * w.z;\r\n\t\tpublic Vector3 FaceNormal(int t) => Vector3.Cross(A(t, 1) - A(t, 0), A(t, 2) - A(t, 0)).Normal;\r\n\t\tstatic float Distance(Node n, Vector3 p) => (p - Vector3.Max(n.Min, Vector3.Min(n.Max, p))).LengthSquared;\r\n\t\tpublic Hit Nearest(Vector3 p)\r\n\t\t{\r\n\t\t\tfloat best = float.MaxValue;\r\n\t\t\tint face = 0;\r\n\t\t\tVector3 bary = default, point = default;\r\n\t\t\tvoid Search(Node node)\r\n\t\t\t{\r\n\t\t\t\tif (Distance(node, p) > best)\r\n\t\t\t\t\treturn;\r\n\t\t\t\tif (node.Faces is null)\r\n\t\t\t\t{\r\n\t\t\t\t\tvar a = node.Left;\r\n\t\t\t\t\tvar b = node.Right;\r\n\t\t\t\t\tif (Distance(a, p) > Distance(b, p))\r\n\t\t\t\t\t\t(a, b) = (b, a);\r\n\t\t\t\t\tSearch(a);\r\n\t\t\t\t\tSearch(b);\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tforeach (var t in node.Faces)\r\n\t\t\t\t{\r\n\t\t\t\t\tvar w = Closest(p, A(t, 0), A(t, 1), A(t, 2));\r\n\t\t\t\t\tvar q = Interpolate(t, w);\r\n\t\t\t\t\tvar distance = (p - q).LengthSquared;\r\n\t\t\t\t\tif (distance < best)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tbest = distance;\r\n\t\t\t\t\t\tface = t;\r\n\t\t\t\t\t\tbary = w;\r\n\t\t\t\t\t\tpoint = q;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tSearch(tree);\r\n\t\t\treturn new(face, bary, point, MathF.Sqrt(best));\r\n\t\t}\r\n\r\n\t\tstatic Vector3 Closest(Vector3 p, Vector3 a, Vector3 b, Vector3 c)\r\n\t\t{\r\n\t\t\tvar ab = b - a;\r\n\t\t\tvar ac = c - a;\r\n\t\t\tvar ap = p - a;\r\n\t\t\tfloat d1 = Vector3.Dot(ab, ap), d2 = Vector3.Dot(ac, ap);\r\n\t\t\tif (d1 <= 0 && d2 <= 0)\r\n\t\t\t\treturn new(1, 0, 0);\r\n\t\t\tvar bp = p - b;\r\n\t\t\tfloat d3 = Vector3.Dot(ab, bp), d4 = Vector3.Dot(ac, bp);\r\n\t\t\tif (d3 >= 0 && d4 <= d3)\r\n\t\t\t\treturn new(0, 1, 0);\r\n\t\t\tfloat vc = d1 * d4 - d3 * d2;\r\n\t\t\tif (vc <= 0 && d1 >= 0 && d3 <= 0)\r\n\t\t\t{\r\n\t\t\t\tvar v = d1 / (d1 - d3);\r\n\t\t\t\treturn new(1 - v, v, 0);\r\n\t\t\t}\r\n\r\n\t\t\tvar cp = p - c;\r\n\t\t\tfloat d5 = Vector3.Dot(ab, cp), d6 = Vector3.Dot(ac, cp);\r\n\t\t\tif (d6 >= 0 && d5 <= d6)\r\n\t\t\t\treturn new(0, 0, 1);\r\n\t\t\tfloat vb = d5 * d2 - d1 * d6;\r\n\t\t\tif (vb <= 0 && d2 >= 0 && d6 <= 0)\r\n\t\t\t{\r\n\t\t\t\tvar w = d2 / (d2 - d6);\r\n\t\t\t\treturn new(1 - w, 0, w);\r\n\t\t\t}\r\n\r\n\t\t\tfloat va = d3 * d6 - d5 * d4;\r\n\t\t\tif (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0)\r\n\t\t\t{\r\n\t\t\t\tvar w = (d4 - d3) / (d4 - d3 + d5 - d6);\r\n\t\t\t\treturn new(0, 1 - w, w);\r\n\t\t\t}\r\n\r\n\t\t\tfloat sum = va + vb + vc;\r\n\t\t\tif (MathF.Abs(sum) < 1e-12f)\r\n\t\t\t\treturn new(1, 0, 0);\r\n\t\t\treturn new(va / sum, vb / sum, vc / sum);\r\n\t\t}\r\n\t}\r\n}// Runtime fitting integration.\r\n\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "CharacterKit/CharacterAppearance.cs",
"FileName": "CharacterAppearance.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\n\r\nnamespace CharacterEditor;\r\npublic static class CharacterAppearance\r\n{\r\n\tpublic static string ValidateForApply(CharacterAppearanceData data)\r\n\t{\r\n\t\tif (CharacterAppearanceData.Validate(data)is string error)\r\n\t\t\treturn error;\r\n\t\tvar skin = ClothingResources.Get(UnifiedHuman.PresentationSkin(data));\r\n\t\tif (skin?.HasHumanSkin != true)\r\n\t\t\treturn \"The selected native skin resource is unavailable.\";\r\n\t\tvar body = Model.Load(data.BaseShape is null ? HumanMarkingModels.ModelPath(skin.HumanSkinModel, data.Makeup) : UnifiedHuman.ModelPath);\r\n\t\tif (!body.IsValid || body.IsError)\r\n\t\t\treturn \"The selected native human model is unavailable.\";\r\n\t\tvar clothes = new List<Clothing>\r\n\t\t{\r\n\t\t\tskin\r\n\t\t};\r\n\t\tforeach (var path in new[]\r\n\t\t{\r\n\t\t\tdata.HairPath,\r\n\t\t\tdata.FacialHairPath\r\n\t\t}.Where(x => !string.IsNullOrEmpty(x)))\r\n\t\t{\r\n\t\t\tvar clothing = ClothingResources.Get(path);\r\n\t\t\tif (clothing is null)\r\n\t\t\t\treturn \"A selected native hairstyle resource is unavailable.\";\r\n\t\t\tif (path == data.HairPath && !ClothingResources.IsHair(clothing) || path == data.FacialHairPath && !ClothingResources.IsFacialHair(clothing))\r\n\t\t\t\treturn \"The selected item does not belong in this hair slot.\";\r\n\t\t\tclothes.Add(clothing);\r\n\t\t}\r\n\r\n\t\tvar tags = new TagSet(skin.HumanSkinTags.TryGetAll());\r\n\t\ttags.Add(\"human\");\r\n\t\tforeach (var clothing in clothes.Skip(1))\r\n\t\t{\r\n\t\t\tvar path = clothing.GetModel(clothes, tags);\r\n\t\t\tif (string.IsNullOrEmpty(path))\r\n\t\t\t\treturn \"A selected hairstyle does not support this native human body.\";\r\n\t\t\tvar model = Model.Load(path);\r\n\t\t\tif (!model.IsValid || model.IsError)\r\n\t\t\t\treturn \"A selected native hairstyle model is unavailable.\";\r\n\t\t}\r\n\r\n\t\treturn null;\r\n\t}\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "CharacterKit/CharacterCreatorPanel.razor",
"FileName": "CharacterCreatorPanel.razor",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "@using Sandbox\r\n\r\n@using Sandbox.UI\r\n\r\n@using System\r\n\r\n@using System.Text.Json\r\n\r\n@using System.Threading.Tasks\r\n\r\n@namespace CharacterEditor\r\n\r\n@inherits Panel\r\n\r\n<root class=\"interactive creatoroverlay\">\r\n\r\n @if(true)\r\n\r\n {\r\n\r\n <div class=\"creatorwindow\">\r\n\r\n <div class=\"creatorheader\"><div><div class=\"creatorbreadcrumb\"><span>@ProductLabel</span><span class=\"material-icons\">chevron_right</span><span>@LocationLabel</span></div><div class=\"creatortitle\">@Title</div></div><button class=\"creatorbutton quiet cancelcreation\" onclick=@Back><span class=\"material-icons\">arrow_back</span>Back</button></div>\r\n\r\n <div class=\"creatorbody\">\r\n\r\n <div class=\"creatorcontrols\">\r\n\r\n <div class=\"creatortabs\">@foreach(var tab in Tabs){<button class=@($\"creatorbutton creatortab {(Tab==tab?\"chosen\":\"\")}\") onclick=@(()=>ChangeTab(tab))>@tab</button>}</div>\r\n\r\n <div class=@($\"creatoroptions {(Tab==\"Voice\"?\"voiceoptions\":\"\")}\")>\r\n\r\n <ReferenceFacePanel style=@(Tab==\"Photos\"?\"display: flex;\":\"display: none;\") Appearance=@Appearance Generated=@AddPhotoFace />\r\n @if(Tab==\"Presets\")\r\n\r\n {\r\n\r\n <div class=\"optionheading\">Choose a starting look</div><div class=\"optiondescription\">Start with a preset, then make it your own.</div><div class=\"presetgrid\">\r\n\r\n @foreach(var preset in AppearanceCatalog.Presets){<button class=@($\"creatorbutton presetchoice {(ChosenPreset==preset.Id?\"chosen\":\"\")}\") onclick=@(()=>ApplyPreset(preset.Id))><div class=\"presetportrait\"><CharacterCreatorPreview [email protected] Portrait=@true /></div><div class=\"presetlabel\">@preset.Label</div></button>}\r\n\r\n </div>\r\n\r\n }\r\n\r\n else if(Tab==\"Face & body\")\r\n\r\n {\r\n\r\n @if(Appearance.BaseShape is not null)\r\n {\r\n <div class=\"optionheading\">Blend your base face</div><div class=\"optiondescription\">Overall sets the whole face. In regional tabs, blend that result with individual faces for that area. Refine the result in Shape; choose textures in Skin.</div>\r\n <div class=\"blendtabs\">@foreach(var region in new[]{\"overall\"}.Concat(FaceRegions.Ids)){<button class=@($\"creatorbutton blendtab blend-tab-{region} {(BlendRegion==region?\"chosen\":\"\")}\") onclick=@(()=>BlendRegion=region)>@FaceRegions.Label(region)</button>}</div>\r\n @if(BlendRegion!=\"overall\")\r\n {\r\n <div class=\"regionstatus\"><span>Mix the live Overall blend with individual faces. Relative weights determine each share.</span><button class=\"creatorbutton quiet reset-region\" onclick=@ResetRegion>Reset region</button></div>\r\n <div class=\"overallblendrow\"><div class=\"shapelabel\"><span>Overall blend</span><span>@($\"{MathF.Round(OverallWeight*100)} influence \u00b7 {MathF.Round(FaceRegions.OverallShare(Appearance.BaseShape,BlendRegion)*100)}% of region\")</span></div><SliderControl class=\"shapeslider region-overall-weight\" Value=@OverallWeight OnValueChanged=@SetOverallWeight Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /><div class=\"optiondescription\">Follows changes made in the Overall tab.</div></div>\r\n }\r\n @for(var i=0;i<7;i++)\r\n {\r\n var index=i;\r\n <div class=\"shaperow\"><div class=\"shapelabel\"><span>Face @(index+1)</span><span>@BlendWeightLabel(index)</span></div><SliderControl class=@($\"shapeslider head-weight-{index+1}\") Value=@HeadWeight(index) OnValueChanged=@((float value)=>SetHeadWeight(index,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n }\r\n @foreach(var face in Appearance.BaseShape.PhotoFaces)\r\n {\r\n <div class=\"photoblendcard\">\r\n <div class=\"photoblendheader\"><span class=\"photoblendname\">@face.Name</span><button class=\"creatorbutton quiet delete-photo-face\" tooltip=\"Delete this custom face from the character and all regions\" onclick=@(()=>FaceRegions.RemovePhoto(Appearance.BaseShape,face))><span class=\"material-icons\">delete_outline</span></button></div>\r\n <div class=\"shapelabel\"><span>Blend influence</span><span>@($\"{MathF.Round(PhotoWeight(face)*100)} influence \u00b7 {MathF.Round(FaceRegions.PhotoEntryShare(Appearance.BaseShape,face,BlendRegion)*100)}% of blend\")</span></div>\r\n <SliderControl class=\"shapeslider photo-face-weight\" Value=@PhotoWeight(face) OnValueChanged=@((float value)=>SetPhotoWeight(face,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false />\r\n @if(BlendRegion==\"overall\")\r\n {\r\n <div class=\"shapelabel\"><span>Photo detail strength</span><span>@MathF.Round(face.Strength*100)%</span></div>\r\n <SliderControl class=\"shapeslider photo-face-strength\" [email protected] OnValueChanged=@((float value)=>{face.Strength=Math.Clamp(value,0,2);}) Min=@(0f) Max=@(2f) Step=@(.01f) ShowTextEntry=@false />\r\n <div class=\"optiondescription\">100% is the fitted shape. Higher values exaggerate its slider changes, within normal Shape limits. This also affects regions using this face.</div>\r\n }\r\n </div>\r\n }\r\n @if(Appearance.BaseShape.TotalInfluence==0){<div class=\"optiondescription\">All influences are zero: using Face 3 as the neutral base.</div>}\r\n <div class=\"optionheading\">Base body</div><div class=\"shapelabel\"><span>@MathF.Round((1-Appearance.BaseShape.MaleBody)*100)% female</span><span>@MathF.Round(Appearance.BaseShape.MaleBody*100)% male</span></div><SliderControl class=\"shapeslider base-body\" [email protected] OnValueChanged=@SetBodyBlend Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false />\r\n @foreach(var variant in new[]{\"male\",\"female\"})\r\n {\r\n <button class=@($\"creatorbutton quiet clothing-prefer-{variant}\") role=\"checkbox\" aria-checked=@(Appearance.BaseShape.ClothingVariant==variant?\"true\":\"false\") onclick=@(()=>ToggleClothingVariant(variant))><span class=\"material-icons\">@(Appearance.BaseShape.ClothingVariant==variant?\"check_box\":\"check_box_outline_blank\")</span>Prefer @variant clothing version</button>\r\n }\r\n <div class=\"optiondescription\">Neither selected: use the body blend. Available versions still fit your body shape.</div>\r\n <div class=\"optiondescription\">Body blend is independent of face and skin. Breast adjustments fade with the female amount; bulge adjustments fade with the male amount.</div>\r\n }\r\n else\r\n {\r\n <div class=\"optionheading\">Native base</div><div class=\"optiondescription\">This saved character uses its original native face and body.</div><button class=\"creatorbutton\" onclick=@EnableBlending>Enable weighted base blending</button>\r\n <div class=\"bodychoices\"><button class=\"creatorbutton\" onclick=@(()=>ChooseBody(\"male\"))>Masculine</button><button class=\"creatorbutton\" onclick=@(()=>ChooseBody(\"female\"))>Feminine</button></div><div class=\"skingrid\">@foreach(var skin in AppearanceCatalog.Skins.Where(x=>x.BodyType==BodyType)){<button class=\"creatorbutton skinchoice\" onclick=@(()=>ChooseSkin(skin))>@FaceLabel(skin)</button>}</div>\r\n }\r\n\r\n <div class=\"optionheading\">Eye color</div><div class=\"optiondescription\">Color the iris while keeping the pupil and whites of the eyes.</div>\r\n <div class=\"colorchoices\"><button class=@($\"creatorbutton colorchoice eyecolorchoice {(!Appearance.CustomEyeColor?\"chosen\":\"\")}\") onclick=@(()=>ChooseEyeColor(null))>Native</button>@foreach(var color in EyeColors){<button class=@($\"creatorbutton colorchoice eyecolorchoice {(Appearance.CustomEyeColor && Appearance.EyeColor==color.Color?\"chosen\":\"\")}\") onclick=@(()=>ChooseEyeColor(color.Color))><div class=\"colorswatch\" style=@($\"background-color: {ColorCss(color.Color)};\")></div><span>@color.Label</span></button>}</div>\r\n <button class=\"creatorbutton quiet eyecustomtoggle\" onclick=@(()=>ChooseEyeColor(Appearance.EyeColor))><span class=\"material-icons\">palette</span>Custom eye color</button>\r\n @if(Appearance.CustomEyeColor){@foreach(var channel in new[]{\"Red\",\"Green\",\"Blue\"}){<div class=\"skinchannel\"><div class=\"shapelabel\">@channel</div><SliderControl class=@($\"shapeslider eye-{channel.ToLowerInvariant()}\") Value=@EyeChannel(channel) OnValueChanged=@((float value)=>SetEyeChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}}\r\n }\r\n\r\n else if(Tab==\"Skin\")\r\n\r\n {\r\n\r\n @if(Appearance.BaseShape is not null){<div class=\"optionheading\">Skin details</div><div class=\"optiondescription\">Choose the native texture set: pigment, pores, freckles and other details. This does not change your face weights or body shape.</div><div class=\"skingrid\">@foreach(var skin in AppearanceCatalog.Skins){<button class=@($\"creatorbutton skinchoice {(Appearance.SkinPath==skin.ResourcePath?\"chosen\":\"\")}\") onclick=@(()=>ChooseSkin(skin))>Skin @skin.Id.Replace(\"human\",\"\")</button>}</div>}\r\n <div class=\"optionheading\">Skin color</div><div class=\"optiondescription\">Choose a complexion independently of face and body. Native restores the selected skin's original color.</div>\r\n\r\n <div class=\"shapecategories\"><button class=@($\"creatorbutton skinpalette {(SkinPalette==\"Human\"?\"chosen\":\"\")}\") onclick=@(()=>SkinPalette=\"Human\")>Human</button><button class=@($\"creatorbutton skinpalette {(SkinPalette==\"Fantasy\"?\"chosen\":\"\")}\") onclick=@(()=>SkinPalette=\"Fantasy\")>Fantasy & custom</button></div>\r\n\r\n <div class=\"colorchoices\">@foreach(var color in SkinPalette==\"Human\"?AppearanceCatalog.HumanSkinColors:AppearanceCatalog.FantasySkinColors){<button class=@($\"creatorbutton colorchoice skincolorchoice skincolor-{color.Id} {(Appearance.SkinColor==color.Color && Appearance.NormalizeSkinTone==color.NormalizeSkinTone?\"chosen\":\"\")}\") onclick=@(()=>ChooseSkinColor(color.Color,color.NormalizeSkinTone)) [email protected]><div class=\"colorswatch\" style=@($\"background-color: {ColorCss(color.Color)};\")></div><span>@color.Label</span></button>}</div>\r\n\r\n @if(SkinPalette==\"Fantasy\")\r\n\r\n {\r\n\r\n <div class=\"fieldlabel colorlabel\">Custom color</div><div class=\"skincolorpreview\"><div class=\"colorswatch\" style=@($\"background-color: {ColorCss(Appearance.SkinColor)};\")></div><span>@($\"RGB {MathF.Round(Appearance.SkinColorR*255)} \u00b7 {MathF.Round(Appearance.SkinColorG*255)} \u00b7 {MathF.Round(Appearance.SkinColorB*255)}\")</span></div>\r\n\r\n @foreach(var channel in new[]{\"Red\",\"Green\",\"Blue\"}){<div class=\"skinchannel\"><div class=\"shapelabel\">@channel</div><SliderControl class=@($\"shapeslider skin-{channel.ToLowerInvariant()}\") Value=@SkinChannel(channel) OnValueChanged=@((float value)=>SetSkinChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}\r\n\r\n }\r\n\r\n <button class=\"creatorbutton quiet resetshape resetskincolor\" onclick=@(()=>ChooseSkinColor(Color.White))><span class=\"material-icons\">restart_alt</span>Reset skin color</button>\r\n\r\n }\r\n\r\n else if(Tab==\"Makeup\")\r\n\r\n {\r\n\r\n <div class=\"optionheading\">Makeup & markings</div><div class=\"optiondescription\">Make it yours. Choose None to leave any makeup layer bare.</div>\r\n\r\n <div class=\"shapecategories\">@foreach(var layer in MakeupCatalog.Layers){<button class=@($\"creatorbutton makeuplayer {(MakeupLayer==layer?\"chosen\":\"\")}\") onclick=@(()=>ChooseMakeupLayer(layer))>@(layer==\"Tattoo\"?\"Markings\":layer)</button>}</div>\r\n\r\n @if(MakeupLayer==\"Tattoo\")\r\n\r\n {\r\n\r\n <div class=\"markinglayers\"><div class=\"markinglist\">@for(var i=0;i<Appearance.Makeup.Markings.Count;i++){var index=i;var mark=Appearance.Makeup.Markings[i];<button class=@($\"creatorbutton markinglayer {(SelectedMarking==i?\"chosen\":\"\")}\") onclick=@(()=>SelectMarking(index))>@($\"{i+1} \u00b7 {MarkingCatalog.Style(mark.Style).Label}\")</button>}</div><button class=\"creatorbutton addmarking\" onclick=@AddMarking tooltip=\"Add an independent marking layer\"><span class=\"material-icons\">add</span>Add marking</button></div>\r\n\r\n @if(CurrentMarking is null)\r\n\r\n {<div class=\"markingempty\"><span class=\"material-icons\">brush</span><div class=\"fieldlabel\">None</div><div class=\"optiondescription\">Add a marking to choose its design, location and ink. Combine up to six.</div></div>}\r\n\r\n else\r\n\r\n {\r\n\r\n <div class=\"markingeditorheading\"><button class=\"creatorbutton quiet inspectmarking\" onclick=@InspectMarking tooltip=\"Focus the preview on this marking\"><span class=\"material-icons\">center_focus_strong</span>@MarkingCatalog.Region(CurrentMarking.Region).Label</button><button class=\"creatorbutton quiet deletemarking\" onclick=@DeleteMarking tooltip=\"Delete this marking layer\"><span class=\"material-icons\">delete_outline</span>Delete layer</button></div>\r\n <div class=\"markingsections\">@foreach(var section in new[]{\"Design\",\"Placement\",\"Ink\"}){<button class=@($\"creatorbutton markingsection {(MarkingSection==section?\"chosen\":\"\")}\") onclick=@(()=>MarkingSection=section)>@section</button>}</div>\r\n\r\n @if(MarkingSection==\"Design\")\r\n\r\n {\r\n\r\n <div class=\"markingdesigns\">@foreach(var style in MarkingCatalog.Styles){<button class=@($\"creatorbutton markingstyle style-{style.Id} {(CurrentMarking.Style==style.Id?\"chosen\":\"\")}\") onclick=@(()=>ChooseMarkingStyle(style.Id))><span class=\"material-icons\">@MarkingSymbol(style.Id)</span><span>@style.Label</span></button>}</div>\r\n\r\n <div class=\"fieldlabel markinglocationlabel\">Location</div><div class=\"markinglocations\">@foreach(var region in MarkingCatalog.Locations(CurrentMarking.Style)){<button class=@($\"creatorbutton markinglocation location-{region.Id} {(CurrentMarking.Region==region.Id?\"chosen\":\"\")}\") onclick=@(()=>ChooseMarkingRegion(region.Id))>@region.Label</button>}</div>\r\n\r\n }\r\n\r\n else if(MarkingSection==\"Placement\")\r\n\r\n {\r\n\r\n <div class=\"optiondescription\">@(MarkingCatalog.Style(CurrentMarking.Style).Band?\"This band wraps around the selected region. Slide it along the body and adjust its size and barb spacing.\":\"Move, turn and resize this marking within the selected region.\")</div>\r\n\r\n <div class=\"markingplacement\">@foreach(var control in MarkingCatalog.Style(CurrentMarking.Style).Band?new[]{\"Vertical\",\"Scale\",\"Spacing\"}:new[]{\"Horizontal\",\"Vertical\",\"Scale\",\"Rotation\"}){<div class=\"skinchannel\"><div class=\"shapelabel\">@(control==\"Vertical\" && MarkingCatalog.Style(CurrentMarking.Style).Band?\"Position along region\":control==\"Scale\"?\"Size\":control==\"Spacing\"?\"Barb spacing\":control)</div><SliderControl class=@($\"shapeslider marking-{control.ToLowerInvariant()}\") Value=@MarkingValue(control) OnValueChanged=@((float value)=>SetMarkingValue(control,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>}</div><button class=\"creatorbutton quiet resetmarkingplacement\" onclick=@ResetMarkingPlacement><span class=\"material-icons\">restart_alt</span>Reset placement</button>\r\n\r\n }\r\n\r\n }\r\n\r\n }\r\n\r\n @if(MakeupLayer!=\"Tattoo\" || CurrentMarking is not null && MarkingSection==\"Ink\")\r\n\r\n {\r\n\r\n <div class=\"colorchoices\">\r\n\r\n @if(MakeupLayer!=\"Tattoo\"){<button class=@($\"creatorbutton colorchoice makeupnone {(!CurrentMakeupActive?\"chosen\":\"\")}\") onclick=@ChooseNoMakeup><span class=\"material-icons\">block</span><span>None</span></button>}\r\n\r\n @foreach(var color in MakeupCatalog.Colors(MakeupLayer)){<button class=@($\"creatorbutton colorchoice makeupcolor makeup-{color.Id} {(CurrentMakeupActive && CurrentMakeup.Color==color.Color?\"chosen\":\"\")}\") onclick=@(()=>ChooseMakeupColor(color.Color)) [email protected]><div class=\"colorswatch\" style=@($\"background-color: {ColorCss(color.Color)};\")></div><span>@color.Label</span></button>}\r\n\r\n </div>\r\n\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>@(MakeupLayer==\"Eyeliner\"?\"Upper eyelid strength\":\"Strength\")</span><span class=\"shapevalue\">@(MathF.Round(CurrentMakeup.Amount*100))%</span></div><SliderControl class=\"shapeslider makeupamount\" [email protected] OnValueChanged=@SetMakeupAmount Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n @if(MakeupLayer==\"Eyeliner\")\r\n {\r\n <div class=\"optiondescription\">Upper and lower liner share one color. Wings follow the upper liner strength; inner-corner liner is optional.</div>\r\n @foreach(var part in new[]{\"Lower eyelid\",\"Inner corner\"})\r\n {\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>@part strength</span><span class=\"shapevalue\">@(MathF.Round(EyelinerStrength(part)*100))%</span></div><SliderControl class=@($\"shapeslider eyeliner-{(part==\"Lower eyelid\"?\"lower\":\"inner\")}\") Value=@EyelinerStrength(part) OnValueChanged=@((float value)=>SetEyelinerStrength(part,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n }\r\n <div class=\"fieldlabel colorlabel\">Wing</div>\r\n <div class=\"choicegrid\">@foreach(var wing in new[]{\"none\",\"wing\"}){<button class=@($\"creatorbutton eyelinerwing {((wing==\"none\")== (Appearance.Makeup.EyelinerWing==\"none\")?\"chosen\":\"\")}\") onclick=@(()=>ChooseEyelinerWing(wing))>@(wing==\"none\"?\"No wing\":\"Wing\")</button>}</div>\r\n @if(Appearance.Makeup.EyelinerWing!=\"none\")\r\n {\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>Wing strength</span><span class=\"shapevalue\">@(MathF.Round(Appearance.Makeup.EyelinerWingStrength*100))%</span></div><SliderControl class=\"shapeslider eyeliner-wing\" [email protected] OnValueChanged=@((float value)=>SetEyelinerStrength(\"Wing\",value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>Wing tip horizontal</span><span class=\"shapevalue\">@(MathF.Round(Appearance.Makeup.WingTipHorizontal*100))%</span></div><SliderControl class=\"shapeslider eyeliner-wing-horizontal\" [email protected] OnValueChanged=@((float value)=>SetWingTip(false,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>Wing tip vertical</span><span class=\"shapevalue\">@(Appearance.Makeup.WingTipVertical==0?\"Level\":$\"{MathF.Round(MathF.Abs(Appearance.Makeup.WingTipVertical)*100)}% {(Appearance.Makeup.WingTipVertical>0?\"up\":\"down\")}\")</span></div><SliderControl class=\"shapeslider eyeliner-wing-vertical\" [email protected] OnValueChanged=@((float value)=>SetWingTip(true,value)) Min=@(-1f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>Wing thickness</span><span class=\"shapevalue\">@(MathF.Round(Appearance.Makeup.EyelinerWingThickness*100))%</span></div><SliderControl class=\"shapeslider eyeliner-wing-thickness\" [email protected] OnValueChanged=@SetWingThickness Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n }\r\n }\r\n\r\n <button class=\"creatorbutton quiet makeupcustomtoggle\" onclick=@(()=>CustomMakeup=!CustomMakeup)><span class=\"material-icons\">palette</span>@(CustomMakeup?\"Hide custom color\":\"Custom color\")</button>\r\n\r\n @if(CustomMakeup){<div class=\"makeupcustomgrid\">@foreach(var channel in new[]{\"Red\",\"Green\",\"Blue\"}){<div class=\"skinchannel\"><div class=\"shapelabel\">@channel</div><SliderControl class=@($\"shapeslider makeup-{channel.ToLowerInvariant()}\") Value=@MakeupChannel(channel) OnValueChanged=@((float value)=>SetMakeupChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}</div>}\r\n\r\n }\r\n\r\n <div class=\"makeupresets\"><button class=\"creatorbutton quiet clearmakeup\" onclick=@ClearMakeup>Clear all makeup & markings</button></div>\r\n\r\n }\r\n\r\n else if(Tab==\"Clothing\")\r\n {\r\n <div class=\"optionheading\">Your wardrobe</div><div class=\"optiondescription\">Click an item to wear it, click again to remove it. Outfits replace tops and bottoms; jackets and accessories can be layered.</div>\r\n <button class=\"creatorbutton quiet browsecloud\" onclick=@(()=>CloudWardrobeOpen=!CloudWardrobeOpen)>@(CloudWardrobeOpen?\"Close cloud browser\":\"Browse cloud clothing & purchased cosmetics\")</button>\r\n @if(CloudWardrobeOpen){<CloudClothingBrowser Selected=@ChooseCloudClothing Appearance=@Appearance />}\r\n <TextEntry class=\"wardrobesearch\" Value=@WardrobeSearch OnTextEdited=@((string value)=>{WardrobeSearch=value;}) Placeholder=\"Search clothing\" />\r\n <div class=\"shapecategories\">@foreach(var category in WardrobeCatalog.Categories){<button class=@($\"creatorbutton {(WardrobeCategory==category?\"chosen\":\"\")}\") onclick=@(()=>WardrobeCategory=category)>@category</button>}</div>\r\n <div class=\"fieldlabel\">Wearing @WardrobeCatalog.Paths(Appearance).Count() items</div><div class=\"choicegrid\">@foreach(var path in WardrobeCatalog.Paths(Appearance).ToArray()){var worn=ClothingResources.Available.FirstOrDefault(x=>x.Path==path);@if(worn is not null){<button class=\"creatorbutton chosen\" onclick=@(()=>ChooseClothing(worn))>@($\"{worn.Name} \u00d7\")</button>}}</div>\r\n <div class=\"fieldlabel\">@($\"{WardrobeCategory} \u00b7 {VisibleWardrobe.Count()} choices\")</div><div class=\"choicegrid thumbnailgrid\">@foreach(var item in VisibleWardrobe){<button class=@($\"creatorbutton wardrobechoice {((WardrobeCatalog.Paths(Appearance).Contains(item.Path)||Appearance.HairPath==item.Path||Appearance.FacialHairPath==item.Path)?\"chosen\":\"\")}\") onclick=@(()=>ChooseClothing(item))><div class=\"thumbnailframe\">@if(ClothingThumbnails.For(item.Path) is { Length: > 0 } icon){<div class=\"itemthumbnail\" style=@($\"background-image: url('{icon}');\")></div>}else{<span class=\"material-icons thumbnailplaceholder\">block</span>}</div><span class=\"itemlabel\">@item.Name</span></button>}</div>\r\n @if(!VisibleWardrobe.Any()){<div class=\"optiondescription\">No clothing matches your search in this category.</div>}\r\n <div class=\"choicegrid\"><button class=\"creatorbutton quiet\" onclick=@ClearClothing>Remove clothing</button><button class=\"creatorbutton quiet\" onclick=@ResetClothing>Default outfit</button></div>\r\n }\r\n else if(Tab==\"Hair\")\r\n\r\n {\r\n\r\n <div class=\"optionheading\">Hair</div><div class=\"choicegrid thumbnailgrid\">@foreach(var hair in AvailableHair){<button class=@($\"creatorbutton hairchoice {(Appearance.HairPath==hair.ResourcePath?\"chosen\":\"\")}\") onclick=@(()=>ChooseHair(hair))><div class=\"thumbnailframe\">@if(ClothingThumbnails.For(hair.ResourcePath) is { Length: > 0 } icon){<div class=\"itemthumbnail\" style=@($\"background-image: url('{icon}');\")></div>}else{<span class=\"material-icons thumbnailplaceholder\">block</span>}</div><span class=\"itemlabel\">@hair.Label</span></button>}</div><div class=\"fieldlabel colorlabel\">Hair color</div><div class=\"colorchoices\">@foreach(var color in AppearanceCatalog.Colors){<button class=@($\"creatorbutton colorchoice {(SameColor(color.Color)?\"chosen\":\"\")}\") onclick=@(()=>ChooseColor(color)) [email protected]><div class=\"colorswatch\" style=@($\"background-color: {ColorCss(color.Color)};\")></div><span>@color.Label</span></button>}</div><div class=\"fieldlabel colorlabel\">Facial hair</div><div class=\"choicegrid thumbnailgrid\">@foreach(var facial in AvailableFacialHair.Where(x=>x.BodyType==\"\"||x.BodyType==BodyType)){<button class=@($\"creatorbutton facialchoice {(Appearance.FacialHairPath==facial.ResourcePath?\"chosen\":\"\")}\") onclick=@(()=>ChooseFacial(facial))><div class=\"thumbnailframe\">@if(ClothingThumbnails.For(facial.ResourcePath) is { Length: > 0 } icon){<div class=\"itemthumbnail\" style=@($\"background-image: url('{icon}');\")></div>}else{<span class=\"material-icons thumbnailplaceholder\">block</span>}</div><span class=\"itemlabel\">@facial.Label</span></button>}</div>\r\n\r\n }\r\n\r\n else if(Tab==\"Shape\")\r\n\r\n {\r\n\r\n <div class=\"optionheading\">Refine your look</div><div class=\"optiondescription\">Neutral is 50%; ear taper and belly start at 0%.</div>\r\n\r\n <div class=\"shapecategories\">@foreach(var category in AppearanceCatalog.Sliders.Select(x=>x.Group).Distinct()){<button class=@($\"creatorbutton shapecategory {(ShapeCategory==category?\"chosen\":\"\")}\") onclick=@(()=>ChangeShapeCategory(category))>@category</button>}</div>\r\n\r\n @if(ShapeCategory==\"Body\" && Appearance.BaseShape is not null)\r\n {\r\n @foreach(var breast in new[]{true,false}){<div class=\"shaperow\"><div class=\"shapelabel\"><span>@(breast?\"Breast size\":\"Bulge size\")</span><span>@MathF.Round((breast?1-Appearance.BaseShape.MaleBody:Appearance.BaseShape.MaleBody)*100)% effect</span><button class=\"creatorbutton quiet\" onclick=@(()=>SetBodyFeature(breast,.5f))>Reset</button></div><SliderControl class=@($\"shapeslider {(breast?\"breast\":\"bulge\")}-size\") Value=@(breast?Appearance.BaseShape.BreastSize:Appearance.BaseShape.BulgeSize) OnValueChanged=@((float value)=>SetBodyFeature(breast,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>}\r\n }\r\n <div class=\"shapegrid\">@foreach(var slider in AppearanceCatalog.Sliders.Where(x=>x.Group==ShapeCategory && (Appearance.BaseShape is null || x.Id!=\"bodyfeature\")))\r\n\r\n {\r\n\r\n <div class=@($\"shaperow shape-{slider.Id}\")><div class=\"shapelabel\"><span>@slider.LabelFor(Appearance)</span><div class=\"shapeactions\"><span class=\"shapevalue\">@(MathF.Round(slider.Get(Appearance)*100))%</span><button class=\"creatorbutton quiet resetslider\" onclick=@(()=>SetShape(slider,slider.Neutral)) tooltip=@($\"Reset {slider.LabelFor(Appearance).ToLowerInvariant()} to {MathF.Round(slider.Neutral*100)}%\")><span class=\"material-icons\">restart_alt</span></button></div></div><SliderControl class=\"shapeslider\" [email protected](Appearance) OnValueChanged=@((float value)=>SetShape(slider,value)) [email protected] [email protected] Step=@(0.01f) ShowTextEntry=@false />@if(slider.Id==\"columella\"){<div class=\"fitnesshint\">Width between the nostrils</div>}@if(slider.Id==\"philtrum\"){<div class=\"fitnesshint\">Width between nose and upper lip</div>}@if(slider.Id==\"eartaper\"){<div class=\"fitnesshint\">Normal \u00b7 Pointed</div>}@if(slider.Id==\"fitness\"){<div class=\"fitnesshint\">Soft \u00b7 Neutral \u00b7 Muscular</div>}@if(slider.Id==\"nostrils\"){<div class=\"fitnesshint\">Width of the nostril wings</div>}@if(slider.Id==\"nostrilfullness\"){<div class=\"fitnesshint\">Smaller \u00b7 Neutral \u00b7 Fuller wings</div>}@if(slider.Id==\"noseheight\" || slider.Id==\"chinheight\"){<div class=\"fitnesshint\">Lower \u00b7 Neutral \u00b7 Higher</div>}</div>\r\n\r\n }</div>\r\n\r\n <button class=\"creatorbutton quiet resetshape\" onclick=@ResetShapes>Reset body & face</button>\r\n\r\n }\r\n\r\n @if(ExtraTabs.Contains(Tab) && ExtraContent is not null){@ExtraContent(this)}\r\n\r\n </div>\r\n\r\n <div class=\"creatorutility\"><button class=\"creatorbutton quiet\" onclick=@Randomize><span class=\"material-icons\">shuffle</span>Randomize</button><button class=\"creatorbutton quiet\" onclick=@Reset><span class=\"material-icons\">restart_alt</span>Reset appearance</button></div>\r\n\r\n </div>\r\n\r\n <div class=\"creatorpresentation\"><div class=\"previewheading\"><div class=\"previewname\">@(string.IsNullOrWhiteSpace(CharacterName)?\"Your character\":CharacterName.Trim())</div><div class=\"previewcaption\">@(PreviewPan.Length>0.01f || MathF.Abs(PreviewPitch)>0.01f?\"Free inspection\":Zoom>CharacterCreatorPreview.FaceZoom+0.001f?\"Face detail\":Portrait?\"Face preview\":\"Character preview\")</div></div><CharacterCreatorPreview @ref=Preview class=\"mainpreview\" Hero=@true Appearance=@Appearance Portrait=@Portrait Zoom=@Zoom Yaw=@Yaw Pitch=@PreviewPitch PitchChanged=@SetPitch PanOffset=@PreviewPan PanChanged=@SetPan Interactive=@(!LoadingCatalog) OrbitChanged=@SetYaw ZoomChanged=@SetZoom ShowClothing=@ShowClothing /><div class=\"previewcontrols\"><div class=\"previewviewrow\"><button class=\"creatorbutton quiet rotateleft\" onclick=@(()=>Rotate(-30)) tooltip=\"Turn left\"><span class=\"material-icons\">rotate_left</span></button><button class=@($\"creatorbutton bodyview {(Zoom==0?\"chosen\":\"\")}\") onclick=@(()=>SetView(0))>Full body</button><button class=@($\"creatorbutton previewzoom {(MathF.Abs(Zoom-CharacterCreatorPreview.FaceZoom)<0.001f?\"chosen\":\"\")}\") onclick=@(()=>SetView(CharacterCreatorPreview.FaceZoom))>Face view</button><button class=\"creatorbutton quiet rotateright\" onclick=@(()=>Rotate(30)) tooltip=\"Turn right\"><span class=\"material-icons\">rotate_right</span></button></div><button class=\"creatorbutton quiet clothingtoggle\" onclick=@ToggleClothing><span class=\"material-icons\">@(ShowClothing?\"visibility_off\":\"visibility\")</span>@(ShowClothing?\"Hide clothing\":\"Show clothing\")</button></div><div class=\"previewzoomrow\"><span>Zoom</span><SliderControl class=\"customzoom\" Value=@Zoom OnValueChanged=@SetZoom Min=@(0f) [email protected] Step=@(0.005f) ShowTextEntry=@false /></div><div class=\"previewrecenter\"><button class=\"creatorbutton quiet recenterpreview\" onclick=@RecenterPreview><span class=\"material-icons\">center_focus_strong</span>Recenter preview</button></div><div class=\"previewhint\">Left drag: turn \u00b7 Right: tilt \u00b7 Middle: pan \u00b7 Scroll: zoom</div></div>\r\n\r\n </div>\r\n\r\n <div class=\"creatorfooter\"><div class=\"creatoridentity\"><div class=\"identityfields\"><div class=\"namefield\"><div class=\"fieldlabel\">Character name</div><TextEntry class=\"charactername\" Value=@CharacterName OnTextEdited=@SetCharacterName MaxLength=@(40) Placeholder=\"Your character's name\" /></div>@FooterContent</div></div><div class=\"creatorfeedback\">@(Feedback??(LoadingCatalog?\"Loading character options\u2026\":Preview?.Error)??(\"\"))</div><button class=\"creatorbutton primary startplaythrough\" disabled=@Disabled onclick=@Start><span class=\"material-icons\">play_arrow</span>@(SaveLabel)</button></div>\r\n\r\n </div>\r\n\r\n }\r\n\r\n</root>\r\n\r\n@code {\r\n\t/// <summary>Consumer-specific pages; built-in categories remain owned by Character Kit.</summary>\r\n\tpublic IReadOnlyList<string> ExtraTabs { get; set; } = Array.Empty<string>();\r\n\tpublic IReadOnlyList<string> HiddenTabs { get; set; } = Array.Empty<string>();\r\n\tpublic RenderFragment<CharacterCreatorPanel> ExtraContent { get; set; }\r\n\tpublic RenderFragment FooterContent { get; set; }\r\n\tpublic Action StopPreview { get; set; }\r\n\tpublic bool Disabled { get; set; }\r\n\tpublic string ProductLabel { get; set; } = \"Character Editor\";\r\n\tpublic string LocationLabel { get; set; } = \"Studio\";\r\n\tpublic string Title { get; set; } = \"Create your character\";\r\n\tpublic CharacterAppearanceData EditableAppearance => Appearance;\r\n\tpublic SkinnedModelRenderer PreviewRenderer => Preview?.Body;\r\n\tpublic string SelectedTab => Tab;\r\n\tpublic string SaveLabel { get; set; } = \"Save character\";\r\n\tpublic Func<string, CharacterAppearanceData, string> Saved { get; set; }\r\n\tpublic Action Closed { get; set; }\r\n\tpublic CharacterPreset InitialPreset { get; set; }\r\n\r\n\tCharacterPreset loadedPreset;\r\n\tpublic CharacterAppearanceData DraftAppearance => Appearance.DeepCopy();\r\n\tpublic string DraftCharacterName => CharacterName;\r\n\r\n\tpublic void LoadDraft(string name, CharacterAppearanceData data)\r\n\t{\r\n\t\tCharacterName = name;\r\n\t\tAppearance = data.DeepCopy();\r\n\t\tChosenPreset = null;\r\n\t\tTab = \"Face & body\";\r\n\t}\r\n\r\n\tpublic bool AppearanceReady => CatalogReady && Preview?.Loading != true && CharacterAppearance.ValidateForApply(Appearance)is null;\r\n\r\n\tIReadOnlyList<string> Tabs => new[]\r\n\t{\r\n\t\t\"Presets\",\r\n\t\t\"Skin\",\r\n\t\t\"Face & body\",\r\n\t\t\"Photos\",\r\n\t\t\"Hair\",\r\n\t\t\"Clothing\",\r\n\t\t\"Shape\",\r\n\t\t\"Makeup\"\r\n\t}.Where(x => !HiddenTabs.Contains(x)).Concat(ExtraTabs).Distinct().ToArray();\r\n\r\n\tbool CloudWardrobeOpen;\r\n\tstring WardrobeSearch = \"\", WardrobeCategory = \"Tops\";\r\n\tIEnumerable<WardrobeItem> VisibleWardrobe => ClothingResources.Available.Where(x => x.Category == WardrobeCategory && (string.IsNullOrWhiteSpace(WardrobeSearch) || x.Name.Contains(WardrobeSearch, StringComparison.OrdinalIgnoreCase))).OrderBy(x => x.Name);\r\n\r\n\tvoid ChooseCloudClothing(WardrobeItem item)\r\n\t{\r\n\t\tWardrobeCategory = item.Category;\r\n\t\tWardrobeSearch = \"\";\r\n\t\tChooseClothing(item);\r\n\t\tAppearance.Version = 2;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tvoid ChooseClothing(WardrobeItem item)\r\n\t{\r\n\t\tvar clothing = ClothingResources.Get(item.Path);\r\n\t\tif (clothing is not null && ClothingResources.IsHair(clothing))\r\n\t\t\tAppearance.HairPath = Appearance.HairPath == item.Path || ClothingResources.Get(Appearance.HairPath) == clothing ? \"\" : item.Path;\r\n\t\telse if (clothing is not null && ClothingResources.IsFacialHair(clothing))\r\n\t\t\tAppearance.FacialHairPath = Appearance.FacialHairPath == item.Path || ClothingResources.Get(Appearance.FacialHairPath) == clothing ? \"\" : item.Path;\r\n\t\telse\r\n\t\t\tWardrobeCatalog.Toggle(Appearance, item);\r\n\t\tAppearance.Version = 2;\r\n\t\tShowClothing = true;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ClearClothing()\r\n\t{\r\n\t\tAppearance.ClothingPaths = Array.Empty<string>();\r\n\t\tAppearance.Version = 2;\r\n\t\tShowClothing = true;\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid ResetClothing()\r\n\t{\r\n\t\tAppearance.ClothingPaths = WardrobeCatalog.DefaultPaths.ToArray();\r\n\t\tAppearance.Version = 2;\r\n\t\tShowClothing = true;\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tCharacterAppearanceData Appearance = AppearanceCatalog.CreateDefault();\r\n\tstring CharacterName = \"Rowan\", PlaythroughName = \"A new adventure\", Tab = \"Presets\", ShapeCategory = \"Body\", ChosenPreset = \"traveller\", Feedback;\r\n\tstring BlendRegion = \"overall\";\r\n\tbool CatalogReady, LoadingCatalog;\r\n\tstring SkinPalette = \"Human\";\r\n\tstring MakeupLayer = \"Lipstick\";\r\n\tbool CustomMakeup;\r\n\tint SelectedMarking;\r\n\tstring MarkingSection = \"Design\";\r\n\tCharacterMarkingData CurrentMarking => Appearance.Makeup.Markings.Count == 0 ? null : Appearance.Makeup.Markings[Math.Clamp(SelectedMarking, 0, Appearance.Makeup.Markings.Count - 1)];\r\n\r\n\tMakeupLayerData CurrentMakeup => MakeupLayer == \"Tattoo\" ? CurrentMarking?.Ink ?? Appearance.Makeup.Tattoo : MakeupCatalog.Layer(Appearance.Makeup, MakeupLayer);\r\n\r\n\tvoid ChooseMakeupLayer(string layer)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tMakeupLayer = layer;\r\n\t\tif (layer == \"Tattoo\")\r\n\t\t\tAppearance.Makeup.EditMarkings();\r\n\t}\r\n\r\n\tvoid SelectMarking(int index)\r\n\t{\r\n\t\tif (LoadingCatalog || index < 0 || index >= Appearance.Makeup.Markings.Count)\r\n\t\t\treturn;\r\n\t\tSelectedMarking = index;\r\n\t}\r\n\r\n\tvoid AddMarking()\r\n\t{\r\n\t\tif (LoadingCatalog || Appearance.Makeup.Markings.Count >= MarkingCatalog.Maximum)\r\n\t\t{\r\n\t\t\tFeedback = \"You can combine up to six marking layers.\";\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar marking = new CharacterMarkingData();\r\n\t\tMarkingCatalog.ResetPosition(marking);\r\n\t\tAppearance.Makeup.Markings.Add(marking);\r\n\t\tSelectedMarking = Appearance.Makeup.Markings.Count - 1;\r\n\t\tMarkingSection = \"Design\";\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid DeleteMarking()\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null)\r\n\t\t\treturn;\r\n\t\tAppearance.Makeup.Markings.Remove(CurrentMarking);\r\n\t\tSelectedMarking = Math.Clamp(SelectedMarking, 0, Math.Max(0, Appearance.Makeup.Markings.Count - 1));\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tstring MarkingSymbol(string style) => style switch\r\n\t{\r\n\t\t\"crescent\" => \"brightness_2\",\r\n\t\t\"teardrop\" => \"water_drop\",\r\n\t\t\"star\" => \"star_outline\",\r\n\t\t\"rune\" => \"gesture\",\r\n\t\t\"barbed-band\" => \"link\",\r\n\t\t_ => \"block\"\r\n\t};\r\n\tvoid ChooseMarkingStyle(string style)\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null)\r\n\t\t\treturn;\r\n\t\tMarkingCatalog.ChangeStyle(CurrentMarking, style);\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid ChooseMarkingRegion(string region)\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null)\r\n\t\t\treturn;\r\n\t\tMarkingCatalog.ChangeRegion(CurrentMarking, region);\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tfloat MarkingValue(string control) => control switch\r\n\t{\r\n\t\t\"Horizontal\" => CurrentMarking.Horizontal,\r\n\t\t\"Vertical\" => CurrentMarking.Vertical,\r\n\t\t\"Scale\" => CurrentMarking.Scale,\r\n\t\t\"Spacing\" => CurrentMarking.Spacing,\r\n\t\t_ => CurrentMarking.Rotation\r\n\t};\r\n\tvoid SetMarkingValue(string control, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tvalue = Math.Clamp(value, 0, 1);\r\n\t\tif (MarkingCatalog.Style(CurrentMarking.Style).Band && control is not \"Vertical\" and not \"Scale\" and not \"Spacing\")\r\n\t\t\treturn;\r\n\t\tif (control == \"Horizontal\")\r\n\t\t\tCurrentMarking.Horizontal = value;\r\n\t\telse if (control == \"Vertical\")\r\n\t\t\tCurrentMarking.Vertical = value;\r\n\t\telse if (control == \"Scale\")\r\n\t\t\tCurrentMarking.Scale = value;\r\n\t\telse if (control == \"Spacing\")\r\n\t\t\tCurrentMarking.Spacing = value;\r\n\t\telse if (control == \"Rotation\")\r\n\t\t\tCurrentMarking.Rotation = value;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid ResetMarkingPlacement()\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null)\r\n\t\t\treturn;\r\n\t\tMarkingCatalog.ResetPosition(CurrentMarking);\r\n\t\tCurrentMarking.Scale = .5f;\r\n\t\tCurrentMarking.Rotation = .5f;\r\n\t\tCurrentMarking.Spacing = .5f;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid InspectMarking()\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null)\r\n\t\t\treturn;\r\n\t\tvar region = MarkingCatalog.Region(CurrentMarking.Region);\r\n\t\tvar center = MarkingCatalog.Focus(CurrentMarking);\r\n\t\tvar outward = MarkingCatalog.Outward(CurrentMarking);\r\n\t\tYaw = -MathF.Atan2(outward.y, outward.x) * 180 / MathF.PI;\r\n\t\tSetZoom(region.Head ? 1.55f : 1.15f);\r\n\t\tvar rotated = Rotation.FromYaw(Yaw) * center;\r\n\t\tvar targetHeight = 62 + 1.5f * MathF.Max(0, Zoom - 1);\r\n\t\tPreviewPan = new(rotated.x, rotated.y, rotated.z - targetHeight);\r\n\t\tPreviewPitch = 0;\r\n\t}\r\n\r\n\tvoid ChangedCosmetics()\r\n\t{\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ChooseNoMakeup()\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tCurrentMakeup.Amount = 0;\r\n\t\tif (MakeupLayer == \"Eyeliner\") MakeupCatalog.Reset(Appearance.Makeup, \"Eyeliner\");\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid ChooseMakeupColor(Color color)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tCurrentMakeup.Color = color;\r\n\t\tif (!CurrentMakeupActive)\r\n\t\t\tCurrentMakeup.Amount = MakeupLayer == \"Eyeliner\" ? 1f : .65f;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid SetMakeupAmount(float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tCurrentMakeup.Amount = Math.Clamp(value, 0, 1);\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tbool CurrentMakeupActive => MakeupLayer == \"Eyeliner\" ? Appearance.Makeup.EyelinerActive : CurrentMakeup.Amount > 0;\r\n\r\n\tfloat EyelinerStrength(string part) => part == \"Lower eyelid\" ? Appearance.Makeup.EyelinerLowerStrength : part == \"Wing\" ? Appearance.Makeup.EyelinerWingStrength : Appearance.Makeup.EyelinerInnerStrength;\r\n\tvoid SetEyelinerStrength(string part, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value)) return;\r\n\t\tvalue = Math.Clamp(value, 0, 1);\r\n\t\tif (part == \"Lower eyelid\") Appearance.Makeup.EyelinerLowerStrength = value;\r\n\t\telse if (part == \"Wing\") Appearance.Makeup.EyelinerWingStrength = value;\r\n\t\telse Appearance.Makeup.EyelinerInnerStrength = value;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid PrepareAdjustableWing()\r\n\t{\r\n\t\tvar makeup = Appearance.Makeup;\r\n\t\tmakeup.EyelinerWingHorizontal = makeup.WingTipHorizontal;\r\n\t\tmakeup.EyelinerWingVertical = makeup.WingTipVertical;\r\n\t\tmakeup.EyelinerWing = \"wing\";\r\n\t}\r\n\r\n\tvoid SetWingTip(bool vertical, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value)) return;\r\n\t\tPrepareAdjustableWing();\r\n\t\tif (vertical) Appearance.Makeup.EyelinerWingVertical = Math.Clamp(value, -1, 1);\r\n\t\telse Appearance.Makeup.EyelinerWingHorizontal = Math.Clamp(value, 0, 1);\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid SetWingThickness(float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value)) return;\r\n\t\tPrepareAdjustableWing();\r\n\t\tAppearance.Makeup.EyelinerWingThickness = Math.Clamp(value, 0, 1);\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid ChooseEyelinerWing(string wing)\r\n\t{\r\n\t\tif (LoadingCatalog) return;\r\n\t\tif (wing == \"wing\") PrepareAdjustableWing();\r\n\t\telse Appearance.Makeup.EyelinerWing = \"none\";\r\n\t\tif (wing != \"none\" && CurrentMakeup.Amount == 0) CurrentMakeup.Amount = 1;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tfloat MakeupChannel(string channel) => channel == \"Red\" ? CurrentMakeup.Red : channel == \"Green\" ? CurrentMakeup.Green : CurrentMakeup.Blue;\r\n\tvoid SetMakeupChannel(string channel, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tvalue = Math.Clamp(value, 0, 1);\r\n\t\tif (channel == \"Red\")\r\n\t\t\tCurrentMakeup.Red = value;\r\n\t\telse if (channel == \"Green\")\r\n\t\t\tCurrentMakeup.Green = value;\r\n\t\telse\r\n\t\t\tCurrentMakeup.Blue = value;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid ClearMakeup()\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.Makeup = new();\r\n\t\tSelectedMarking = 0;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tbool Portrait = true, ShowClothing = true;\r\n\tfloat Yaw, Zoom = CharacterCreatorPreview.FaceZoom;\r\n\tCharacterCreatorPreview Preview;\r\n\tVector3 PreviewPan;\r\n\tfloat PreviewPitch;\r\n\tstring BodyType => AppearanceCatalog.IsFemale(Appearance) ? \"female\" : \"male\";\r\n\r\n\tIEnumerable<AppearanceOption> AvailableHair => AppearanceCatalog.Hair.Concat(ClothingResources.Available.Where(x=>x.Category==\"Hair\").Select(x=>new AppearanceOption(x.Path,x.Name,x.Path))).DistinctBy(x=>x.ResourcePath);\r\n IEnumerable<AppearanceOption> AvailableFacialHair => AppearanceCatalog.FacialHair.Concat(ClothingResources.Available.Where(x=>x.Category==\"Facial hair\").Select(x=>new AppearanceOption(x.Path,x.Name,x.Path))).DistinctBy(x=>x.ResourcePath);\r\n string HairLabel => AppearanceCatalog.Hair.FirstOrDefault(x => x.ResourcePath == Appearance.HairPath)?.Label ?? \"Hair\";\r\n\r\n\tstring FaceLabel(AppearanceOption option) => option.Label.Replace(\" \u00b7 male\", \"\").Replace(\" \u00b7 female\", \"\");\r\n\tstring ColorCss(Color color) => $\"rgb({(int)(color.r * 255)},{(int)(color.g * 255)},{(int)(color.b * 255)})\";\r\n\tvoid SetCharacterName(string value)\r\n\t{\r\n\t\tCharacterName = value;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid SetPlaythroughName(string value)\r\n\t{\r\n\t\tPlaythroughName = value;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tbool SameColor(Color color) => MathF.Abs(Appearance.HairColorR - color.r) < 0.001f && MathF.Abs(Appearance.HairColorG - color.g) < 0.001f && MathF.Abs(Appearance.HairColorB - color.b) < 0.001f;\r\n\tvoid ChangeTab(string tab)\r\n\t{\r\n\t\tif (tab != Tab)\n\t\t\tStopVoicePreview();\r\n\t\tTab = tab;\r\n\t\tif (tab == \"Clothing\")\r\n\t\t\tSetZoom(0);\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid StopVoicePreview()\r\n\t{\r\n\t\tStopPreview?.Invoke();\r\n\t}\r\n\r\n\tvoid ChangeShapeCategory(string category)\r\n\t{\r\n\t\tShapeCategory = category;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ApplyPreset(string id)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tvar voice = Appearance.VoiceId;\r\n\t\tbool unified = Appearance.BaseShape is not null;\r\n\t\tAppearance = AppearanceCatalog.ApplyPreset(id);\r\n\t\tif (unified)\r\n\t\t\tUnifiedHuman.Enable(Appearance);\r\n\t\tAppearance.VoiceId = voice;\r\n\t\tChosenPreset = id;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid EnableBlending()\r\n\t{\r\n\t\tUnifiedHuman.Enable(Appearance);\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid AddPhotoFace(PhotoFace face)\r\n\t{\r\n\t\tif (!face.Valid)\r\n\t\t\treturn;\r\n\t\tUnifiedHuman.Enable(Appearance);\r\n\t\tif (Appearance.BaseShape.PhotoFaces.Count >= 4)\r\n\t\t\treturn;\r\n\t\tArray.Fill(Appearance.BaseShape.HeadWeights, 0f);\r\n\t\tforeach (var old in Appearance.BaseShape.PhotoFaces)\r\n\t\t\told.Influence = 0;\r\n\t\tAppearance.BaseShape.PhotoFaces.Add(face);\r\n\t\tChosenPreset = null;\r\n\t\tTab = \"Face & body\";\r\n\t\tBlendRegion = \"overall\";\r\n\t\tSetView(CharacterCreatorPreview.FaceZoom);\r\n\t\tFeedback = \"Custom base added. Blend it here or refine it in Shape, then save your character.\";\r\n\t}\r\n\r\n\tFaceRegionBlend Region(bool create = false)\r\n\t{\r\n\t\tif (BlendRegion == \"overall\")\r\n\t\t\treturn null;\r\n\t\tif (!Appearance.BaseShape.Regions.TryGetValue(BlendRegion, out var value) && create)\r\n\t\t{\r\n\t\t\tvalue = new()\r\n\t\t\t{\r\n\t\t\t\tOverallInfluence = .5f\r\n\t\t\t};\r\n\t\t\tAppearance.BaseShape.Regions.Add(BlendRegion, value);\r\n\t\t}\r\n\r\n\t\treturn value;\r\n\t}\r\n\r\n\tfloat OverallWeight => Region()?.OverallInfluence ?? .5f;\r\n\r\n\tvoid SetOverallWeight(float value)\r\n\t{\r\n\t\tif (!float.IsFinite(value) || BlendRegion == \"overall\")\r\n\t\t\treturn;\r\n\t\tRegion(true).OverallInfluence = Math.Clamp(value, 0, 1);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tfloat HeadWeight(int index) => BlendRegion == \"overall\" ? Appearance.BaseShape.HeadWeights[index] : Region()?.HeadWeights[index] ?? 0;\r\n\tfloat PhotoWeight(PhotoFace face) => BlendRegion == \"overall\" ? face.Influence : Region()?.PhotoWeights.GetValueOrDefault(face.Id) ?? 0;\r\n\tstring BlendWeightLabel(int index)\r\n\t{\r\n\t\tfloat share = FaceRegions.NativeShare(Appearance.BaseShape, index, BlendRegion);\r\n\t\treturn $\"{MathF.Round(HeadWeight(index) * 100)} influence \u00b7 {MathF.Round(share * 100)}% of blend\";\r\n\t}\r\n\r\n\tvoid SetHeadWeight(int index, float value)\r\n\t{\r\n\t\tif (Appearance.BaseShape is null || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\t(BlendRegion == \"overall\" ? Appearance.BaseShape.HeadWeights : Region(true).HeadWeights)[index] = Math.Clamp(value, 0, 1);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid SetPhotoWeight(PhotoFace face, float value)\r\n\t{\r\n\t\tif (!float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tif (BlendRegion == \"overall\")\r\n\t\t\tface.Influence = Math.Clamp(value, 0, 1);\r\n\t\telse\r\n\t\t\tRegion(true).PhotoWeights[face.Id] = Math.Clamp(value, 0, 1);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid ResetRegion()\r\n\t{\r\n\t\tif (BlendRegion != \"overall\")\r\n\t\t\tAppearance.BaseShape.Regions.Remove(BlendRegion);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid SetBodyBlend(float value)\r\n\t{\r\n\t\tif (Appearance.BaseShape is null || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tAppearance.BaseShape.MaleBody = Math.Clamp(value, 0, 1);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid ToggleClothingVariant(string variant)\r\n\t{\r\n\t\tif (Appearance.BaseShape is null)\r\n\t\t\treturn;\r\n\t\tAppearance.BaseShape.ClothingVariant = Appearance.BaseShape.ClothingVariant == variant ? null : variant;\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid SetBodyFeature(bool breast, float value)\r\n\t{\r\n\t\tif (Appearance.BaseShape is null || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tif (breast)\r\n\t\t\tAppearance.BaseShape.BreastSize = Math.Clamp(value, 0, 1);\r\n\t\telse\r\n\t\t\tAppearance.BaseShape.BulgeSize = Math.Clamp(value, 0, 1);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid ChooseBody(string body)\r\n\t{\r\n\t\tvar skin = AppearanceCatalog.Skins.FirstOrDefault(x => x.BodyType == body);\r\n\t\tif (skin is not null)\r\n\t\t\tChooseSkin(skin);\r\n\t}\r\n\r\n\tvoid ChooseSkin(AppearanceOption skin)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.SkinPath = skin.ResourcePath;\r\n\t\tif (Appearance.BaseShape is null && !AppearanceCatalog.FacialHair.Any(x => x.ResourcePath == Appearance.FacialHairPath && (x.BodyType == \"\" || x.BodyType == skin.BodyType)))\r\n\t\t\tAppearance.FacialHairPath = \"\";\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ChooseHair(AppearanceOption hair)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.HairPath = hair.ResourcePath;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ChooseFacial(AppearanceOption facial)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.FacialHairPath = facial.ResourcePath;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ChooseColor(AppearanceColorOption color)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.HairColor = color.Color;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ChooseSkinColor(Color color, bool normalize = false)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.SkinColor = color;\r\n\t\tAppearance.NormalizeSkinTone = normalize;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tstatic IReadOnlyList<AppearanceColorOption> EyeColors => new[]\r\n\t{\r\n\t\tnew AppearanceColorOption(\"brown\", \"Brown\", new(.24f, .12f, .055f)),\r\n\t\tnew(\"hazel\", \"Hazel\", new(.34f, .3f, .11f)),\r\n\t\tnew(\"amber\", \"Amber\", new(.58f, .34f, .075f)),\r\n\t\tnew(\"green\", \"Green\", new(.13f, .34f, .19f)),\r\n\t\tnew(\"blue\", \"Blue\", new(.09f, .3f, .55f)),\r\n\t\tnew(\"grey\", \"Grey\", new(.32f, .36f, .4f)),\r\n\t\tnew(\"violet\", \"Violet\", new(.32f, .16f, .46f))\r\n\t};\r\n\r\n\tvoid ChooseEyeColor(Color? color)\r\n\t{\r\n\t\tif (LoadingCatalog || LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.CustomEyeColor = color.HasValue;\r\n\t\tif (color.HasValue)\r\n\t\t\tAppearance.EyeColor = color.Value;\r\n\t}\r\n\r\n\tfloat EyeChannel(string channel) => channel == \"Red\" ? Appearance.EyeColorR : channel == \"Green\" ? Appearance.EyeColorG : Appearance.EyeColorB;\r\n\tvoid SetEyeChannel(string channel, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.CustomEyeColor = true;\r\n\t\tvalue = Math.Clamp(value, 0, 1);\r\n\t\tif (channel == \"Red\")\r\n\t\t\tAppearance.EyeColorR = value;\r\n\t\telse if (channel == \"Green\")\r\n\t\t\tAppearance.EyeColorG = value;\r\n\t\telse\r\n\t\t\tAppearance.EyeColorB = value;\r\n\t}\r\n\r\n\tfloat SkinChannel(string channel) => channel == \"Red\" ? Appearance.SkinColorR : channel == \"Green\" ? Appearance.SkinColorG : Appearance.SkinColorB;\r\n\tvoid SetSkinChannel(string channel, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tvalue = Math.Clamp(value, 0, 1);\r\n\t\tAppearance.NormalizeSkinTone = true;\r\n\t\tif (channel == \"Red\")\r\n\t\t\tAppearance.SkinColorR = value;\r\n\t\telse if (channel == \"Green\")\r\n\t\t\tAppearance.SkinColorG = value;\r\n\t\telse\r\n\t\t\tAppearance.SkinColorB = value;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid SetShape(AppearanceSlider slider, float value)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tslider.Set(Appearance, value);\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ResetShapes()\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearanceCatalog.ResetShapes(Appearance);\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid Randomize()\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tvar voice = Appearance.VoiceId;\r\n\t\tvar variant = Appearance.BaseShape?.ClothingVariant;\r\n\t\tvar photos = Appearance.BaseShape?.PhotoFaces.Select(x => x.DeepCopy()).ToList();\r\n\t\tAppearance = AppearanceCatalog.Randomize(blendHeads: Appearance.BaseShape is not null);\r\n\t\tAppearance.VoiceId = voice;\r\n\t\tif (Appearance.BaseShape is not null)\r\n\t\t{\r\n\t\t\tAppearance.BaseShape.ClothingVariant = variant;\r\n\t\t\tif (photos is not null)\r\n\t\t\t{\r\n\t\t\t\tforeach (var face in photos)\r\n\t\t\t\t\tface.Influence = 0;\r\n\t\t\t\tAppearance.BaseShape.PhotoFaces = photos;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid Reset()\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tvar voice = Appearance.VoiceId;\r\n\t\tbool unified = Appearance.BaseShape is not null;\r\n\t\tAppearance = AppearanceCatalog.CreateDefault();\r\n\t\tif (unified)\r\n\t\t\tUnifiedHuman.Enable(Appearance);\r\n\t\tAppearance.VoiceId = voice;\r\n\t\tChosenPreset = \"traveller\";\r\n\t\tYaw = 0;\r\n\t\tRecenterPreview();\r\n\t\tSetZoom(CharacterCreatorPreview.FaceZoom);\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid Rotate(float delta)\r\n\t{\r\n\t\tYaw = (Yaw + delta) % 360;\r\n\t}\r\n\r\n\tvoid SetYaw(float value)\r\n\t{\r\n\t\tYaw = value;\r\n\t}\r\n\r\n\tvoid SetView(float value)\r\n\t{\r\n\t\tRecenterPreview();\r\n\t\tSetZoom(value);\r\n\t}\r\n\r\n\tvoid SetZoom(float value)\r\n\t{\r\n\t\tif (float.IsFinite(value))\r\n\t\t{\r\n\t\t\tZoom = Math.Clamp(value, 0, CharacterCreatorPreview.MaxZoom);\r\n\t\t\tPortrait = Zoom >= CharacterCreatorPreview.FaceZoom - 0.001f;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid SetPitch(float value)\r\n\t{\r\n\t\tPreviewPitch = value;\r\n\t}\r\n\r\n\tvoid SetPan(Vector3 value)\r\n\t{\r\n\t\tPreviewPan = value;\r\n\t}\r\n\r\n\tvoid RecenterPreview()\r\n\t{\r\n\t\tYaw = 0;\r\n\t\tPreviewPan = Vector3.Zero;\r\n\t\tPreviewPitch = 0;\r\n\t\tPreview?.Recenter();\r\n\t}\r\n\r\n\tvoid ToggleClothing()\r\n\t{\r\n\t\tShowClothing = !ShowClothing;\r\n\t}\r\n\r\n\tvoid Back()\r\n\t{\r\n\t\tif (!LoadingCatalog)\r\n\t\t{\r\n\t\t\tStopVoicePreview();\r\n\t\t\tClosed?.Invoke();\r\n\t\t}\r\n\t}\r\n\r\n\tvoid Start()\r\n\t{\r\n\t\tif (Disabled || LoadingCatalog || Preview?.Loading == true)\r\n\t\t\treturn;\r\n\t\tif (!CatalogReady)\r\n\t\t{\r\n\t\t\tFeedback = LoadingCatalog ? \"Loading character options\u2026\" : \"Character options could not be loaded.\";\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif (CharacterAppearance.ValidateForApply(Appearance)is string error)\r\n\t\t{\r\n\t\t\tFeedback = error;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tStopVoicePreview();\r\n\t\tFeedback = Saved?.Invoke(CharacterName, Appearance.DeepCopy());\r\n\t}\r\n\r\n\tpublic override void Delete(bool immediate = false)\r\n\t{\r\n\t\tStopVoicePreview();\r\n\t\tbase.Delete(immediate);\r\n\t}\r\n\r\n\tprotected override async Task OnParametersSetAsync()\r\n\t{\r\n\t\tif (InitialPreset is not null && !ReferenceEquals(loadedPreset, InitialPreset))\r\n\t\t{\r\n\t\t\tLoadDraft(InitialPreset.Name, InitialPreset.Appearance);\r\n\t\t\tloadedPreset = InitialPreset;\r\n\t\t}\r\n\r\n\t\tif (CatalogReady || LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tLoadingCatalog = true;\r\n\t\ttry\r\n\t\t{\r\n\t\t\tawait AppearanceCatalog.EnsureLoadedAsync();\r\n\t\t\tCatalogReady = true;\r\n\t\t}\r\n\t\tcatch (Exception)\r\n\t\t{\r\n\t\t\tFeedback = \"Character options could not be loaded. Please try again.\";\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\tLoadingCatalog = false;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override int BuildHash() => HashCode.Combine(CharacterName, PlaythroughName, HashCode.Combine(Tab, ShapeCategory, SkinPalette, MakeupLayer, CustomMakeup, SelectedMarking, MarkingSection, BlendRegion), ChosenPreset, JsonSerializer.Serialize(Appearance) + Appearance.Makeup.PresentationKey(), HashCode.Combine(Portrait, Yaw, Feedback, LoadingCatalog, SaveLabel, Preview?.Error, CatalogReady, HashCode.Combine(LoadingCatalog, Disabled, ProductLabel, LocationLabel, Title)), Zoom, HashCode.Combine(ShowClothing, PreviewPan, PreviewPitch, WardrobeSearch, WardrobeCategory, ClothingResources.Revision, CloudWardrobeOpen));\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "CharacterKit/PhotoProfileAnchors.cs",
"FileName": "PhotoProfileAnchors.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "namespace CharacterEditor;\n// Generated by ce_calibrate_photo_profile from native topology and packed engine deformers.\npublic static class PhotoProfileAnchors\n{\n public static readonly int[] Ids={10,168,4,1,13,14,152};\n public static readonly string[] Controls={\"head\",\"jaw\",\"eyes\",\"nosewidth\",\"nostrils\",\"noseheight\",\"mouthwidth\",\"nose\",\"chin\",\"mouthprojection\",\"chinheight\"};\n public static readonly Vector3[][] Heads={\nnew Vector3[]{new(3.6992545f,-0.066790596f,68.14279f),new(5.2946215f,0.11305821f,64.60188f),new(5.959813f,-0.041913833f,63.655262f),new(6.1343274f,-0.058205567f,63.346245f),new(5.8273835f,0.07009718f,61.76828f),new(5.7244573f,0.09594756f,61.383133f),new(4.6132507f,-0.040823318f,59.63866f)},\nnew Vector3[]{new(3.6878302f,-0.014352961f,68.02986f),new(5.085779f,0.0712296f,64.49504f),new(5.841306f,-0.03587849f,63.486412f),new(6.0319343f,-0.045126636f,63.211014f),new(5.53363f,0.09146978f,61.719315f),new(5.485255f,0.1368592f,61.244686f),new(4.676547f,-0.012164734f,59.841347f)},\nnew Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2011194f,0.07334158f,64.609566f),new(5.979442f,-0.033484418f,63.572296f),new(6.1241097f,-0.03964384f,63.281433f),new(5.778057f,0.053285245f,61.74f),new(5.719415f,0.069648676f,61.50413f),new(4.7025223f,-0.06262799f,59.70981f)},\nnew Vector3[]{new(3.5149546f,-0.009116913f,68.08719f),new(5.0824428f,0.056152936f,64.4079f),new(5.857973f,-0.08385171f,63.312653f),new(6.0649567f,-0.10289183f,62.99997f),new(5.4969316f,0.05211438f,61.666157f),new(5.502199f,0.10385137f,61.29811f),new(4.67235f,-0.1021266f,59.774113f)},\nnew Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2132f,0.087325275f,64.6164f),new(5.957733f,0.064072326f,63.39234f),new(6.137288f,0.064377435f,63.1475f),new(5.6911297f,0.052945673f,61.71075f),new(5.713699f,0.06934876f,61.489025f),new(4.702329f,-0.055987295f,59.563744f)},\nnew Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2012277f,0.1540686f,64.59866f),new(6.0016494f,-0.08192249f,63.62896f),new(6.165094f,-0.049669266f,63.36159f),new(5.7699647f,0.05389818f,61.78059f),new(5.719415f,0.06964957f,61.493782f),new(4.700662f,-0.0723621f,59.84928f)},\nnew Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.173374f,0.09990916f,64.60435f),new(5.9552946f,0.042667035f,63.536186f),new(6.1126857f,0.03899601f,63.296906f),new(5.6911297f,0.0529435f,61.727646f),new(5.713699f,0.07963325f,61.420948f),new(4.702329f,-0.05598729f,59.74437f)}\n };\n // [head * Controls.Length * 2 + control * 2 + positiveEndpoint]\n public static readonly Vector3[][] Deltas={\nnew Vector3[]{new(-0.13209844f,0.0031800792f,-0.07652283f),new(-0.24389982f,-0.0069441274f,-0.03074646f),new(-0.23990822f,0.0022496209f,-0.010990143f),new(-0.23487473f,0.0029714853f,-0.0055236816f),new(-0.23757076f,-0.003810294f,0.020923615f),new(-0.23392248f,-0.0052277073f,0.02753067f),new(-0.2026701f,0.0023912787f,0.061531067f)},\nnew Vector3[]{new(0.13209844f,-0.0031800792f,0.07652283f),new(0.24389982f,0.0069441274f,0.03074646f),new(0.23990822f,-0.0022496209f,0.010990143f),new(0.23487473f,-0.0029714853f,0.0055236816f),new(0.23757076f,0.003810294f,-0.020923615f),new(0.23392248f,0.0052277073f,-0.02753067f),new(0.2026701f,-0.0023912787f,-0.061531067f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.011735871f,0f),new(0f,0.016297556f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.011735871f,0f),new(0f,-0.016297556f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3449974f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.0107391365f,0f),new(0f,-0.0056006014f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.010739133f,0f),new(0f,0.0056006014f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.041153908f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.0532794f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.13788319f,0f,0f),new(-0.052534103f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.19303656f,0f,0f),new(0.07354784f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.47891617f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.31927872f)},\nnew Vector3[]{new(-0.1366136f,0.0007089311f,-0.07763672f),new(-0.24782324f,-0.004627906f,-0.03035736f),new(-0.2453003f,0.002008915f,-0.00851059f),new(-0.23964739f,0.0023905002f,-0.0034942627f),new(-0.24498606f,-0.0053994358f,0.02362442f),new(-0.23736048f,-0.007896334f,0.031650543f),new(-0.20918512f,0.0007255161f,0.05886841f)},\nnew Vector3[]{new(0.1366136f,-0.0007089311f,0.07763672f),new(0.24782324f,0.004627906f,0.03035736f),new(0.2453003f,-0.002008915f,0.00851059f),new(0.23964739f,-0.0023905002f,0.0034942627f),new(0.24498606f,0.0053994358f,-0.02362442f),new(0.23736048f,0.007896334f,-0.031650543f),new(0.20918512f,-0.0007255161f,-0.05886841f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.010045975f,0f),new(0f,0.012437068f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.010045975f,0f),new(0f,-0.012437068f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.33958435f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.33958435f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.018210128f,0f),new(0f,-0.0033276677f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.018210128f,0f),new(0f,0.0033276677f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4882121f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4882121f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.2155137f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.27901363f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.17917538f,0f,0f),new(-0.02188301f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.25084543f,0f,0f),new(0.03063631f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5400009f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3600006f)},\nnew Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.2449646f,-0.004605703f,-0.031585693f),new(-0.2398367f,0.001790762f,-0.009563446f),new(-0.23560143f,0.0020335242f,-0.004512787f),new(-0.2387433f,-0.002935592f,0.021694183f),new(-0.23645878f,-0.0038393363f,0.02576828f),new(-0.2047453f,0.0036357194f,0.059688568f)},\nnew Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.2449646f,0.004605703f,0.031585693f),new(0.2398367f,-0.001790762f,0.009563446f),new(0.23560143f,-0.0020335242f,0.004512787f),new(0.2387433f,0.002935592f,-0.021694183f),new(0.23645878f,0.0038393363f,-0.02576828f),new(0.2047453f,-0.0036357194f,-0.059688568f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009375636f,0f),new(0f,0.011100274f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009375636f,0f),new(0f,-0.011100274f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.008732874f,0f),new(0f,-0.008089323f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.008732874f,0f),new(0f,0.008089326f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.1130929f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.14641476f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.14750004f,0f,0f),new(-0.104530334f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.20650053f,0f,0f),new(0.14634228f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5328598f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.35523987f)},\nnew Vector3[]{new(-0.13274455f,0.0004590759f,-0.08004761f),new(-0.24919367f,-0.0036709309f,-0.028762817f),new(-0.24560404f,0.004687473f,-0.0054626465f),new(-0.238554f,0.0053960904f,0f),new(-0.24498224f,-0.0030967854f,0.02476883f),new(-0.23813248f,-0.0059928596f,0.03068924f),new(-0.20696688f,0.006031759f,0.059539795f)},\nnew Vector3[]{new(0.13274455f,-0.0004590759f,0.08004761f),new(0.24919367f,0.0036709309f,0.028762817f),new(0.24560404f,-0.004687473f,0.0054626465f),new(0.238554f,-0.0053960904f,0f),new(0.24498224f,0.0030967854f,-0.02476883f),new(0.23813248f,0.0059928596f,-0.03068924f),new(0.20696688f,-0.006031759f,-0.059539795f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,-5.4202974E-05f,0f),new(0f,0.02339752f,0f),new(0f,0.020724386f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,5.4202974E-05f,0f),new(0f,-0.02339752f,0f),new(0f,-0.020724379f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,-0.0011901855f),new(0f,0f,-0.34381104f),new(0f,0f,-0.24817657f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0.0011901855f),new(0f,0f,0.34381104f),new(0f,0f,0.24817657f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.01034468f,0f),new(0f,-0.0050033107f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.01034468f,0f),new(0f,0.0050033107f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(-0.001709938f,0f,0f),new(-0.49428988f,0f,0f),new(-0.3567996f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0.001709938f,0f,0f),new(0.49428988f,0f,0f),new(0.3567996f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.15790367f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.20442915f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.17864943f,0f,0f),new(-0.043359756f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.2501092f,0f,0f),new(0.060703754f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.53889465f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.35926437f)},\nnew Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.24469805f,-0.0054651946f,-0.03161621f),new(-0.24176359f,-0.0034667253f,-0.0066337585f),new(-0.23539543f,-0.0032922663f,-0.002357483f),new(-0.24088907f,-0.0029880553f,0.02273941f),new(-0.23634768f,-0.0038248226f,0.026042938f),new(-0.19970894f,0.0031703971f,0.060806274f)},\nnew Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.24469805f,0.0054651946f,0.03161621f),new(0.24176359f,0.0034667253f,0.0066337585f),new(0.23539543f,0.0032922626f,0.002357483f),new(0.24088907f,0.0029880553f,-0.02273941f),new(0.23634768f,0.0038248226f,-0.026042938f),new(0.19970894f,-0.0031703971f,-0.060806274f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.017940246f,0f),new(0f,-0.017056447f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.017940246f,0f),new(0f,0.017056443f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.32645035f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.32645035f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009510666f,0f),new(0f,-0.0076843873f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009510666f,0f),new(0f,0.0076843947f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4693303f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4693303f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.015622139f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.020225048f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.16166735f,0f,0f),new(-0.099727154f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.22633457f,0f,0f),new(0.13961792f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.4568138f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.30454254f)},\nnew Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.2450552f,-0.009678587f,-0.03138733f),new(-0.23850155f,0.004340723f,-0.010414124f),new(-0.23345089f,0.0025077388f,-0.005706787f),new(-0.2396326f,-0.002984602f,0.021102905f),new(-0.2362647f,-0.003836237f,0.025924683f),new(-0.2093401f,0.004296787f,0.05846405f)},\nnew Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.2450552f,0.009678587f,0.03138733f),new(0.23850155f,-0.004340723f,0.010414124f),new(0.23345089f,-0.0025077388f,0.005706787f),new(0.2396326f,0.002984602f,-0.021102905f),new(0.2362647f,0.003836237f,-0.025924683f),new(0.2093401f,-0.004296787f,-0.05846405f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.022938296f,0f),new(0f,0.013907395f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.022938296f,0f),new(0f,-0.013907395f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009057671f,0f),new(0f,-0.0077751838f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009057675f,0f),new(0f,0.00777518f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.22866583f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.29604006f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.15124607f,0f,0f),new(-0.10046959f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.21174479f,0f,0f),new(0.14065742f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5400009f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3600006f)},\nnew Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.24530983f,-0.006316647f,-0.031700134f),new(-0.24099922f,-0.002302207f,-0.0090408325f),new(-0.23603249f,-0.0020077005f,-0.004776001f),new(-0.24115801f,-0.0029912703f,0.022464752f),new(-0.23502398f,-0.004367456f,0.027061462f),new(-0.2059083f,0.0032688156f,0.05939865f)},\nnew Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.24530983f,0.006316647f,0.031700134f),new(0.24099922f,0.002302207f,0.0090408325f),new(0.23603249f,0.0020077005f,0.004776001f),new(0.24115801f,0.0029912703f,-0.022464752f),new(0.23502398f,0.004367456f,-0.027061462f),new(0.2059083f,-0.0032688156f,-0.05939865f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.011946769f,0f),new(0f,-0.010918882f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.011946768f,0f),new(0f,0.0109188855f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009599518f,0f),new(0f,-0.0062645525f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009599518f,0f),new(0f,0.0062645525f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.14241362f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.18437433f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.16318464f,0f,0f),new(-0.07080078f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.2284584f,0f,0f),new(0.099121094f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5386009f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.359066f)}\n };\n}\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/AppearanceCatalog.cs",
"FileName": "AppearanceCatalog.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Threading.Tasks;\r\n\r\nnamespace CharacterEditor;\r\npublic sealed record AppearanceOption(string Id, string Label, string ResourcePath, string BodyType = \"\", string Icon = \"\");\r\npublic sealed record AppearancePreset(string Id, string Label, CharacterAppearanceData Appearance);\r\npublic sealed record AppearanceColorOption(string Id, string Label, Color Color, bool NormalizeSkinTone = false);\r\npublic sealed class AppearanceSlider\r\n{\r\n\tpublic string Id { get; }\r\n\tpublic string Label { get; }\r\n\tpublic string Group { get; }\r\n\tpublic float Min => 0;\r\n\tpublic float Max => 1;\r\n\tpublic float Neutral => Id is \"eartaper\" or \"belly\" ? 0 : 0.5f;\r\n\r\n\tpublic string LabelFor(CharacterAppearanceData data) => Id == \"bodyfeature\" ? (AppearanceCatalog.IsFemale(data) ? \"Breast size\" : \"Bulge size\") : Label;\r\n\tpublic AppearanceSlider(string id, string label, string group)\r\n\t{\r\n\t\tId = id;\r\n\t\tLabel = label;\r\n\t\tGroup = group;\r\n\t}\r\n\r\n\tpublic float Get(CharacterAppearanceData data) => Id switch\r\n\t{\r\n\t\t\"neck\" => data.NeckSize,\r\n\t\t\"trapezius\" => data.TrapeziusSize,\r\n\t\t\"collarbone\" => data.CollarboneHeight,\r\n\t\t\"belly\" => data.BellySize,\r\n\t\t\"waist\" => data.WaistSize,\r\n\t\t\"chest\" => data.ChestSize,\r\n\t\t\"fitness\" => data.Fitness,\r\n\t\t\"head\" => data.HeadShape,\r\n\t\t\"nose\" => data.NoseSize,\r\n\t\t\"nosewidth\" => data.NoseWidth,\r\n\t\t\"chin\" => data.ChinSize,\r\n\t\t\"chinwidth\" => data.ChinWidth,\r\n\t\t\"jaw\" => data.JawWidth,\r\n\t\t\"nostrils\" => data.NostrilWidth,\r\n\t\t\"forehead\" => data.ForeheadWidth,\r\n\t\t\"cheekbones\" => data.CheekboneWidth,\r\n\t\t\"ears\" => data.EarSize,\r\n\t\t\"eyes\" => data.EyeSize,\r\n\t\t\"cheeks\" => data.CheekFullness,\r\n\t\t\"nostrilfullness\" => data.NostrilFullness,\r\n\t\t\"noseheight\" => data.NoseHeight,\r\n\t\t\"chinheight\" => data.ChinHeight,\r\n\t\t\"submental\" => data.SubmentalFullness,\r\n\t\t\"brow\" => data.BrowProminence,\r\n\t\t\"bodyfeature\" => data.BodyFeatureSize,\r\n\t\t\"butt\" => data.ButtSize,\r\n\t\t\"feet\" => data.FootSize,\r\n\t\t\"eartaper\" => data.EarTaper,\r\n\t\t\"philtrum\" => data.PhiltrumWidth,\r\n\t\t\"columella\" => data.ColumellaWidth,\r\n\t\t\"mouthwidth\" => data.MouthWidth,\r\n\t\t\"mouthprojection\" => data.MouthProjection,\r\n\t\t\"upperlip\" => data.UpperLipFullness,\r\n\t\t\"lowerlip\" => data.LowerLipFullness,\r\n\t\t_ => Neutral\r\n\t};\r\n\tpublic void Set(CharacterAppearanceData data, float value)\r\n\t{\r\n\t\tif (data is null || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tvalue = Math.Clamp(value, Min, Max);\r\n\t\t// Store data rather than delegates in the catalog so native hotload can preserve these selectors.\r\n\t\tswitch (Id)\r\n\t\t{\r\n\t\t\tcase \"neck\":\r\n\t\t\t\tdata.NeckSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"trapezius\":\r\n\t\t\t\tdata.TrapeziusSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"collarbone\":\r\n\t\t\t\tdata.CollarboneHeight = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"belly\":\r\n\t\t\t\tdata.BellySize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"waist\":\r\n\t\t\t\tdata.WaistSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"chest\":\r\n\t\t\t\tdata.ChestSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"fitness\":\r\n\t\t\t\tdata.Fitness = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"head\":\r\n\t\t\t\tdata.HeadShape = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"nose\":\r\n\t\t\t\tdata.NoseSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"nosewidth\":\r\n\t\t\t\tdata.NoseWidth = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"chin\":\r\n\t\t\t\tdata.ChinSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"chinwidth\":\r\n\t\t\t\tdata.ChinWidth = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"jaw\":\r\n\t\t\t\tdata.JawWidth = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"nostrils\":\r\n\t\t\t\tdata.NostrilWidth = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"forehead\":\r\n\t\t\t\tdata.ForeheadWidth = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"cheekbones\":\r\n\t\t\t\tdata.CheekboneWidth = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"ears\":\r\n\t\t\t\tdata.EarSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"eyes\":\r\n\t\t\t\tdata.EyeSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"cheeks\":\r\n\t\t\t\tdata.CheekFullness = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"nostrilfullness\":\r\n\t\t\t\tdata.NostrilFullness = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"noseheight\":\r\n\t\t\t\tdata.NoseHeight = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"chinheight\":\r\n\t\t\t\tdata.ChinHeight = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"submental\":\r\n\t\t\t\tdata.SubmentalFullness = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"brow\":\r\n\t\t\t\tdata.BrowProminence = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"bodyfeature\":\r\n\t\t\t\tdata.BodyFeatureSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"butt\":\r\n\t\t\t\tdata.ButtSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"feet\":\r\n\t\t\t\tdata.FootSize = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"eartaper\":\r\n\t\t\t\tdata.EarTaper = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"philtrum\":\r\n\t\t\t\tdata.PhiltrumWidth = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"columella\":\r\n\t\t\t\tdata.ColumellaWidth = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"mouthwidth\":\r\n\t\t\t\tdata.MouthWidth = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"mouthprojection\":\r\n\t\t\t\tdata.MouthProjection = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"upperlip\":\r\n\t\t\t\tdata.UpperLipFullness = value;\r\n\t\t\t\tbreak;\r\n\t\t\tcase \"lowerlip\":\r\n\t\t\t\tdata.LowerLipFullness = value;\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\t}\r\n}\r\n\r\n/// <summary>Curated installed, non-inventory native assets. IDs and paths are stable; no cloud discovery or external download is needed.</summary>\r\npublic static class AppearanceCatalog\r\n{\r\n\tstatic readonly Dictionary<string, Task> resourceLoads = new(StringComparer.OrdinalIgnoreCase);\r\n\tpublic static bool Loading => resourceLoads.Values.Any(x => x is not null && !x.IsCompleted);\r\n\tpublic static string Error { get; private set; }\r\n\tpublic static int Revision { get; private set; }\r\n\r\n\tconst string Root = \"models/citizen_clothes/\";\r\n\tstatic AppearanceOption Skin(string number, string label, string body) => new(\"human\" + number, label, Root + \"body/human\" + number + \".clothing\", body, Root + \"body/human\" + number + \".clothing.png\");\r\n\tstatic AppearanceOption Style(string id, string label, string relative, string body = \"\") => new(id, label, Root + \"hair/\" + relative + \".clothing\", body, Root + \"hair/\" + relative + \".clothing.png\");\r\n\tpublic static IReadOnlyList<AppearanceOption> Skins { get; } = Array.AsReadOnly(new[] { Skin(\"01\", \"Face 1 \u00b7 dark \u00b7 male\", \"male\"), Skin(\"02\", \"Face 2 \u00b7 dark \u00b7 female\", \"female\"), Skin(\"03\", \"Face 3 \u00b7 light \u00b7 male\", \"male\"), Skin(\"04\", \"Face 4 \u00b7 light \u00b7 female\", \"female\"), Skin(\"05\", \"Face 5 \u00b7 light \u00b7 male\", \"male\"), Skin(\"06\", \"Face 6 \u00b7 dark \u00b7 male\", \"male\"), Skin(\"07\", \"Face 7 \u00b7 light \u00b7 male\", \"male\") });\r\n\tpublic static IReadOnlyList<AppearanceOption> Hair { get; } = Array.AsReadOnly(new[] { new AppearanceOption(\"none\", \"No hair\", \"\"), Style(\"scruffy\", \"Short scruffy\", \"hair_shortscruffy/hair_shortscruffy_brown\"), Style(\"short_braids\", \"Short braids\", \"hair_short_braids/hair_short_braids\"), Style(\"short_dreads\", \"Short dreadlocks\", \"hair_short_dreadlocks/short_dreadlocks\"), Style(\"bob\", \"Bob cut\", \"hair_bobcut/hair_bobcut\"), Style(\"long_bob\", \"Long bob\", \"hair_longbob/hair_longbob\"), Style(\"messy_bun\", \"Messy bun\", \"hair_messy_bun/hair_messy_bun\"), Style(\"ponytail\", \"Tight ponytail\", \"hair_tight_ponytail/hair_tight_ponytail_brown\"), Style(\"long_curly\", \"Long curly\", \"hair_longcurly/hair_longcurly_brown\"), Style(\"long_bangs\", \"Long bangs\", \"hair_long_bangs/hair_long_bangs\"), Style(\"long_dreads\", \"Long dreadlocks\", \"hair_long_dreadlocks/hair_long_dreadlocks\"), Style(\"tied_braids\", \"Tied braids\", \"hair_tied_braids/hair_tied_braids\"), Style(\"tied_dreads\", \"Tied dreadlocks\", \"hair_tied_dreads/hair_tied_dreads\"), Style(\"slicked\", \"Slicked back\", \"hair_slickedback/hair_slickedback\"), Style(\"balding\", \"Balding\", \"hair_balding/hair_baldingbrown\"), Style(\"thinning\", \"Thinning short\", \"hair_thinning_short/thinning_short\"), Style(\"pigtails\", \"Pigtails\", \"hair_pigtails/hair_pigtails\"), Style(\"long_brown\", \"Long hair\", \"hair_longbrown/Models/hair_longbrown\"), Style(\"loose\", \"Loose hair\", \"hair_looseblonde/hair.loose.grey\"), Style(\"slicked_balding\", \"Slicked back balding\", \"hair_slicked_back_balding/hair_slicked_back_balding\") });\r\n\tpublic static IReadOnlyList<AppearanceOption> FacialHair { get; } = Array.AsReadOnly(new[] { new AppearanceOption(\"none\", \"No facial hair\", \"\"), Style(\"patchy\", \"Patchy beard\", \"patchy_beard/patchy_beard\", \"male\"), Style(\"medium\", \"Medium beard\", \"medium_beard/medium_beard\", \"male\"), Style(\"goatee\", \"Goatee\", \"goatee/goatee\", \"male\"), Style(\"moustache\", \"Moustache\", \"moustache/moustache\", \"male\"), Style(\"handlebar\", \"Handlebar moustache\", \"handlebar_moustache/handlebar_moustache\", \"male\"), Style(\"chops\", \"Sideburns\", \"mutton_chops/mutton_chops\", \"male\"), Style(\"braided\", \"Braided beard\", \"braided_long_beard/braided_long_beard\", \"male\"), Style(\"decorated\", \"Decorated beard\", \"decorated_beard/decorated_beard\", \"male\") });\r\n\tpublic static IReadOnlyList<AppearanceColorOption> Colors { get; } = Array.AsReadOnly(new[] { new AppearanceColorOption(\"black\", \"Black\", new Color(0.045f, 0.035f, 0.028f)), new AppearanceColorOption(\"brown\", \"Brown\", new Color(0.22f, 0.13f, 0.08f)), new AppearanceColorOption(\"auburn\", \"Auburn\", new Color(0.48f, 0.17f, 0.07f)), new AppearanceColorOption(\"blonde\", \"Blonde\", new Color(0.74f, 0.57f, 0.31f)), new AppearanceColorOption(\"silver\", \"Silver\", new Color(0.66f, 0.67f, 0.7f)), new AppearanceColorOption(\"white\", \"White\", new Color(0.92f, 0.89f, 0.82f)) });\r\n\r\n\tpublic const int ShapeControlCount = 34;\r\n\tpublic static bool IsFemale(CharacterAppearanceData data) => data?.BaseShape is not null ? data.BaseShape.MaleBody < .5f : Skins.FirstOrDefault(x => string.Equals(x.ResourcePath, data?.SkinPath, StringComparison.OrdinalIgnoreCase))?.BodyType == \"female\";\r\n\tstatic IReadOnlyList<AppearanceColorOption> humanSkinColors;\r\n\tpublic static IReadOnlyList<AppearanceColorOption> HumanSkinColors\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif (humanSkinColors?.Count != 7 || !humanSkinColors[1].NormalizeSkinTone)\r\n\t\t\t\thumanSkinColors = CreateHumanSkinColors();\r\n\t\t\treturn humanSkinColors;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic IReadOnlyList<AppearanceColorOption> CreateHumanSkinColors() => Array.AsReadOnly(new[] { new AppearanceColorOption(\"native\", \"Native\", Color.White), new AppearanceColorOption(\"fair\", \"Fair\", Color.White, true), new AppearanceColorOption(\"rose\", \"Rose\", new Color(1, 0.96f, 0.96f), true), new AppearanceColorOption(\"golden\", \"Golden\", new Color(1, 0.96f, 0.88f), true), new AppearanceColorOption(\"olive\", \"Olive\", new Color(0.91f, 0.88f, 0.77f), true), new AppearanceColorOption(\"tan\", \"Tan\", new Color(0.72f, 0.6f, 0.5f), true), new AppearanceColorOption(\"umber\", \"Umber\", new Color(0.42f, 0.3f, 0.23f), true) });\r\n\tstatic IReadOnlyList<AppearanceColorOption> fantasySkinColors;\r\n\tpublic static IReadOnlyList<AppearanceColorOption> FantasySkinColors\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif (fantasySkinColors?.Count != 6 || fantasySkinColors.Any(x => !x.NormalizeSkinTone))\r\n\t\t\t\tfantasySkinColors = CreateFantasySkinColors();\r\n\t\t\treturn fantasySkinColors;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic IReadOnlyList<AppearanceColorOption> CreateFantasySkinColors() => Array.AsReadOnly(new[] { new AppearanceColorOption(\"blue\", \"Blue\", new Color(0.18f, 0.4f, 1), true), new AppearanceColorOption(\"purple\", \"Purple\", new Color(0.6f, 0.25f, 1), true), new AppearanceColorOption(\"green\", \"Green\", new Color(0.3f, 0.85f, 0.4f), true), new AppearanceColorOption(\"red\", \"Crimson\", new Color(1, 0.25f, 0.22f), true), new AppearanceColorOption(\"teal\", \"Teal\", new Color(0.18f, 0.85f, 0.9f), true), new AppearanceColorOption(\"grey\", \"Slate\", new Color(0.5f, 0.6f, 0.74f), true) });\r\n\t// Native hotload can retain the previous static list when the catalog grows.\r\n\t// Build the supported immutable catalog lazily so new controls are available\r\n\t// in an already running editor as well as a cold-start game.\r\n\tpublic static IReadOnlyList<AppearanceSlider> Sliders\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif (contourControls?.Count != ShapeControlCount)\r\n\t\t\t\tcontourControls = CreateContourControls();\r\n\t\t\treturn contourControls;\r\n\t\t}\r\n\t}\r\n\r\n\tstatic IReadOnlyList<AppearanceSlider> contourControls;\r\n\tstatic IReadOnlyList<AppearanceSlider> CreateContourControls() => Array.AsReadOnly(new[] { new AppearanceSlider(\"neck\", \"Neck size\", \"Body\"), new AppearanceSlider(\"trapezius\", \"Trapezius\", \"Body\"), new AppearanceSlider(\"collarbone\", \"Collarbone height\", \"Body\"), new AppearanceSlider(\"belly\", \"Belly size\", \"Body\"), new AppearanceSlider(\"waist\", \"Waist size\", \"Body\"), new AppearanceSlider(\"chest\", \"Chest size\", \"Body\"), new AppearanceSlider(\"fitness\", \"Fitness\", \"Body\"), new AppearanceSlider(\"bodyfeature\", \"Body contour\", \"Body\"), new AppearanceSlider(\"butt\", \"Butt size\", \"Body\"), new AppearanceSlider(\"feet\", \"Foot size\", \"Body\"), new AppearanceSlider(\"head\", \"Head shape\", \"Face\"), new AppearanceSlider(\"forehead\", \"Forehead width\", \"Face\"), new AppearanceSlider(\"cheekbones\", \"Cheekbone width\", \"Face\"), new AppearanceSlider(\"cheeks\", \"Cheek fullness\", \"Face\"), new AppearanceSlider(\"jaw\", \"Jaw width\", \"Jaw & chin\"), new AppearanceSlider(\"chin\", \"Chin projection\", \"Jaw & chin\"), new AppearanceSlider(\"chinwidth\", \"Chin width\", \"Jaw & chin\"), new AppearanceSlider(\"chinheight\", \"Chin height\", \"Jaw & chin\"), new AppearanceSlider(\"submental\", \"Under-chin fullness\", \"Jaw & chin\"), new AppearanceSlider(\"nose\", \"Nose projection\", \"Nose\"), new AppearanceSlider(\"nosewidth\", \"Nose width\", \"Nose\"), new AppearanceSlider(\"nostrils\", \"Nostril width\", \"Nose\"), new AppearanceSlider(\"nostrilfullness\", \"Nostril-wing fullness\", \"Nose\"), new AppearanceSlider(\"noseheight\", \"Nose tip height\", \"Nose\"), new AppearanceSlider(\"columella\", \"Columella width\", \"Nose\"), new AppearanceSlider(\"philtrum\", \"Philtrum width\", \"Mouth\"), new AppearanceSlider(\"mouthwidth\", \"Mouth width\", \"Mouth\"), new AppearanceSlider(\"mouthprojection\", \"Mouth projection\", \"Mouth\"), new AppearanceSlider(\"upperlip\", \"Upper lip fullness\", \"Mouth\"), new AppearanceSlider(\"lowerlip\", \"Lower lip fullness\", \"Mouth\"), new AppearanceSlider(\"eyes\", \"Eye size\", \"Eyes & ears\"), new AppearanceSlider(\"ears\", \"Ear size\", \"Eyes & ears\"), new AppearanceSlider(\"brow\", \"Brow prominence\", \"Eyes & ears\"), new AppearanceSlider(\"eartaper\", \"Ear taper\", \"Eyes & ears\") });\r\n\tstatic CharacterAppearanceData Preset(string skin, string hair, string facial, string color) => new()\r\n\t{\r\n\t\tSkinPath = Skins.Single(x => x.Id == skin).ResourcePath,\r\n\t\tHairPath = Hair.Single(x => x.Id == hair).ResourcePath,\r\n\t\tFacialHairPath = FacialHair.Single(x => x.Id == facial).ResourcePath,\r\n\t\tHairColor = Colors.Single(x => x.Id == color).Color\r\n\t};\r\n\tstatic IReadOnlyList<AppearancePreset> presets;\r\n\tpublic static IReadOnlyList<AppearancePreset> Presets\r\n\t{\r\n\t\tget\r\n\t\t{\r\n\t\t\tif (presets?.Count != 7)\r\n\t\t\t\tpresets = Array.AsReadOnly(new[]\r\n\t\t\t\t{\r\n\t\t\t\t\tnew AppearancePreset(\"traveller\", \"Traveller\", Preset(\"human03\", \"scruffy\", \"none\", \"brown\")),\r\n\t\t\t\t\tnew AppearancePreset(\"scout\", \"Scout\", Preset(\"human02\", \"short_braids\", \"none\", \"black\")),\r\n\t\t\t\t\tnew AppearancePreset(\"scholar\", \"Scholar\", Preset(\"human04\", \"messy_bun\", \"none\", \"auburn\")),\r\n\t\t\t\t\tnew AppearancePreset(\"veteran\", \"Veteran\", Preset(\"human07\", \"balding\", \"medium\", \"silver\")),\r\n\t\t\t\t\tnew AppearancePreset(\"wanderer\", \"Wanderer\", Preset(\"human06\", \"short_dreads\", \"none\", \"black\")),\r\n\t\t\t\t\tnew AppearancePreset(\"artisan\", \"Artisan\", Preset(\"human05\", \"slicked\", \"goatee\", \"brown\")),\r\n\t\t\t\t\tnew AppearancePreset(\"explorer\", \"Explorer\", Preset(\"human01\", \"tied_braids\", \"none\", \"black\"))\r\n\t\t\t\t});\r\n\t\t\treturn presets;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static CharacterAppearanceData CreateDefault() => ApplyPreset(\"traveller\");\r\n\tpublic static CharacterAppearanceData ApplyPreset(string id) => Presets.FirstOrDefault(x => x.Id == id)?.Appearance.DeepCopy() ?? new CharacterAppearanceData();\r\n\tpublic static void ResetShapes(CharacterAppearanceData data)\r\n\t{\r\n\t\tif (data is null)\r\n\t\t\treturn;\r\n\t\tforeach (var slider in Sliders)\r\n\t\t\tslider.Set(data, slider.Neutral);\r\n\t\tif (data.BaseShape is not null)\r\n\t\t{\r\n\t\t\tdata.BaseShape.BreastSize = .5f;\r\n\t\t\tdata.BaseShape.BulgeSize = .5f;\r\n\t\t}\r\n\t}\r\n\r\n\tpublic static CharacterAppearanceData Randomize(Random rng = null, bool blendHeads = false)\r\n\t{\r\n\t\trng ??= new Random();\r\n\t\tvar body = rng.Next(2) == 0 ? \"male\" : \"female\";\r\n\t\t// Cross-body skins need weighted heads; native (v1) characters keep their native pool.\r\n\t\tvar skins = Skins.Where(x => blendHeads ? (body == \"male\" || x.Id is not (\"human05\" or \"human07\")) : x.BodyType == body).ToArray();\r\n\t\tvar skin = skins[rng.Next(skins.Length)];\r\n\t\tvar hair = RandomHairFor(body);\r\n\t\tvar facial = FacialHair.Where(x => x.BodyType == \"\" || x.BodyType == body).ToArray();\r\n\t\tvar tones = HumanSkinColors.Where(x => body == \"female\" && skin.Id == \"human06\" ? x.Id is \"native\" or \"tan\" or \"umber\" : x.NormalizeSkinTone).ToArray();\r\n\t\tvar tone = tones[rng.Next(tones.Length)];\r\n\t\tvar data = new CharacterAppearanceData\r\n\t\t{\r\n\t\t\tSkinPath = skin.ResourcePath,\r\n\t\t\tHairPath = hair[rng.Next(hair.Count)].ResourcePath,\r\n\t\t\tFacialHairPath = facial[rng.Next(facial.Length)].ResourcePath,\r\n\t\t\tHairColor = Colors[rng.Next(Colors.Count)].Color,\r\n\t\t\tSkinColor = tone.Color,\r\n\t\t\tNormalizeSkinTone = tone.NormalizeSkinTone\r\n\t\t};\r\n\t\tforeach (var slider in Sliders.Where(x => x.Id != \"belly\"))\r\n\t\t\tslider.Set(data, 0.3f + (float)rng.NextDouble() * 0.4f);\r\n\t\t// An added belly is an occasional, modest feature rather than a default.\r\n\t\tdata.BellySize = rng.NextDouble() < 0.10 ? 0.1f + (float)rng.NextDouble() * 0.3f : 0;\r\n\t\tif (body == \"female\" && rng.NextDouble() < 0.60)\r\n\t\t{\r\n\t\t\tAddMakeup(\"Lipstick\", 0.70, 0.35f, 0.85f, 4);\r\n\t\t\tAddMakeup(\"Eyeshadow\", 0.45, 0.20f, 0.65f, 3);\r\n\t\t\tAddMakeup(\"Blush\", 0.55, 0.15f, 0.55f, 4);\r\n\t\t}\r\n\r\n\t\tif (blendHeads)\r\n\t\t{\r\n\t\t\tUnifiedHuman.Enable(data);\r\n\t\t\t// Body identity must follow the roll, not the selected skin's native body.\r\n\t\t\tbool female = body == \"female\";\r\n\t\t\tdata.BaseShape.MaleBody = female ? 0 : 1;\r\n\t\t\tdata.BaseShape.BreastSize = female ? data.BodyFeatureSize : .5f;\r\n\t\t\tdata.BaseShape.BulgeSize = female ? .5f : data.BodyFeatureSize;\r\n\t\t\tdata.BaseShape.HeadWeights = RandomHeadWeights(rng, female, skin.Id, false);\r\n\t\t\tforeach (var region in FaceRegions.Ids)\r\n\t\t\t{\r\n\t\t\t\t// Each part gets its own dominant source rather than averaging every head\r\n\t\t\t\t// equally. Keep jaw/neck identity especially close to the body's family.\r\n\t\t\t\tdata.BaseShape.Regions[region] = new()\r\n\t\t\t\t{\r\n\t\t\t\t\tOverallInfluence = .15f + (float)rng.NextDouble() * .20f,\r\n\t\t\t\t\tHeadWeights = RandomHeadWeights(rng, female, skin.Id, region is \"jaw\" or \"throat\")\r\n\t\t\t\t};\r\n\t\t\t}\r\n\r\n\t\t\t// A shared jaw/throat source keeps the lower-face silhouette coherent.\r\n\t\t\tdata.BaseShape.Regions[\"throat\"] = data.BaseShape.Regions[\"jaw\"].DeepCopy();\r\n\t\t}\r\n\r\n\t\tdata.ClothingPaths = WardrobeCatalog.Randomize(rng, body == \"female\");\r\n\t\tdata.Version = 2;\r\n\t\treturn data;\r\n\t\tvoid AddMakeup(string layer, double chance, float low, float high, int paletteCount)\r\n\t\t{\r\n\t\t\tif (rng.NextDouble() >= chance)\r\n\t\t\t\treturn;\r\n\t\t\tvar options = MakeupCatalog.Colors(layer);\r\n\t\t\tvar target = MakeupCatalog.Layer(data.Makeup, layer);\r\n\t\t\ttarget.Color = options[rng.Next(Math.Min(paletteCount, options.Count))].Color;\r\n\t\t\ttarget.Amount = low + (float)rng.NextDouble() * (high - low);\r\n\t\t}\r\n\t}\r\n\r\n\tstatic float[] RandomHeadWeights(Random rng, bool female, string skin, bool structural)\r\n\t{\r\n\t\t// Allocate by family before choosing individual heads: five masculine faces\r\n\t\t// must not overwhelm the two feminine ones. Skin 04 on men excludes them.\r\n\t\tfloat ownShare = structural ? .95f + (float)rng.NextDouble() * .05f : .85f + (float)rng.NextDouble() * .15f;\r\n\t\tfloat femaleShare = female ? ownShare : skin == \"human04\" ? 0 : 1 - ownShare;\r\n\t\tvar weights = new float[7];\r\n\t\tFill(new[] { 1, 3 }, femaleShare);\r\n\t\tFill(new[] { 0, 2, 4, 5, 6 }, 1 - femaleShare);\r\n\t\tfloat max = weights.Max();\r\n\t\tfor (int i = 0; i < weights.Length; i++)\r\n\t\t\tweights[i] /= max;\r\n\t\treturn weights;\r\n\t\tvoid Fill(int[] indices, float share)\r\n\t\t{\r\n\t\t\tint primary = indices[rng.Next(indices.Length)];\r\n\t\t\tfloat dominance = .50f + (float)rng.NextDouble() * .40f;\r\n\t\t\tweights[primary] = share * dominance;\r\n\t\t\tfloat total = 0;\r\n\t\t\tforeach (int i in indices)\r\n\t\t\t\tif (i != primary)\r\n\t\t\t\t{\r\n\t\t\t\t\tweights[i] = .05f + (float)rng.NextDouble();\r\n\t\t\t\t\ttotal += weights[i];\r\n\t\t\t\t}\r\n\r\n\t\t\tforeach (int i in indices)\r\n\t\t\t\tif (i != primary)\r\n\t\t\t\t\tweights[i] *= share * (1 - dominance) / total;\r\n\t\t}\r\n\t}\r\n\r\n\t// Preferences apply only to Randomize. Every hairstyle remains manually\r\n\t// selectable and valid on either human body, including male pigtails.\r\n\tpublic static IReadOnlyList<AppearanceOption> RandomHairFor(string bodyType) => Hair.Where(x => bodyType == \"female\" ? x.Id is not (\"balding\" or \"thinning\" or \"slicked_balding\") : x.Id is \"none\" or \"scruffy\" or \"short_braids\" or \"short_dreads\" or \"long_dreads\" or \"tied_braids\" or \"tied_dreads\" or \"slicked\" or \"balding\" or \"thinning\" or \"ponytail\" or \"long_curly\" or \"long_brown\" or \"slicked_balding\").ToArray();\r\n\t/// <summary>Loads only curated native resources, either the whole creator catalog or one selected/saved appearance.</summary>\r\n\tpublic static async Task EnsureLoadedAsync(CharacterAppearanceData data = null)\r\n\t{\r\n\t\tif (data is not null && CharacterAppearanceData.Validate(data)is string error)\r\n\t\t\tthrow new InvalidOperationException(error);\r\n\t\tvar paths = data is null ? Skins.Concat(Hair).Concat(FacialHair).Select(x => x.ResourcePath) : new[]\r\n\t\t{\r\n\t\t\tdata.SkinPath,\r\n\t\t\tUnifiedHuman.PresentationSkin(data),\r\n\t\t\tdata.HairPath,\r\n\t\t\tdata.FacialHairPath\r\n\t\t};\r\n\t\ttry\r\n\t\t{\r\n\t\t\tforeach (var path in paths.Where(x => !string.IsNullOrEmpty(x)).Distinct(StringComparer.OrdinalIgnoreCase))\r\n\t\t\t\tawait Load(path);\r\n\t\t\tif (data is null || data.Makeup.Active)\r\n\t\t\t\tforeach (var skin in data is null ? Skins : Skins.Where(x => x.ResourcePath == data.SkinPath))\r\n\t\t\t\t\tawait HumanMakeupAssets.EnsureLoadedAsync(skin.Id);\r\n\t\t\tif (data is null || data.CustomEyeColor)\r\n\t\t\t\tawait EyeColorPresentation.EnsureLoadedAsync();\r\n\t\t\tif (data is not null)\r\n\t\t\t\tawait HumanMarkingModels.EnsureLoadedAsync(data);\r\n\t\t\tif (data?.BaseShape is not null)\r\n\t\t\t{\r\n\t\t\t\tvar model = await Model.LoadAsync(UnifiedHuman.ModelPath);\r\n\t\t\t\tif (model is null || model.IsError)\r\n\t\t\t\t\tthrow new InvalidOperationException(\"The unified human model is unavailable.\");\r\n\t\t\t\tawait UnifiedNeckAssets.LoadHeadAsync(data.SkinPath);\r\n\t\t\t}\r\n\r\n\t\t\tError = null;\r\n\t\t}\r\n\t\tcatch (Exception e)\r\n\t\t{\r\n\t\t\tError = e.Message;\r\n\t\t\tthrow;\r\n\t\t}\r\n\r\n\t\tstatic Task Load(string path)\r\n\t\t{\r\n\t\t\tif (resourceLoads.TryGetValue(path, out var pending) && pending is not null && !pending.IsFaulted && !pending.IsCanceled)\r\n\t\t\t\treturn pending;\r\n\t\t\treturn resourceLoads[path] = LoadResource(path);\r\n\t\t}\r\n\r\n\t\tstatic async Task LoadResource(string path)\r\n\t\t{\r\n\t\t\tClothing clothing = null;\r\n\t\t\tfor (var attempt = 0; attempt < 3; attempt++)\r\n\t\t\t{\r\n\t\t\t\tclothing = await ClothingResources.Load(path);\r\n\t\t\t\tif (clothing is not null && !clothing.IsError)\r\n\t\t\t\t\tbreak;\r\n\t\t\t\tif (attempt < 2)\r\n\t\t\t\t\tawait Task.Delay(150);\r\n\t\t\t}\r\n\r\n\t\t\tif (clothing is null || clothing.IsError)\r\n\t\t\t\tthrow new InvalidOperationException(\"A native appearance resource is unavailable: \" + path);\r\n\t\t\tforeach (var modelPath in new[]\r\n\t\t\t{\r\n\t\t\t\tclothing.HumanSkinModel,\r\n\t\t\t\tclothing.HumanAltModel,\r\n\t\t\t\tclothing.HumanAltFemaleModel\r\n\t\t\t}.Where(x => !string.IsNullOrEmpty(x)).Distinct(StringComparer.OrdinalIgnoreCase))\r\n\t\t\t{\r\n\t\t\t\tvar model = await Model.LoadAsync(modelPath);\r\n\t\t\t\tif (model is null || model.IsError)\r\n\t\t\t\t\tthrow new InvalidOperationException(\"A native appearance model is unavailable: \" + modelPath);\r\n\t\t\t}\r\n\r\n\t\t\tRevision++;\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/CharacterBaseShape.cs",
"FileName": "CharacterBaseShape.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\n\r\nnamespace CharacterEditor;\r\n/// <summary>Version 2 identity shape, independent of skin. Weights are in human01..human07 order.</summary>\r\npublic sealed class CharacterBaseShape\r\n{\r\n\tpublic float[] HeadWeights { get; set; } = new[]\r\n\t{\r\n\t\t0f,\r\n\t\t0f,\r\n\t\t1f,\r\n\t\t0f,\r\n\t\t0f,\r\n\t\t0f,\r\n\t\t0f\r\n\t};\r\n\tpublic List<PhotoFace> PhotoFaces { get; set; } = new();\r\n\tpublic Dictionary<string, FaceRegionBlend> Regions { get; set; } = new();\r\n\r\n\t[System.Text.Json.Serialization.JsonIgnore]\r\n\tpublic float TotalInfluence => HeadWeights.Sum() + (PhotoFaces?.Sum(x => x.Influence) ?? 0);\r\n\r\n\tpublic float NativeShare(int i) => TotalInfluence > 0 ? HeadWeights[i] / TotalInfluence : (i == 2 ? 1 : 0);\r\n\tpublic float PhotoShare(PhotoFace face) => TotalInfluence > 0 ? face.Influence / TotalInfluence : 0;\r\n\tpublic float MaleBody { get; set; } = 1;\r\n\tpublic string ClothingVariant { get; set; }\r\n\tpublic float BreastSize { get; set; } = .5f;\r\n\tpublic float BulgeSize { get; set; } = .5f;\r\n\r\n\t[System.Text.Json.Serialization.JsonIgnore]\r\n\tpublic bool Valid => HeadWeights?.Length == 7 && HeadWeights.All(Unit) && PhotoFaces is not null && PhotoFaces.Count <= 4 && PhotoFaces.All(x => x?.Valid == true) && PhotoFaces.Select(x => x.Id).Distinct().Count() == PhotoFaces.Count && Regions is not null && Regions.Count <= FaceRegions.Ids.Length && Regions.All(x => FaceRegions.Ids.Contains(x.Key) && x.Value?.Valid(PhotoFaces) == true) && Unit(MaleBody) && Unit(BreastSize) && Unit(BulgeSize) && ClothingVariant is null or \"male\" or \"female\";\r\n\r\n\tstatic bool Unit(float x) => float.IsFinite(x) && x >= 0 && x <= 1;\r\n\tpublic float[] Normalized()\r\n\t{\r\n\t\tif (!Valid)\r\n\t\t\treturn new[]\r\n\t\t\t{\r\n\t\t\t\t0f,\r\n\t\t\t\t0f,\r\n\t\t\t\t1f,\r\n\t\t\t\t0f,\r\n\t\t\t\t0f,\r\n\t\t\t\t0f,\r\n\t\t\t\t0f\r\n\t\t\t};\r\n\t\tfloat sum = TotalInfluence;\r\n\t\treturn sum > 0 ? HeadWeights.Select((x, i) => (x + PhotoFaces.Sum(f => f.Influence * f.HeadWeights[i])) / sum).ToArray() : new[]\r\n\t\t{\r\n\t\t\t0f,\r\n\t\t\t0f,\r\n\t\t\t1f,\r\n\t\t\t0f,\r\n\t\t\t0f,\r\n\t\t\t0f,\r\n\t\t\t0f\r\n\t\t};\r\n\t}\r\n\r\n\tpublic CharacterBaseShape DeepCopy() => new()\r\n\t{\r\n\t\tHeadWeights = HeadWeights?.ToArray(),\r\n\t\tPhotoFaces = PhotoFaces?.Select(x => x?.DeepCopy()).ToList(),\r\n\t\tRegions = Regions?.ToDictionary(x => x.Key, x => x.Value.DeepCopy()),\r\n\t\tMaleBody = MaleBody,\r\n\t\tBreastSize = BreastSize,\r\n\t\tBulgeSize = BulgeSize,\r\n\t\tClothingVariant = ClothingVariant\r\n\t};\r\n}\r\n\r\npublic static class UnifiedHuman\r\n{\r\n\tpublic const string ModelPath = \"models/characterkit/unified_human.vmdl\";\r\n\tpublic static void Enable(CharacterAppearanceData data)\r\n\t{\r\n\t\tif (data.BaseShape is not null)\r\n\t\t\treturn;\r\n\t\tbool female = AppearanceCatalog.IsFemale(data);\r\n\t\tvar index = AppearanceCatalog.Skins.ToList().FindIndex(s => s.ResourcePath == data.SkinPath);\r\n\t\tdata.BaseShape = new()\r\n\t\t{\r\n\t\t\tMaleBody = female ? 0 : 1,\r\n\t\t\tHeadWeights = new float[7],\r\n\t\t\tBreastSize = female ? data.BodyFeatureSize : .5f,\r\n\t\t\tBulgeSize = female ? .5f : data.BodyFeatureSize\r\n\t\t};\r\n\t\tdata.BaseShape.HeadWeights[Math.Max(0, index)] = 1;\r\n\t\tdata.Version = 2;\r\n\t}\r\n\r\n\tpublic static float MaleAmount(CharacterAppearanceData a) => a.BaseShape?.MaleBody ?? (AppearanceCatalog.IsFemale(a) ? 0 : 1);\r\n\tpublic static string PresentationSkin(CharacterAppearanceData a) => a.BaseShape is null ? a.SkinPath : (MaleAmount(a) >= .5f ? \"models/citizen_clothes/body/human03.clothing\" : \"models/citizen_clothes/body/human04.clothing\");\r\n\tpublic static void Apply(SkinnedModelRenderer body, CharacterAppearanceData a)\r\n\t{\r\n\t\tif (a.BaseShape is null)\r\n\t\t\treturn;\r\n\t\tvar weights = FaceRegions.ReferenceWeights(a.BaseShape);\r\n\t\tfor (int i = 0; i < 7; i++)\r\n\t\t\tif (i != 2)\r\n\t\t\t\tbody.Morphs.Set($\"ce_human{i + 1:00}\", weights[i]);\r\n\t\tFaceRegions.Apply(body, a.BaseShape, weights);\r\n\t\tbody.Morphs.Set(\"ce_body_female\", 1 - a.BaseShape.MaleBody);\r\n\t}\r\n\r\n\tpublic static string HeadMaterial(string skin)\r\n\t{\r\n\t\tvar id = System.IO.Path.GetFileNameWithoutExtension(skin);\r\n\t\tvar suffix = id switch\r\n\t\t{\r\n\t\t\t\"human01\" => \"malehead01\",\r\n\t\t\t\"human02\" => \"femalehead01\",\r\n\t\t\t\"human03\" => \"malehead02\",\r\n\t\t\t\"human04\" => \"femalehead02\",\r\n\t\t\t\"human05\" => \"malehead03\",\r\n\t\t\t\"human06\" => \"malehead04\",\r\n\t\t\t_ => \"malehead05\"\r\n\t\t};\r\n\t\treturn $\"models/citizen_human/heads/citizen_human_{suffix}/citizen_human_{suffix}.vmat\";\r\n\t}\r\n\r\n\tpublic static Material SkinMaterial(Material slot, string skin, bool unifiedBody = true)\r\n\t{\r\n\t\tvar original = HumanMarkingModels.NativeMaterial(slot);\r\n\t\tif (original.Name.Contains(\"/heads/\") && original.ShaderName == \"shaders/skin.shader\" && !original.Name.EndsWith(\"/mouth.vmat\"))\r\n\t\t\treturn Material.Load(HeadMaterial(skin));\r\n\t\tif (original.Name.Contains(\"/bodies/\") && original.ShaderName == \"shaders/skin.shader\")\r\n\t\t{\r\n\t\t\t// The unified surface uses one body material family. Female chest normal maps\r\n\t\t\t// encode breast contours that become distorted on the flattened male endpoint.\r\n\t\t\tvar id = System.IO.Path.GetFileNameWithoutExtension(skin);\r\n\t\t\tbool female = !unifiedBody && (id is \"human02\" or \"human04\"), dark = id is \"human01\" or \"human02\" or \"human06\";\r\n\t\t\treturn Material.Load($\"models/citizen_human/bodies/{(female ? \"female\" : \"male\")}/{(female ? (dark ? \"female_body_mode_dark\" : \"female_body_mode_light\") : (dark ? \"body_mode_dark\" : \"body_male_light\"))}.vmat\");\r\n\t\t}\r\n\r\n\t\treturn original;\r\n\t}\r\n\r\n\tpublic static Color BodyToneTint(Material material, string skin)\r\n\t{\r\n\t\tif (!material.Name.Contains(\"/bodies/\"))\r\n\t\t\treturn Color.White;\r\n\t\treturn BodyToneTint(skin);\r\n\t}\r\n\r\n\tpublic static Color BodyToneTint(string skin)\r\n\t{\r\n\t\t// Albedo ratios of the native female/male materials sampled just inside\r\n\t\t// the shared neck boundary. Keep the female faces' authored complexion.\r\n\t\treturn System.IO.Path.GetFileNameWithoutExtension(skin) switch\r\n\t\t{\r\n\t\t\t\"human02\" => new Color(1.0192f, .9194f, .8924f),\r\n\t\t\t\"human04\" => new Color(1.0856f, 1.0137f, .8969f),\r\n\t\t\t_ => Color.White\r\n\t\t};\r\n\t}\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/CharacterMarkingData.cs",
"FileName": "CharacterMarkingData.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Text.Json.Serialization;\r\n\r\nnamespace CharacterEditor;\r\n/// <summary>One independently editable ink layer. Placement is normalized within a curated surface region.</summary>\r\npublic sealed class CharacterMarkingData\r\n{\r\n\tpublic string Style { get; set; } = \"crescent\";\r\n\tpublic string Region { get; set; } = \"face\";\r\n\tpublic float Horizontal { get; set; } = .5f;\r\n\tpublic float Vertical { get; set; } = .5f;\r\n\tpublic float Scale { get; set; } = .5f;\r\n\tpublic float Rotation { get; set; } = .5f;\r\n\tpublic float Spacing { get; set; } = .5f;\r\n\tpublic MakeupLayerData Ink { get; set; } = new()\r\n\t{\r\n\t\tRed = .1f,\r\n\t\tGreen = .12f,\r\n\t\tBlue = .16f,\r\n\t\tAmount = .8f\r\n\t};\r\n\r\n\t[JsonIgnore]\r\n\tpublic bool Valid => MarkingCatalog.ValidPlacement(Style, Region) && Ink?.Valid == true && new[]\r\n\t{\r\n\t\tHorizontal,\r\n\t\tVertical,\r\n\t\tScale,\r\n\t\tRotation,\r\n\t\tSpacing\r\n\t}.All(x => float.IsFinite(x) && x >= 0 && x <= 1) && (Style != \"barbed-band\" || (Horizontal == .5f && Rotation == .5f));\r\n\r\n\tpublic CharacterMarkingData DeepCopy() => new()\r\n\t{\r\n\t\tStyle = Style,\r\n\t\tRegion = Region,\r\n\t\tHorizontal = Horizontal,\r\n\t\tVertical = Vertical,\r\n\t\tScale = Scale,\r\n\t\tRotation = Rotation,\r\n\t\tSpacing = Spacing,\r\n\t\tInk = Ink?.DeepCopy()\r\n\t};\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/PhotoProfileAnchors.cs",
"FileName": "PhotoProfileAnchors.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "namespace CharacterEditor;\n// Generated by ce_calibrate_photo_profile from native topology and packed engine deformers.\npublic static class PhotoProfileAnchors\n{\n public static readonly int[] Ids={10,168,4,1,13,14,152};\n public static readonly string[] Controls={\"head\",\"jaw\",\"eyes\",\"nosewidth\",\"nostrils\",\"noseheight\",\"mouthwidth\",\"nose\",\"chin\",\"mouthprojection\",\"chinheight\"};\n public static readonly Vector3[][] Heads={\nnew Vector3[]{new(3.6992545f,-0.066790596f,68.14279f),new(5.2946215f,0.11305821f,64.60188f),new(5.959813f,-0.041913833f,63.655262f),new(6.1343274f,-0.058205567f,63.346245f),new(5.8273835f,0.07009718f,61.76828f),new(5.7244573f,0.09594756f,61.383133f),new(4.6132507f,-0.040823318f,59.63866f)},\nnew Vector3[]{new(3.6878302f,-0.014352961f,68.02986f),new(5.085779f,0.0712296f,64.49504f),new(5.841306f,-0.03587849f,63.486412f),new(6.0319343f,-0.045126636f,63.211014f),new(5.53363f,0.09146978f,61.719315f),new(5.485255f,0.1368592f,61.244686f),new(4.676547f,-0.012164734f,59.841347f)},\nnew Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2011194f,0.07334158f,64.609566f),new(5.979442f,-0.033484418f,63.572296f),new(6.1241097f,-0.03964384f,63.281433f),new(5.778057f,0.053285245f,61.74f),new(5.719415f,0.069648676f,61.50413f),new(4.7025223f,-0.06262799f,59.70981f)},\nnew Vector3[]{new(3.5149546f,-0.009116913f,68.08719f),new(5.0824428f,0.056152936f,64.4079f),new(5.857973f,-0.08385171f,63.312653f),new(6.0649567f,-0.10289183f,62.99997f),new(5.4969316f,0.05211438f,61.666157f),new(5.502199f,0.10385137f,61.29811f),new(4.67235f,-0.1021266f,59.774113f)},\nnew Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2132f,0.087325275f,64.6164f),new(5.957733f,0.064072326f,63.39234f),new(6.137288f,0.064377435f,63.1475f),new(5.6911297f,0.052945673f,61.71075f),new(5.713699f,0.06934876f,61.489025f),new(4.702329f,-0.055987295f,59.563744f)},\nnew Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.2012277f,0.1540686f,64.59866f),new(6.0016494f,-0.08192249f,63.62896f),new(6.165094f,-0.049669266f,63.36159f),new(5.7699647f,0.05389818f,61.78059f),new(5.719415f,0.06964957f,61.493782f),new(4.700662f,-0.0723621f,59.84928f)},\nnew Vector3[]{new(3.699734f,-0.0662729f,68.142685f),new(5.173374f,0.09990916f,64.60435f),new(5.9552946f,0.042667035f,63.536186f),new(6.1126857f,0.03899601f,63.296906f),new(5.6911297f,0.0529435f,61.727646f),new(5.713699f,0.07963325f,61.420948f),new(4.702329f,-0.05598729f,59.74437f)}\n };\n // [head * Controls.Length * 2 + control * 2 + positiveEndpoint]\n public static readonly Vector3[][] Deltas={\nnew Vector3[]{new(-0.13209844f,0.0031800792f,-0.07652283f),new(-0.24389982f,-0.0069441274f,-0.03074646f),new(-0.23990822f,0.0022496209f,-0.010990143f),new(-0.23487473f,0.0029714853f,-0.0055236816f),new(-0.23757076f,-0.003810294f,0.020923615f),new(-0.23392248f,-0.0052277073f,0.02753067f),new(-0.2026701f,0.0023912787f,0.061531067f)},\nnew Vector3[]{new(0.13209844f,-0.0031800792f,0.07652283f),new(0.24389982f,0.0069441274f,0.03074646f),new(0.23990822f,-0.0022496209f,0.010990143f),new(0.23487473f,-0.0029714853f,0.0055236816f),new(0.23757076f,0.003810294f,-0.020923615f),new(0.23392248f,0.0052277073f,-0.02753067f),new(0.2026701f,-0.0023912787f,-0.061531067f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.011735871f,0f),new(0f,0.016297556f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.011735871f,0f),new(0f,-0.016297556f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3449974f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.0107391365f,0f),new(0f,-0.0056006014f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.010739133f,0f),new(0f,0.0056006014f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.041153908f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.0532794f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.13788319f,0f,0f),new(-0.052534103f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.19303656f,0f,0f),new(0.07354784f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.47891617f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.31927872f)},\nnew Vector3[]{new(-0.1366136f,0.0007089311f,-0.07763672f),new(-0.24782324f,-0.004627906f,-0.03035736f),new(-0.2453003f,0.002008915f,-0.00851059f),new(-0.23964739f,0.0023905002f,-0.0034942627f),new(-0.24498606f,-0.0053994358f,0.02362442f),new(-0.23736048f,-0.007896334f,0.031650543f),new(-0.20918512f,0.0007255161f,0.05886841f)},\nnew Vector3[]{new(0.1366136f,-0.0007089311f,0.07763672f),new(0.24782324f,0.004627906f,0.03035736f),new(0.2453003f,-0.002008915f,0.00851059f),new(0.23964739f,-0.0023905002f,0.0034942627f),new(0.24498606f,0.0053994358f,-0.02362442f),new(0.23736048f,0.007896334f,-0.031650543f),new(0.20918512f,-0.0007255161f,-0.05886841f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.010045975f,0f),new(0f,0.012437068f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.010045975f,0f),new(0f,-0.012437068f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.33958435f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.33958435f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.018210128f,0f),new(0f,-0.0033276677f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.018210128f,0f),new(0f,0.0033276677f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4882121f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4882121f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.2155137f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.27901363f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.17917538f,0f,0f),new(-0.02188301f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.25084543f,0f,0f),new(0.03063631f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5400009f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3600006f)},\nnew Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.2449646f,-0.004605703f,-0.031585693f),new(-0.2398367f,0.001790762f,-0.009563446f),new(-0.23560143f,0.0020335242f,-0.004512787f),new(-0.2387433f,-0.002935592f,0.021694183f),new(-0.23645878f,-0.0038393363f,0.02576828f),new(-0.2047453f,0.0036357194f,0.059688568f)},\nnew Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.2449646f,0.004605703f,0.031585693f),new(0.2398367f,-0.001790762f,0.009563446f),new(0.23560143f,-0.0020335242f,0.004512787f),new(0.2387433f,0.002935592f,-0.021694183f),new(0.23645878f,0.0038393363f,-0.02576828f),new(0.2047453f,-0.0036357194f,-0.059688568f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009375636f,0f),new(0f,0.011100274f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009375636f,0f),new(0f,-0.011100274f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.008732874f,0f),new(0f,-0.008089323f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.008732874f,0f),new(0f,0.008089326f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.1130929f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.14641476f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.14750004f,0f,0f),new(-0.104530334f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.20650053f,0f,0f),new(0.14634228f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5328598f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.35523987f)},\nnew Vector3[]{new(-0.13274455f,0.0004590759f,-0.08004761f),new(-0.24919367f,-0.0036709309f,-0.028762817f),new(-0.24560404f,0.004687473f,-0.0054626465f),new(-0.238554f,0.0053960904f,0f),new(-0.24498224f,-0.0030967854f,0.02476883f),new(-0.23813248f,-0.0059928596f,0.03068924f),new(-0.20696688f,0.006031759f,0.059539795f)},\nnew Vector3[]{new(0.13274455f,-0.0004590759f,0.08004761f),new(0.24919367f,0.0036709309f,0.028762817f),new(0.24560404f,-0.004687473f,0.0054626465f),new(0.238554f,-0.0053960904f,0f),new(0.24498224f,0.0030967854f,-0.02476883f),new(0.23813248f,0.0059928596f,-0.03068924f),new(0.20696688f,-0.006031759f,-0.059539795f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,-5.4202974E-05f,0f),new(0f,0.02339752f,0f),new(0f,0.020724386f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,5.4202974E-05f,0f),new(0f,-0.02339752f,0f),new(0f,-0.020724379f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,-0.0011901855f),new(0f,0f,-0.34381104f),new(0f,0f,-0.24817657f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0.0011901855f),new(0f,0f,0.34381104f),new(0f,0f,0.24817657f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.01034468f,0f),new(0f,-0.0050033107f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.01034468f,0f),new(0f,0.0050033107f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(-0.001709938f,0f,0f),new(-0.49428988f,0f,0f),new(-0.3567996f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0.001709938f,0f,0f),new(0.49428988f,0f,0f),new(0.3567996f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.15790367f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.20442915f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.17864943f,0f,0f),new(-0.043359756f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.2501092f,0f,0f),new(0.060703754f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.53889465f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.35926437f)},\nnew Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.24469805f,-0.0054651946f,-0.03161621f),new(-0.24176359f,-0.0034667253f,-0.0066337585f),new(-0.23539543f,-0.0032922663f,-0.002357483f),new(-0.24088907f,-0.0029880553f,0.02273941f),new(-0.23634768f,-0.0038248226f,0.026042938f),new(-0.19970894f,0.0031703971f,0.060806274f)},\nnew Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.24469805f,0.0054651946f,0.03161621f),new(0.24176359f,0.0034667253f,0.0066337585f),new(0.23539543f,0.0032922626f,0.002357483f),new(0.24088907f,0.0029880553f,-0.02273941f),new(0.23634768f,0.0038248226f,-0.026042938f),new(0.19970894f,-0.0031703971f,-0.060806274f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.017940246f,0f),new(0f,-0.017056447f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.017940246f,0f),new(0f,0.017056443f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.32645035f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.32645035f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009510666f,0f),new(0f,-0.0076843873f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009510666f,0f),new(0f,0.0076843947f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4693303f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4693303f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.015622139f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.020225048f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.16166735f,0f,0f),new(-0.099727154f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.22633457f,0f,0f),new(0.13961792f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.4568138f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.30454254f)},\nnew Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.2450552f,-0.009678587f,-0.03138733f),new(-0.23850155f,0.004340723f,-0.010414124f),new(-0.23345089f,0.0025077388f,-0.005706787f),new(-0.2396326f,-0.002984602f,0.021102905f),new(-0.2362647f,-0.003836237f,0.025924683f),new(-0.2093401f,0.004296787f,0.05846405f)},\nnew Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.2450552f,0.009678587f,0.03138733f),new(0.23850155f,-0.004340723f,0.010414124f),new(0.23345089f,-0.0025077388f,0.005706787f),new(0.2396326f,0.002984602f,-0.021102905f),new(0.2362647f,0.003836237f,-0.025924683f),new(0.2093401f,-0.004296787f,-0.05846405f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.022938296f,0f),new(0f,0.013907395f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.022938296f,0f),new(0f,-0.013907395f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009057671f,0f),new(0f,-0.0077751838f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009057675f,0f),new(0f,0.00777518f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.22866583f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.29604006f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.15124607f,0f,0f),new(-0.10046959f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.21174479f,0f,0f),new(0.14065742f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5400009f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.3600006f)},\nnew Vector3[]{new(-0.13210607f,0.0031552091f,-0.0765152f),new(-0.24530983f,-0.006316647f,-0.031700134f),new(-0.24099922f,-0.002302207f,-0.0090408325f),new(-0.23603249f,-0.0020077005f,-0.004776001f),new(-0.24115801f,-0.0029912703f,0.022464752f),new(-0.23502398f,-0.004367456f,0.027061462f),new(-0.2059083f,0.0032688156f,0.05939865f)},\nnew Vector3[]{new(0.13210607f,-0.0031552091f,0.0765152f),new(0.24530983f,0.006316647f,0.031700134f),new(0.24099922f,0.002302207f,0.0090408325f),new(0.23603249f,0.0020077005f,0.004776001f),new(0.24115801f,0.0029912703f,-0.022464752f),new(0.23502398f,0.004367456f,-0.027061462f),new(0.2059083f,-0.0032688156f,-0.05939865f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.011946769f,0f),new(0f,-0.010918882f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0.011946768f,0f),new(0f,0.0109188855f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.34500122f),new(0f,0f,-0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.34500122f),new(0f,0f,0.34500122f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,-0.009599518f,0f),new(0f,-0.0062645525f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0.009599518f,0f),new(0f,0.0062645525f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(-0.4959998f,0f,0f),new(-0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0.4959998f,0f,0f),new(0.4959998f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.14241362f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.18437433f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(-0.16318464f,0f,0f),new(-0.07080078f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0.2284584f,0f,0f),new(0.099121094f,0f,0f),new(0f,0f,0f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,-0.5386009f)},\nnew Vector3[]{new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0f),new(0f,0f,0.359066f)}\n };\n}\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/WardrobeCatalog.Generated.cs",
"FileName": "WardrobeCatalog.Generated.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "namespace CharacterEditor;\r\npublic static partial class WardrobeCatalog\r\n{\r\n public static IReadOnlyList<WardrobeItem> Items=>entries??=CreateEntries();\r\n static WardrobeItem[] entries;\r\n static WardrobeItem[] CreateEntries()=>new WardrobeItem[]{\r\n new(\"Denim Skirt Black\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/dress/denim_skirt/denim_skirt_black.clothing\",\"LeftThigh, RightThigh, LeftKnee, RightKnee\",\"\",\"\",\"Skirt\"),\r\n new(\"Denim Skirt Blue\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/dress/denim_skirt/denim_skirt_blue.clothing\",\"LeftThigh, RightThigh, LeftKnee, RightKnee\",\"\",\"\",\"Skirt\"),\r\n new(\"Denim Skirt Green\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/dress/denim_skirt/denim_skirt_green.clothing\",\"LeftThigh, RightThigh, LeftKnee, RightKnee\",\"\",\"\",\"Skirt\"),\r\n new(\"Denim Skirt Leapard\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/dress/denim_skirt/denim_skirt_leapard.clothing\",\"LeftThigh, RightThigh, LeftKnee, RightKnee\",\"\",\"\",\"Skirt\"),\r\n new(\"Dotted Dress\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/dress/dotted_dress/dotted_dress.clothing\",\"Chest, Groin, LeftThigh, RightThigh\",\"\",\"Chest\",\"Dress\"),\r\n new(\"Grand Ball Gown\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/dress/posh_dress/models/posh_dress.clothing\",\"Chest, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Chest, Legs\",\"Dress\"),\r\n new(\"Simple Dress\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/dress/simple_dress/simple_dress.clothing\",\"Chest, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Chest\",\"Dress\"),\r\n new(\"Simple Dress Black\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/dress/simple_dress/simple_dress_black.clothing\",\"Chest, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Chest\",\"Dress\"),\r\n new(\"Simple Dress Blue\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/dress/simple_dress/simple_dress_blue.clothing\",\"Chest, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Chest\",\"Dress\"),\r\n new(\"Standard Skirt\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/dress/standard_skirt/standard_skirt.clothing\",\"LeftThigh, RightThigh, LeftKnee, RightKnee\",\"\",\"\",\"Skirt\"),\r\n new(\"Pirate Outfit\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/full_outfit/pirate_outfit/pirate_outfit.clothing\",\"Chest, LeftArm, RightArm, LeftWrist, RightWrist, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot\",\"Chest, LeftArm, RightArm, LeftWrist, RightWrist, LeftHand, RightHand, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot, Waist\",\"Chest, Legs, Feet\",\"Costume\"),\r\n new(\"Nerdy Glasses\",\"Glasses\",\"Glasses\",\"models/citizen_clothes/glasses/nerdy_glasses/nerdy_glasses.clothing\",\"Glasses\",\"\",\"\",\"GlassesEye\"),\r\n new(\"Party Glasses\",\"Glasses\",\"Glasses\",\"models/citizen_clothes/glasses/party_glasses/party_glasses.clothing\",\"Glasses\",\"\",\"\",\"GlassesSun\"),\r\n new(\"Boxing Gloves\",\"Gloves\",\"Gloves\",\"models/citizen_clothes/gloves/boxing_gloves/boxing_gloves.clothing\",\"LeftHand, RightHand\",\"\",\"Hands\",\"Gloves\"),\r\n new(\"Boxing Gloves Blue\",\"Gloves\",\"Gloves\",\"models/citizen_clothes/gloves/boxing_gloves/boxing_gloves_blue.clothing\",\"LeftHand, RightHand\",\"\",\"Hands\",\"Gloves\"),\r\n new(\"Boxing Gloves Red\",\"Gloves\",\"Gloves\",\"models/citizen_clothes/gloves/boxing_gloves/boxing_gloves_red.clothing\",\"LeftHand, RightHand\",\"\",\"Hands\",\"Gloves\"),\r\n new(\"Fingerless Gloves\",\"Gloves\",\"Gloves\",\"models/citizen_clothes/gloves/fingerless_gloves/fingerless_gloves.clothing\",\"LeftHand, RightHand\",\"\",\"Hands\",\"Gloves\"),\r\n new(\"Goalie Gloves\",\"Gloves\",\"Gloves\",\"models/citizen_clothes/gloves/goalie_gloves/goalie_gloves.clothing\",\"LeftHand, RightHand\",\"\",\"Hands\",\"Gloves\"),\r\n new(\"Blue Rubber Gloves\",\"Gloves\",\"Gloves\",\"models/citizen_clothes/gloves/rubber_gloves/rubber_gloves.clothing\",\"LeftHand, RightHand\",\"\",\"Hands\",\"Gloves\"),\r\n new(\"Black Rubber Gloves\",\"Gloves\",\"Gloves\",\"models/citizen_clothes/gloves/rubber_gloves/rubber_gloves_black.clothing\",\"LeftHand, RightHand\",\"\",\"Hands\",\"Gloves\"),\r\n new(\"Mask\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/balaclava/balaclava.clothing\",\"HeadTop, HeadBottom\",\"\",\"\",\"HatBeanie\"),\r\n new(\"Bandana Mask\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/bandana_mask/bandana_mask.clothing\",\"\",\"\",\"\",\"HeadSpecial\"),\r\n new(\"Baseball Cap\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/baseball_cap/baseball_cap.clothing\",\"HeadTop\",\"\",\"\",\"Hat\"),\r\n new(\"Baseball Cap Black\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/baseball_cap/baseball_cap_black.clothing\",\"HeadTop\",\"\",\"\",\"Hat\"),\r\n new(\"Baseball Cap Blue White\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/baseball_cap/baseball_cap_blue_white.clothing\",\"HeadTop\",\"\",\"\",\"Hat\"),\r\n new(\"Baseball Cap Brown\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/baseball_cap/baseball_cap_brown.clothing\",\"HeadTop\",\"\",\"\",\"Hat\"),\r\n new(\"Beanie\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/beanie/beanie.clothing\",\"HeadTop\",\"\",\"\",\"HatBeanie\"),\r\n new(\"Beanie Green\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/beanie/beanie_green.clothing\",\"HeadTop\",\"\",\"\",\"HatBeanie\"),\r\n new(\"Beanie Red\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/beanie/beanie_red.clothing\",\"HeadTop\",\"\",\"\",\"HatBeanie\"),\r\n new(\"Boxing Headgear\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/boxing_headgear/boxing_headgear.clothing\",\"HeadTop\",\"\",\"\",\"HatCostume\"),\r\n new(\"Boxing Headgear Blue\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/boxing_headgear/boxing_headgear_blue.clothing\",\"HeadTop\",\"\",\"\",\"HatCostume\"),\r\n new(\"Boxing Headgear Red\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/boxing_headgear/boxing_headgear_red.clothing\",\"HeadTop, HeadBottom\",\"\",\"\",\"HatCostume\"),\r\n new(\"Bucket Hat\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/bucket_hat/bucket_hat.clothing\",\"HeadTop\",\"\",\"\",\"Hat\"),\r\n new(\"Bucket Hat Black\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/bucket_hat/bucket_hat_black.clothing\",\"HeadTop\",\"\",\"\",\"Hat\"),\r\n new(\"Chef's Hat\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/chefs_hat/chefs_hat.clothing\",\"HeadTop\",\"\",\"\",\"HatUniform\"),\r\n new(\"Cowboy Hat\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/cowboy_hat/cowboy_hat.clothing\",\"HeadTop\",\"\",\"\",\"HatCostume\"),\r\n new(\"Fedora\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/fedora/fedora.clothing\",\"HeadTop\",\"\",\"\",\"HatFormal\"),\r\n new(\"Fedora Brown\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/fedora/fedora_brown.clothing\",\"HeadTop\",\"\",\"\",\"HatFormal\"),\r\n new(\"Flatcap\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/flatcap/flatcap.clothing\",\"HeadTop\",\"\",\"\",\"Hat\"),\r\n new(\"Gasmask\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/gasmask/gasmask.clothing\",\"HeadBottom\",\"\",\"\",\"HatUniform\"),\r\n new(\"Hair Net\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/hair_net/hair_net.clothing\",\"HeadTop\",\"\",\"\",\"HatUniform\"),\r\n new(\"Hair Net White\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/hair_net/hair_net_white.clothing\",\"HeadTop\",\"\",\"\",\"HatUniform\"),\r\n new(\"Hard Hat\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/hard_hat/hard_hat.clothing\",\"HeadTop\",\"\",\"\",\"HatUniform\"),\r\n new(\"Headphones\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/headphones/models/headphones.clothing\",\"\",\"\",\"\",\"HeadTech\"),\r\n new(\"Police Hat\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/police_hat/police_hat.clothing\",\"HeadTop\",\"\",\"\",\"HatUniform\"),\r\n new(\"Racer Helmet\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/racer_helmet/racer_helmet.clothing\",\"HeadTop\",\"\",\"\",\"HatCostume\"),\r\n new(\"Scifi Trooper Helmet\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/scifi_trooper_helmet/scifi_trooper_helmet.clothing\",\"HeadTop\",\"\",\"\",\"HatCostume\"),\r\n new(\"Skater Helmet\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/skater_helmet/skater_helmet.clothing\",\"HeadTop\",\"\",\"\",\"HatUniform\"),\r\n new(\"Skater Helmet Camo\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/skater_helmet/skater_helmet_camo.clothing\",\"HeadTop\",\"\",\"\",\"HatUniform\"),\r\n new(\"Skater Helmet White\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/skater_helmet/skater_helmet_white.clothing\",\"HeadTop\",\"\",\"\",\"HatUniform\"),\r\n new(\"Tactical Helmet\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/tactical_helmet/tactical_helmet.clothing\",\"HeadTop, Glasses\",\"\",\"\",\"HatUniform\"),\r\n new(\"Army Helmet\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/tactical_helmet/tactical_helmet_army.clothing\",\"HeadTop, Glasses\",\"\",\"\",\"HatUniform\"),\r\n new(\"Trapper Hat\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/trapper_hat/trapper_hat.clothing\",\"HeadTop\",\"\",\"\",\"HatBeanie\"),\r\n new(\"Trapper Hat Cloth\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/trapper_hat/trapper_hat_cloth.clothing\",\"HeadTop\",\"\",\"\",\"HatBeanie\"),\r\n new(\"War Helmet\",\"Hats\",\"Hats\",\"models/citizen_clothes/hat/war_helmet/war_helmet.clothing\",\"HeadTop\",\"\",\"\",\"HatUniform\"),\r\n new(\"Biker Jacket\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/biker_jacket/biker_jacket.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Jacket\"),\r\n new(\"Bomber Jacket\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/bomber_jacket/bomber_jacket.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Jacket\"),\r\n new(\"Black Leather Jacket\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/brown_leather_jacket/black_leather_jacket.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Jacket\"),\r\n new(\"Brown Leather Jacket\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/brown_leather_jacket/brown_leather_jacket.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Jacket\"),\r\n new(\"Hoodie\",\"Tops\",\"Tops\",\"models/citizen_clothes/jacket/hoodie/hoodie.clothing\",\"Chest\",\"LeftArm, RightArm\",\"Chest\",\"Hoodie\"),\r\n new(\"Black Hoodie\",\"Tops\",\"Tops\",\"models/citizen_clothes/jacket/hoodie/hoodie_black.clothing\",\"LeftArm, RightArm, LeftWrist, RightWrist\",\"Chest\",\"Chest\",\"Hoodie\"),\r\n new(\"Lab Coat\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/labcoat/labcoat.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Coat\"),\r\n new(\"Puffer Jacket\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/puffer_jacket/puffer_jacket.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Jacket\"),\r\n new(\"Puffer Jacket Blue\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/puffer_jacket/puffer_jacket_blue.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Jacket\"),\r\n new(\"Puffer Jacket Orange\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/puffer_jacket/puffer_jacket_orange.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Jacket\"),\r\n new(\"Rain Coat\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/raincoat/raincoat.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Coat\"),\r\n new(\"Sleeveless Jack\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/sleeveless_jacket/sleeveless_jacket.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Gilet\"),\r\n new(\"Wind Breaker\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/wind_breaker/wind_breaker.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Coat\"),\r\n new(\"Winter Coat\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jacket/winter_coat/winter_coat.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Coat\"),\r\n new(\"Cardigan\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jumper/cardigan/cardigan.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Cardigan\"),\r\n new(\"Cardigan Pink\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jumper/cardigan/cardigan_colored.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Cardigan\"),\r\n new(\"Cardigan Dark\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jumper/cardigan/cardigan_dark.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Cardigan\"),\r\n new(\"Cardigan Dirty\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/jumper/cardigan/cardigan_dirty.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Cardigan\"),\r\n new(\"Loose Jumper\",\"Tops\",\"Tops\",\"models/citizen_clothes/jumper/loose_jumper/loose_jumper.clothing\",\"Chest\",\"LeftArm, RightArm\",\"Chest\",\"Knitwear\"),\r\n new(\"Goalie Jumper\",\"Tops\",\"Tops\",\"models/citizen_clothes/jumper/loose_jumper/loose_jumper_goalie.clothing\",\"Chest\",\"LeftArm, RightArm\",\"Chest\",\"Knitwear\"),\r\n new(\"Loose Jumper Wooly\",\"Tops\",\"Tops\",\"models/citizen_clothes/jumper/loose_jumper/loose_jumper_wooly.clothing\",\"Chest\",\"LeftArm, RightArm\",\"Chest\",\"Knitwear\"),\r\n new(\"Sleeveless Jumper\",\"Tops\",\"Tops\",\"models/citizen_clothes/jumper/sleeveless_jumper/sleeveless_jumper.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Knitwear\"),\r\n new(\"Sleeveless Jumper Grey\",\"Tops\",\"Tops\",\"models/citizen_clothes/jumper/sleeveless_jumper/sleeveless_jumper_grey.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Knitwear\"),\r\n new(\"Sleeveless Jumper Red\",\"Tops\",\"Tops\",\"models/citizen_clothes/jumper/sleeveless_jumper/sleeveless_jumper_red.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Knitwear\"),\r\n new(\"Pearl Necklace\",\"Accessories\",\"necklace\",\"models/citizen_clothes/necklace/pearl_necklace/pearl_necklace.clothing\",\"Chin\",\"\",\"\",\"NecklacePendant\"),\r\n new(\"Vintage Necklace\",\"Accessories\",\"necklace\",\"models/citizen_clothes/necklace/vintage_necklace/vintage_necklace.clothing\",\"Chin\",\"\",\"\",\"NecklacePendant\"),\r\n new(\"Santa Hat 2024\",\"Hats\",\"Hats\",\"models/citizen_clothes/seasonal/2024/santa/santahat.clothing\",\"HeadTop\",\"\",\"\",\"HatCostume\"),\r\n new(\"Santa Jacket 2024\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/seasonal/2024/santa/santajacket.clothing\",\"LeftArm, RightArm\",\"LeftArm, RightArm\",\"Chest\",\"Coat\"),\r\n new(\"Santa Trousers 2024\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/seasonal/2024/santa/santatrousers.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot\",\"Legs, Feet\",\"Trousers\"),\r\n new(\"Easter Bonnet\",\"Hats\",\"Hats\",\"models/citizen_clothes/seasonal/2025/easter_farmer/easter_bonnet/easter_bonnet.clothing\",\"HeadTop\",\"\",\"\",\"HatCostume\"),\r\n new(\"Easter Dungarees\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/seasonal/2025/easter_farmer/easter_dungarees/models/easter_dungarees.clothing\",\"Chest, LeftArm, RightArm, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"Chest, LeftArm, RightArm, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"Chest, Legs\",\"Suit\"),\r\n new(\"Easter Slippers\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/seasonal/2025/easter_farmer/easter_slippers/models/easter_slippers.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Footwear\"),\r\n new(\"Valentines Bear Head 2025\",\"Hats\",\"Hats\",\"models/citizen_clothes/seasonal/2025/valentines_bear/bear_head.clothing\",\"HeadTop, HeadBottom\",\"HeadTop, HeadBottom\",\"Head\",\"HatCostume\"),\r\n new(\"Valentines Bear Top 2025\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/seasonal/2025/valentines_bear/bear_top.clothing\",\"Chest, LeftArm, RightArm, LeftWrist, RightWrist, LeftHand, RightHand\",\"LeftArm, RightArm, LeftWrist, RightWrist, LeftHand, RightHand\",\"Chest, Hands\",\"Costume\"),\r\n new(\"Valentines Bear Trousers 2025\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/seasonal/2025/valentines_bear/bear_trousers.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot\",\"Legs, Feet\",\"Trousers\"),\r\n new(\"Wizard Beard\",\"Accessories\",\"costume_beard\",\"models/citizen_clothes/seasonal/2025/wizard_outfit/wizard_beard/wizard_beard.clothing\",\"Face\",\"\",\"\",\"FacialHairBeard\"),\r\n new(\"Wizard Gown\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/seasonal/2025/wizard_outfit/wizard_gown/wizard_gown.clothing\",\"Chest, LeftArm, RightArm\",\"Chest, LeftArm, RightArm, LeftWrist\",\"Chest\",\"Coat\"),\r\n new(\"Wizard Hat\",\"Hats\",\"Hats\",\"models/citizen_clothes/seasonal/2025/wizard_outfit/wizard_hat/wizard_hat.clothing\",\"HeadTop\",\"\",\"\",\"HatCostume\"),\r\n new(\"Wizard Trousers\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/seasonal/2025/wizard_outfit/wizard_trousers/wizard_trousers.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot\",\"\",\"Legs, Feet\",\"Trousers\"),\r\n new(\"Army Shirt\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/army_shirt/army_shirt.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Shirt\"),\r\n new(\"Chef Shirt\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/chef_shirt/chef_shirt.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Shirt\"),\r\n new(\"Fisherman Jumpsuit\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/shirt/fisherman_jumpsuit/fisherman_jumpsuit.clothing\",\"Chest, LeftArm, RightArm, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin, LeftFoot, RightFoot\",\"\",\"Chest, Legs, Feet\",\"Suit\"),\r\n new(\"Flannel Shirt\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/flannel_shirt/flannel_shirt.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Shirt\"),\r\n new(\"Blue Shirt\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/flannel_shirt/variations/blue_shirt/blue_shirt.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Shirt\"),\r\n new(\"Hawaiian Shirt\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/hawaiian_shirt/hawaiian shirt.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Shirt\"),\r\n new(\"Blue Jumpsuit\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/shirt/jumpsuit/blue_jumpsuit.clothing\",\"Chest, LeftArm, RightArm, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Chest, Legs\",\"Suit\"),\r\n new(\"Prison Jumpsuit\",\"Outfits\",\"Outfits\",\"models/citizen_clothes/shirt/jumpsuit/prison_jumpsuit.clothing\",\"Chest, LeftArm, RightArm, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Chest, Legs\",\"Suit\"),\r\n new(\"Leotard\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/leotard/leotard.clothing\",\"Chest, LeftArm, RightArm, LeftWrist, RightWrist, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"\",\"TShirt\"),\r\n new(\"Leotard Blue\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/leotard/leotard_blue.clothing\",\"Chest, LeftArm, RightArm, LeftWrist, RightWrist, Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"\",\"Shirt\"),\r\n new(\"Loose Crop Top\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/loose_crop_top/loose_crop_top.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Shirt\"),\r\n new(\"Loose Crop Top Pattern\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/loose_crop_top/loose_crop_top_pattern.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Shirt\"),\r\n new(\"Loose Crop Top Purple\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/loose_crop_top/loose_crop_top_purple.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Shirt\"),\r\n new(\"Loose Crop Top White\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/loose_crop_top/loose_crop_top_white.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Shirt\"),\r\n new(\"Loose Shirt\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/loose_shirt/loose_shirt.clothing\",\"Chest, LeftArm, RightArm, LeftWrist, RightWrist\",\"\",\"Chest\",\"TShirt\"),\r\n new(\"Polo Shirt\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/polo_shirt/polo_shirt.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"TShirt\"),\r\n new(\"Polo Shirt White\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/polo_shirt/polo_shirt_white.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"TShirt\"),\r\n new(\"Tank Top \",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/tanktop/tanktop.clothing\",\"Chest\",\"\",\"\",\"TShirt\"),\r\n new(\"Football Tshirt\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/tshirt/football_tshirt.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"TShirt\"),\r\n new(\"Tshirt\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/tshirt/tshirt.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Tops\"),\r\n new(\"Waistcoat and Shirt\",\"Tops\",\"Tops\",\"models/citizen_clothes/shirt/waistcoat_and_shirt/waistcoat_and_shirt.clothing\",\"Chest, LeftArm, RightArm\",\"\",\"Chest\",\"Shirt\"),\r\n new(\"Boot Heels Black\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/boot_heels/boot_heels_black.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Heels\"),\r\n new(\"Boot Heels Brown\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/boot_heels/boot_heels_brown.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Heels\"),\r\n new(\"Boot Heels Leopard\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/boot_heels/boot_heels_leopard.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Heels\"),\r\n new(\"Boot Heels Red\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/boot_heels/boot_heels_red.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Heels\"),\r\n new(\"Army Boots\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/boots/army_boots.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Boots\"),\r\n new(\"Black Boots\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/boots/black_boots.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Boots\"),\r\n new(\"Boxing Boots\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/boxing_boots/boxing_boots.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Boots\"),\r\n new(\"Boxing Boots Blue\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/boxing_boots/boxing_boots_blue.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Boots\"),\r\n new(\"Clogs and Socks\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/crocs_and_socks/clogs_and_socks.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Slippers\"),\r\n new(\"Flat Shoes\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/flatshoes/textures/flat_shoes.clothing\",\"LeftFoot, RightFoot\",\"\",\"\",\"Shoes\"),\r\n new(\"Football Shoes\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/football_shoes/football_shoes.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Trainers\"),\r\n new(\"Slippers\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/slippers/slippers.clothing\",\"LeftFoot, RightFoot\",\"\",\"\",\"Slippers\"),\r\n new(\"Blue Sneakers\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/sneakers/blue_sneakers.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Shoes\"),\r\n new(\"Sneakers\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/sneakers/sneakers.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Shoes\"),\r\n new(\"Trainers\",\"Shoes\",\"Shoes\",\"models/citizen_clothes/shoes/trainers/trainers.clothing\",\"LeftFoot, RightFoot\",\"\",\"Feet\",\"Trainers\"),\r\n new(\"Boxing Shorts\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/shorts/boxing_shorts/boxing_shorts.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Shorts\"),\r\n new(\"Boxing Shorts Blue\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/shorts/boxing_shorts/boxing_shorts_blue.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Shorts\"),\r\n new(\"Boxing Shorts Red\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/shorts/boxing_shorts/boxing_shorts_red.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Shorts\"),\r\n new(\"Football Shorts\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/shorts/football_shorts/football_shorts.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Shorts\"),\r\n new(\"Shorts\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/shorts/shorts/shorts.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Shorts\"),\r\n new(\"Summer Shorts\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/shorts/summer_shorts/summer shorts.clothing\",\"LeftThigh, RightThigh\",\"\",\"Legs\",\"Shorts\"),\r\n new(\"Bin Man Trousers\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/trousers/cargopants/binman_trousers.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Trousers\"),\r\n new(\"Brown Cargo Pants\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/trousers/cargopants/brown_cargo_pants.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Trousers\"),\r\n new(\"Army Cargo Pants\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/trousers/cargopants/cargo_pants_army.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Trousers\"),\r\n new(\"Jeans\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/trousers/jeans/jeans.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Jeans\"),\r\n new(\"Black Jeans\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/trousers/jeans/jeans_black.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Jeans\"),\r\n new(\"Loose Jeans\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/trousers/loosejeans/loose_jeans.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Jeans\"),\r\n new(\"Loose Trousers\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/trousers/loosetrousers/loose_trousers.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Trousers\"),\r\n new(\"Smart Trousers\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/trousers/smarttrousers/trousers.smart.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Trousers\"),\r\n new(\"Trackie Bottoms\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/trousers/trackiebottoms/trackie_bottoms.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Trousers\"),\r\n new(\"Black Trackie Bottoms\",\"Bottoms\",\"Bottoms\",\"models/citizen_clothes/trousers/trackiebottoms/trackie_bottoms_black.clothing\",\"Groin, LeftThigh, RightThigh, LeftKnee, RightKnee, LeftShin, RightShin\",\"\",\"Legs\",\"Trousers\"),\r\n new(\"Chef's Apron\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/vest/chefs_apron/chefs_apron.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Vest\"),\r\n new(\"Tactical Vest\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/vest/tactical_vest/models/tactical_vest.clothing\",\"\",\"LeftArm, RightArm\",\"\",\"Vest\"),\r\n new(\"Army Vest\",\"Outerwear\",\"Outerwear\",\"models/citizen_clothes/vest/tactical_vest/models/tactical_vest_army.clothing\",\"LeftArm, RightArm\",\"\",\"\",\"Vest\"),\r\n new(\"Modern Watch\",\"Accessories\",\"watch\",\"models/citizen_clothes/watch/modern_watch/modern_watch.clothing\",\"LeftWrist\",\"\",\"\",\"WristWatch\"),\r\n new(\"Black Modern Watch\",\"Accessories\",\"watch\",\"models/citizen_clothes/watch/modern_watch/modern_watch_black.clothing\",\"LeftWrist\",\"\",\"\",\"WristWatch\")\r\n };\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "CharacterKit/EyeColorPresentation.cs",
"FileName": "EyeColorPresentation.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\n\r\nnamespace CharacterEditor;\r\n/// <summary>Changes only the iris albedo of the curated native human eyes.</summary>\r\npublic sealed class EyeColorPresentation : IDisposable\r\n{\r\n\tstatic ComputeShader compositor;\r\n\tstatic Material carrier;\r\n\treadonly Dictionary<int, Material> copies = new();\r\n\treadonly Dictionary<int, Texture> textures = new();\r\n\tSkinnedModelRenderer target;\r\n\tModel model;\r\n\tstring group;\r\n\tColor? applied;\r\n\tCharacterAppearanceData requested;\r\n\tDateTime started, nextRefresh;\r\n\tpublic static async System.Threading.Tasks.Task EnsureLoadedAsync()\r\n\t{\r\n\t\tcarrier ??= await Material.LoadAsync(\"materials/makeup/iris_compositor.vmat\");\r\n\t\tif (carrier?.ShaderName != \"shaders/human_iris.shader\" || !carrier.Shader.IsValid || !carrier.Shader.Schema.Variables.Any(x => x.Name == \"IrisColor\"))\r\n\t\t{\r\n\t\t\tcarrier = null;\r\n\t\t\tthrow new InvalidOperationException(\"The human iris shader is unavailable.\");\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Apply(SkinnedModelRenderer body, CharacterAppearanceData data, bool refresh = false)\r\n\t{\r\n\t\tif (!body.IsValid() || body.Model is null)\r\n\t\t\treturn;\r\n\t\tif (target != body || model != body.Model || group != body.MaterialGroup)\r\n\t\t{\r\n\t\t\tDispose();\r\n\t\t\ttarget = body;\r\n\t\t\tmodel = body.Model;\r\n\t\t\tgroup = body.MaterialGroup;\r\n\t\t\tapplied = null;\r\n\t\t}\r\n\r\n\t\tif (data?.CustomEyeColor != true)\r\n\t\t{\r\n\t\t\tforeach (var i in copies.Keys)\r\n\t\t\t\tbody.Materials.SetOverride(i, null);\r\n\t\t\tDispose();\r\n\t\t\tapplied = null;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar color = data.EyeColor;\r\n\t\trequested = data.DeepCopy();\r\n\t\tif (applied != color)\r\n\t\t{\r\n\t\t\tstarted = DateTime.UtcNow;\r\n\t\t\tnextRefresh = started.AddMilliseconds(250);\r\n\t\t}\r\n\r\n\t\tfor (var i = 0; i < body.Materials.Count; i++)\r\n\t\t{\r\n\t\t\tvar original = body.Materials.GetOriginal(i);\r\n\t\t\tif (original?.ShaderName != \"shaders/eyeball.shader\")\r\n\t\t\t\tcontinue;\r\n\t\t\tif (!copies.TryGetValue(i, out var copy))\r\n\t\t\t\tcopies[i] = copy = original.CreateCopy($\"fate-iris-{body.Id}-{i}.vmat\");\r\n\t\t\tif (refresh || applied != color || !textures.ContainsKey(i))\r\n\t\t\t{\r\n\t\t\t\tvar source = original.GetTexture(\"g_tColor\");\r\n\t\t\t\tsource.MarkUsed(Math.Max(source.Width, source.Height));\r\n\t\t\t\tif (!textures.TryGetValue(i, out var texture))\r\n\t\t\t\t{\r\n\t\t\t\t\ttexture = Texture.Create(source.Width, source.Height).WithFormat(ImageFormat.RGBA8888).WithUAVBinding().WithName(\"Local human iris\").Finish();\r\n\t\t\t\t\ttextures[i] = texture;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tvar attributes = new RenderAttributes();\r\n\t\t\t\tattributes.Set(\"BaseColor\", source);\r\n\t\t\t\tattributes.Set(\"IrisColor\", new Vector4(color.r, color.g, color.b, 1));\r\n\t\t\t\tattributes.Set(\"Result\", texture);\r\n\t\t\t\tcompositor ??= new(\"shaders/human_iris.shader\");\r\n\t\t\t\tcompositor.DispatchWithAttributes(attributes, source.Width, source.Height, 1);\r\n\t\t\t\tcopy.Set(\"g_tColor\", texture);\r\n\t\t\t}\r\n\r\n\t\t\tbody.Materials.SetOverride(i, copy);\r\n\t\t}\r\n\r\n\t\tapplied = color;\r\n\t}\r\n\r\n\tpublic void Refresh()\r\n\t{\r\n\t\tif (!target.IsValid() || requested?.CustomEyeColor != true)\r\n\t\t\treturn;\r\n\t\tfor (var i = 0; i < target.Materials.Count; i++)\r\n\t\t{\r\n\t\t\tvar material = target.Materials.GetOriginal(i);\r\n\t\t\tif (material?.ShaderName == \"shaders/eyeball.shader\" && material.GetTexture(\"g_tColor\")is Texture source)\r\n\t\t\t\tsource.MarkUsed(Math.Max(source.Width, source.Height));\r\n\t\t}\r\n\r\n\t\tvar now = DateTime.UtcNow;\r\n\t\tif ((now - started).TotalSeconds < 3 && now >= nextRefresh)\r\n\t\t{\r\n\t\t\tApply(target, requested, true);\r\n\t\t\tnextRefresh = now.AddMilliseconds(250);\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Dispose()\r\n\t{\r\n\t\tforeach (var texture in textures.Values)\r\n\t\t\ttexture.Dispose();\r\n\t\ttextures.Clear();\r\n\t\tcopies.Clear();\r\n\t\tapplied = null;\r\n\t\trequested = null;\r\n\t}\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/ClothingResources.cs",
"FileName": "ClothingResources.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Threading.Tasks;\r\n\r\nnamespace CharacterEditor;\r\n\r\n/// <summary>Native paths and portable cloud identities. Assets remain owned by their original packages.</summary>\r\npublic static class ClothingResources\r\n{\r\n\tstatic readonly Dictionary<string, Clothing> loaded = new(StringComparer.OrdinalIgnoreCase);\r\n\tstatic readonly Dictionary<string, Task<Clothing>> pending = new(StringComparer.OrdinalIgnoreCase);\r\n\tstatic readonly Dictionary<string, Task> iconLoads = new(StringComparer.OrdinalIgnoreCase);\n\tstatic readonly System.Threading.SemaphoreSlim iconRequests = new(3);\n\tstatic readonly Dictionary<string, string> cloudIcons = new(StringComparer.OrdinalIgnoreCase);\r\n\r\n\tpublic static int Revision { get; private set; }\r\n\r\n\tpublic static bool IsCloud(string reference) => reference?.StartsWith(\"cloud:\", StringComparison.OrdinalIgnoreCase) == true;\r\n\r\n\tpublic static bool ValidReference(string reference)\r\n\t{\r\n\t\tif (string.IsNullOrWhiteSpace(reference) || reference.Length > 256 || reference.Contains(\"..\")) return false;\r\n\t\tif (!IsCloud(reference)) return reference.EndsWith(\".clothing\", StringComparison.OrdinalIgnoreCase);\r\n\t\tvar ident = reference[6..];\r\n\t\treturn ident.Split('.').Length == 2 && ident.Split('.').All(x => x.Length > 0) && ident.All(c => char.IsAsciiLetterOrDigit(c) || c is '.' or '_' or '-');\r\n\t}\r\n\r\n\tpublic static Clothing Get(string reference) => reference is null ? null : loaded.TryGetValue(reference, out var item) ? item : ResourceLibrary.Get<Clothing>(reference);\r\n\r\n\tpublic static string Reference(Clothing item)\r\n\t{\r\n\t\tvar cloud = loaded.FirstOrDefault(x => IsCloud(x.Key) && x.Value == item).Key;\r\n\t\tif (cloud is not null) return cloud;\r\n\t\tvar definition = item.SteamItemDefinitionId is { } id ? Sandbox.Services.Inventory.FindDefinition(id) : null;\r\n\t\treturn !string.IsNullOrEmpty(definition?.PackageIdent) ? \"cloud:\" + definition.PackageIdent : item.ResourcePath;\r\n\t}\r\n\r\n\tpublic static Task<Clothing> Load(string reference)\r\n\t{\r\n\t\tif (!ValidReference(reference)) return Task.FromException<Clothing>(new InvalidOperationException(\"Use a .clothing asset path or cloud:creator.item reference.\"));\r\n\t\tif (Get(reference) is { } existing)\r\n\t\t{\r\n\t\t\tif (!SupportsHuman(existing)) return Task.FromException<Clothing>(new InvalidOperationException(\"This item does not have a human model.\"));\r\n\t\t\treturn Finish(reference, existing);\r\n\t\t}\r\n\t\tif (pending.TryGetValue(reference, out var task) && !task.IsFaulted && !task.IsCanceled) return task;\r\n\t\treturn pending[reference] = LoadMissing(reference);\r\n\t}\r\n\r\n\tstatic async Task<Clothing> LoadMissing(string reference)\r\n\t{\r\n\t\tif (!ValidReference(reference)) throw new InvalidOperationException(\"Invalid clothing reference.\");\r\n\t\tvar item = IsCloud(reference) ? await Cloud.Load<Clothing>(reference[6..]) : await ResourceLibrary.LoadAsync<Clothing>(reference);\r\n\t\tif (item is null || item.IsError) throw new InvalidOperationException(\"Clothing unavailable: \" + reference);\r\n\t\tif (!SupportsHuman(item))\r\n\t\t\tthrow new InvalidOperationException(\"This item does not have a human model.\");\r\n\t\tloaded[reference] = item;\r\n\t\tRevision++;\r\n\t\treturn await Finish(reference, item);\r\n\t}\r\n\r\n\tstatic async Task<Clothing> Finish(string reference, Clothing item)\r\n\t{\r\n\t\tif (IsCloud(reference) && !LocalIconExists(item) && !cloudIcons.ContainsKey(reference))\r\n\t\t{\r\n\t\t\tvar definition = item.SteamItemDefinitionId is { } id ? Sandbox.Services.Inventory.FindDefinition(id) : null;\r\n\t\t\tvar icon = definition?.IconUrl;\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tif (string.IsNullOrEmpty(icon)) icon = (await Package.FetchAsync(reference[6..], partial: true))?.ThumbWide;\r\n\t\t\t}\r\n\t\t\tcatch { /* A preview failure must not prevent wearing a downloaded item. */ }\r\n\t\t\tcloudIcons[reference] = icon ?? \"\";\r\n\t\t\tcloudIcons[item.ResourcePath] = icon ?? \"\";\r\n\t\t\tRevision++;\r\n\t\t}\r\n\t\treturn item;\r\n\t}\r\n\r\n\tstatic bool LocalIconExists(Clothing item) => !string.IsNullOrEmpty(item.Icon.Path) &&\r\n\t\t(item.Icon.Path.StartsWith(\"https://\", StringComparison.OrdinalIgnoreCase) || FileSystem.Mounted.FileExists(item.Icon.Path));\r\n\r\n\tpublic static string Icon(string reference)\n\t{\n\t\tvar item = Get(reference);\n\t\tif (item is not null && LocalIconExists(item)) return item.Icon.Path;\n\t\tif (reference is not null && cloudIcons.TryGetValue(reference, out var icon)) return icon;\n\t\tif (item is null) return \"\";\n\t\tif (cloudIcons.TryGetValue(item.ResourcePath, out icon)) return icon;\n\t\tif (!iconLoads.TryGetValue(item.ResourcePath, out var request) || request.IsCompleted)\n\t\t\ticonLoads[item.ResourcePath] = RecoverIcon(item);\n\t\treturn \"\";\n\t}\n\n\tstatic async Task RecoverIcon(Clothing item)\n\t{\n\t\tawait iconRequests.WaitAsync();\n\t\ttry\n\t\t{\n\t\t\tvar reference = Reference(item);\n\t\t\tif (IsCloud(reference))\n\t\t\t{\n\t\t\t\tawait Finish(reference, item);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Previously mounted clothing can appear as a local path without a session\n\t\t\t// alias. Match the package's primary asset, never just its display name.\n\t\t\tvar definition = Sandbox.Services.Inventory.Definitions.FirstOrDefault(d => string.Equals(d.Asset, item.ResourcePath, StringComparison.OrdinalIgnoreCase));\n\t\t\tif (!string.IsNullOrEmpty(definition?.PackageIdent))\n\t\t\t{\n\t\t\t\tRegisterPreview(item, definition.PackageIdent, definition.IconUrl);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tvar words = string.Join(\" \", (item.Title ?? \"\").Split(' ', StringSplitOptions.RemoveEmptyEntries).Where(x => !x.Contains(':')));\n\t\t\tif (words.Length == 0) return;\n\t\t\tvar found = await Package.FindAsync(words + \" type:clothing\", 16);\n\t\t\tforeach (var candidate in found.Packages.OrderByDescending(p => string.Equals(p.Title, item.Title, StringComparison.OrdinalIgnoreCase)))\n\t\t\t{\n\t\t\t\tvar package = await Package.FetchAsync(candidate.FullIdent, partial: false);\n\t\t\t\tif (!string.Equals(package?.PrimaryAsset?.TrimStart('/'), item.ResourcePath.TrimStart('/'), StringComparison.OrdinalIgnoreCase)) continue;\n\t\t\t\tRegisterPreview(item, package.FullIdent, package.ThumbWide);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tcatch { /* Keep the picker usable when hosted previews are unavailable. */ }\n\t\tfinally\n\t\t{\n\t\t\tcloudIcons.TryAdd(item.ResourcePath, \"\");\n\t\t\tRevision++;\n\t\t\ticonRequests.Release();\n\t\t}\n\t}\n\n\tstatic void RegisterPreview(Clothing item, string ident, string icon)\n\t{\n\t\tloaded[\"cloud:\" + ident] = item;\n\t\tcloudIcons[item.ResourcePath] = icon ?? \"\";\n\t\tcloudIcons[\"cloud:\" + ident] = icon ?? \"\";\n\t}\n\r\n\tpublic static bool IsHair(Clothing c) => c.Category.ToString().StartsWith(\"Hair\");\r\n\r\n\tpublic static bool IsFacialHair(Clothing c) => c.Category.ToString().StartsWith(\"FacialHair\");\r\n\r\n\tpublic static bool IsBaseSkin(Clothing c) => c.HasHumanSkin && string.IsNullOrEmpty(c.HumanAltModel) && string.IsNullOrEmpty(c.HumanAltFemaleModel);\r\n\r\n\tstatic bool SupportsHuman(Clothing c) => c.HasHumanSkin || !string.IsNullOrEmpty(c.HumanAltModel) || !string.IsNullOrEmpty(c.HumanAltFemaleModel);\r\n\r\n\tpublic static WardrobeItem Describe(string reference, Clothing item)\r\n\t{\r\n\t\tvar category = Category(item.Category.ToString());\r\n\t\treturn new(item.Title, category, category, reference, item.SlotsUnder.ToString(), item.SlotsOver.ToString(), item.HideBody.ToString(), item.Category.ToString());\r\n\t}\r\n\tstatic string Category(string c) => c switch\r\n\t{\r\n\t\t\"Tops\" or \"TShirt\" or \"Sweatshirt\" or \"Hoodie\" or \"Shirt\" or \"Knitwear\" => \"Tops\",\r\n\t\t\"Bottoms\" or \"Shorts\" or \"Trousers\" or \"Jeans\" or \"Skirt\" or \"Underpants\" => \"Bottoms\",\r\n\t\t\"Footwear\" or \"Heels\" or \"Sandals\" or \"Shoes\" or \"Trainers\" or \"Boots\" or \"Slippers\" or \"Socks\" => \"Shoes\",\r\n\t\t\"Vest\" or \"Jacket\" or \"Cardigan\" or \"Coat\" or \"Gilet\" => \"Outerwear\",\r\n\t\t\"Fullbody\" or \"Dress\" or \"Suit\" or \"Costume\" or \"Uniform\" => \"Outfits\",\r\n\t\t\"Gloves\" => \"Gloves\",\r\n\t\t_ when c.StartsWith(\"Glasses\") || c == \"Eyewear\" => \"Glasses\",\r\n\t\t_ when c.StartsWith(\"Hat\") || c == \"Headwear\" => \"Hats\",\r\n\t\t_ when c.StartsWith(\"FacialHair\") => \"Facial hair\",\r\n\t\t_ when c.StartsWith(\"Hair\") => \"Hair\",\r\n\t\t_ => \"Accessories\"\r\n\t};\r\n\r\n\t/// <summary>Project clothing is discovered automatically; cloud choices are kept for the session.</summary>\r\n\r\n\tpublic static IEnumerable<WardrobeItem> Available => WardrobeCatalog.Items\r\n\t\t.Concat(loaded.Where(x => !IsBaseSkin(x.Value)).Select(x => Describe(x.Key, x.Value)))\r\n\t\t.Concat(ResourceLibrary.GetAll<Clothing>().Where(c => !IsBaseSkin(c) && (!string.IsNullOrEmpty(c.HumanAltModel) || !string.IsNullOrEmpty(c.HumanAltFemaleModel)))\r\n\t\t\t.Where(c => !loaded.Values.Contains(c)).Select(c => Describe(Reference(c), c)))\n\t\t.DistinctBy(x => x.Path);\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/PhotoFitAnchors.cs",
"FileName": "PhotoFitAnchors.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "namespace CharacterEditor;\r\n// Generated by the companion authoring tools/face-transfer/calibrate_photo_fit.py; approximate rest-space anchors.\r\npublic static class PhotoFitAnchors\r\n{\r\n public static readonly string[] Keys={\"faceHeight\",\"jawWidth\",\"eyeSeparation\",\"leftEyeWidth\",\"rightEyeWidth\",\"noseWidth\",\"noseLength\",\"mouthWidth\"};\r\n public static readonly float[][] Ratios={\r\n new float[]{1.44567f,0.8893f,0.18852f,0.27444f,0.25628f,0.26924f,0.26822f,0.50607f},\r\n new float[]{1.39547f,0.87962f,0.16641f,0.28168f,0.26739f,0.22625f,0.27752f,0.47095f},\r\n new float[]{1.43572f,0.89854f,0.17047f,0.25843f,0.25695f,0.28462f,0.27722f,0.45054f},\r\n new float[]{1.4587f,0.8663f,0.16107f,0.30609f,0.27849f,0.22656f,0.30262f,0.43632f},\r\n new float[]{1.44561f,0.89132f,0.16515f,0.25603f,0.25612f,0.19936f,0.30243f,0.43829f},\r\n new float[]{1.41067f,0.89818f,0.17149f,0.26097f,0.25952f,0.26348f,0.25932f,0.4242f},\r\n new float[]{1.41915f,0.89606f,0.16683f,0.25769f,0.25657f,0.22921f,0.28683f,0.42675f}\r\n };\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/SkinColorPresentation.cs",
"FileName": "SkinColorPresentation.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\n\r\nnamespace CharacterEditor;\r\n/// <summary>Per-renderer complexion and cosmetics retain native shading; optional iris color applies separately.</summary>\r\npublic sealed class SkinColorPresentation\r\n{\r\n\treadonly Dictionary<int, Material> copies = new();\r\n\tSkinnedModelRenderer target;\r\n\tModel model;\r\n\tstring group;\r\n\tColor? applied;\r\n\tbool normalized;\r\n\tstring makeupSignature;\r\n\tTexture appliedNeck;\r\n\treadonly Dictionary<int, HumanMakeupPresentation> makeupCopies = new();\r\n\t// These native materials contain a darker photographed base than the other curated faces.\r\n\t// Correct their base when a palette/custom color is chosen, leaving Native/legacy looks exact.\r\n\tstatic readonly HashSet<string> DarkBases = new(StringComparer.OrdinalIgnoreCase)\r\n\t{\r\n\t\t\"models/citizen_human/bodies/male/body_mode_dark.vmat\",\r\n\t\t\"models/citizen_human/bodies/female/female_body_mode_dark.vmat\",\r\n\t\t\"models/citizen_human/heads/citizen_human_malehead01/citizen_human_malehead01.vmat\",\r\n\t\t\"models/citizen_human/heads/citizen_human_malehead04/citizen_human_malehead04.vmat\",\r\n\t\t\"models/citizen_human/heads/citizen_human_femalehead01/citizen_human_femalehead01.vmat\"\r\n\t};\r\n\tpublic static Color MaterialTint(Material original, Color color, bool normalize) => original.GetColor(\"g_vColorTint\") * color * (normalize && DarkBases.Contains(original.Name) ? new Color(1.7f, 1.91f, 2.155f) : Color.White);\r\n\treadonly EyeColorPresentation eyes = new();\r\n\tpublic void Apply(SkinnedModelRenderer body, Color color, bool normalize = false, CharacterMakeupData makeup = null, string skinPath = \"\", CharacterAppearanceData appearance = null)\r\n\t{\r\n\t\tApplySkin(body, color, normalize, makeup, skinPath, false, appearance?.BaseShape is not null, appearance?.BaseShape is not null);\r\n\t\teyes.Apply(body, appearance);\r\n\t}\r\n\r\n\tpublic void ApplyEmbeddedSkin(SkinnedModelRenderer body, Color color, bool normalize = false) => ApplySkin(body, color, normalize, null, \"\", true);\r\n\tpublic void ApplyEmbeddedSkin(SkinnedModelRenderer body, CharacterAppearanceData appearance) => ApplySkin(body, appearance.SkinColor, appearance.NormalizeSkinTone, null, appearance.SkinPath, true, true, appearance.BaseShape is not null);\r\n\tvoid ApplySkin(SkinnedModelRenderer body, Color color, bool normalize, CharacterMakeupData makeup, string skinPath, bool preserveNonSkin = false, bool unified = false, bool unifiedBody = false)\r\n\t{\r\n\t\tif (!body.IsValid() || body.Model is null)\r\n\t\t\treturn;\r\n\t\tif (target != body || model != body.Model || group != body.MaterialGroup)\r\n\t\t{\r\n\t\t\ttarget = body;\r\n\t\t\tmodel = body.Model;\r\n\t\t\tgroup = body.MaterialGroup;\r\n\t\t\tcopies.Clear();\r\n\t\t\tapplied = null;\r\n\t\t\tforeach (var item in makeupCopies.Values)\r\n\t\t\t\titem.Dispose();\r\n\t\t\tmakeupCopies.Clear();\r\n\t\t\tmakeupSignature = null;\r\n\t\t\tClear(body, preserveNonSkin);\r\n\t\t}\r\n\r\n\t\tvar cosmetics = \"matched-neck-albedo\" + makeup?.PresentationKey() + skinPath + unified + unifiedBody + (unifiedBody ? UnifiedHuman.BodyToneTint(skinPath) : Color.White).ToString();\n\t\tvar neck = unifiedBody ? UnifiedNeckAssets.ColorTexture(skinPath) : null;\r\n\t\tif (applied == color && normalized == normalize && makeupSignature == cosmetics && appliedNeck == neck)\r\n\t\t{\r\n\t\t\tRefreshMakeup();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tappliedNeck = neck;\r\n\t\tmakeupSignature = cosmetics;\r\n\t\tif (makeup?.Active != true)\r\n\t\t{\r\n\t\t\tforeach (var item in makeupCopies.Values)\r\n\t\t\t\titem.Dispose();\r\n\t\t\tmakeupCopies.Clear();\r\n\t\t}\r\n\r\n\t\tif (color == Color.White && !normalize && makeup?.Active != true && !unified)\r\n\t\t{\r\n\t\t\tClear(body, preserveNonSkin);\r\n\t\t\tapplied = color;\r\n\t\t\tnormalized = false;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// MaterialAccessor uses original model slots, including alternatives. GetMaterials(group)\r\n\t\t// returns a compact group list and cannot be used as renderer override indices.\r\n\t\tfor (var i = 0; i < body.Materials.Count; i++)\r\n\t\t{\r\n\t\t\tvar slot = body.Materials.GetOriginal(i);\r\n\t\t\tvar original = unified ? UnifiedHuman.SkinMaterial(slot, skinPath, unifiedBody) : HumanMarkingModels.NativeMaterial(slot);\r\n\t\t\tvar armSide = HumanMarkingModels.ArmSide(slot);\r\n\t\t\tif (original?.ShaderName != \"shaders/skin.shader\" || original.Name.EndsWith(\"/mouth.vmat\", StringComparison.OrdinalIgnoreCase))\r\n\t\t\t\tcontinue;\r\n\t\t\tif (!copies.TryGetValue(i, out var copy) || unified)\r\n\t\t\t\tcopies[i] = copy = original.CreateCopy($\"fate-skin-{body.Id}-{i}.vmat\");\r\n\t\t\tvar tint = MaterialTint(original, color, normalize) * (unifiedBody ? UnifiedHuman.BodyToneTint(original, skinPath) : Color.White);\r\n\t\t\tvar selectedHead = IsSelectedHead(original, skinPath);\r\n\t\t\tvar sourceColor = unifiedBody && selectedHead ? UnifiedNeckAssets.ColorTexture(skinPath) : original.GetTexture(\"g_tColor\");\r\n\t\t\tvar bodyInk = original.Name.Contains(\"/bodies/\", StringComparison.OrdinalIgnoreCase) && makeup?.Markings.Any(x => x.Style != \"none\" && x.Ink.Amount > 0 && MarkingCatalog.OnMaterial(x, armSide)) == true;\r\n\t\t\tif (makeup?.Active == true && (selectedHead || bodyInk))\r\n\t\t\t{\r\n\t\t\t\tif (!makeupCopies.TryGetValue(i, out var presentation))\r\n\t\t\t\t\tmakeupCopies[i] = presentation = new();\r\n\t\t\t\t// Keep complexion correction in the native skin shader on every surface.\r\n\t\t\t\t// Baking it into an 8-bit makeup texture changes color-space behavior and\r\n\t\t\t\t// clips bright corrections, creating a visible head/body seam.\r\n\t\t\t\tcopy.Set(\"g_tColor\", presentation.Apply(original, makeup, skinPath, selectedHead, armSide, sourceColor));\r\n\t\t\t\tcopy.Set(\"g_vColorTint\", tint);\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tcopy.Set(\"g_tColor\", sourceColor);\r\n\t\t\t\tcopy.Set(\"g_vColorTint\", tint);\r\n\t\t\t\tif (makeupCopies.Remove(i, out var unused))\r\n\t\t\t\t\tunused.Dispose();\r\n\t\t\t}\r\n\r\n\t\t\tbody.Materials.SetOverride(i, copy);\r\n\t\t}\r\n\r\n\t\tapplied = color;\r\n\t\tnormalized = normalize;\r\n\t}\r\n\r\n\tpublic static bool IsSelectedHead(Material original, string skinPath)\r\n\t{\r\n\t\tvar id = System.IO.Path.GetFileNameWithoutExtension(skinPath);\r\n\t\tvar head = id switch\r\n\t\t{\r\n\t\t\t\"human01\" => \"malehead01\",\r\n\t\t\t\"human02\" => \"femalehead01\",\r\n\t\t\t\"human03\" => \"malehead02\",\r\n\t\t\t\"human04\" => \"femalehead02\",\r\n\t\t\t\"human05\" => \"malehead03\",\r\n\t\t\t\"human06\" => \"malehead04\",\r\n\t\t\t\"human07\" => \"malehead05\",\r\n\t\t\t_ => \"\"\r\n\t\t};\r\n\t\treturn head != \"\" && original.Name.EndsWith($\"citizen_human_{head}.vmat\", StringComparison.OrdinalIgnoreCase);\r\n\t}\r\n\r\n\tpublic void Dispose()\r\n\t{\r\n\t\teyes.Dispose();\r\n\t\tforeach (var item in makeupCopies.Values)\r\n\t\t\titem.Dispose();\r\n\t\tmakeupCopies.Clear();\r\n\t}\r\n\r\n\tvoid RefreshMakeup()\r\n\t{\r\n\t\tforeach (var pair in makeupCopies)\r\n\t\t{\r\n\t\t\tvar texture = pair.Value.Refresh();\r\n\t\t\tif (copies.TryGetValue(pair.Key, out var copy) && copy.GetTexture(\"g_tColor\") != texture)\r\n\t\t\t\tcopy.Set(\"g_tColor\", texture);\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Refresh()\r\n\t{\r\n\t\teyes.Refresh();\r\n\t\tRefreshMakeup();\r\n\t}\r\n\r\n\tpublic void Invalidate()\r\n\t{\r\n\t\tapplied = null;\r\n\t}\r\n\r\n\tstatic void Clear(SkinnedModelRenderer body, bool preserveNonSkin = false)\r\n\t{\r\n\t\tfor (var i = 0; i < body.Materials.Count; i++)\r\n\t\t{\r\n\t\t\tvar slot = body.Materials.GetOriginal(i);\r\n\t\t\tif (preserveNonSkin && (HumanMarkingModels.NativeMaterial(slot)?.ShaderName != \"shaders/skin.shader\" || HumanMarkingModels.NativeMaterial(slot).Name.EndsWith(\"/mouth.vmat\", StringComparison.OrdinalIgnoreCase)))\r\n\t\t\t\tcontinue;\r\n\t\t\tif (HumanMarkingModels.ArmSide(slot) != 0)\r\n\t\t\t\tbody.Materials.SetOverride(i, HumanMarkingModels.NativeMaterial(slot));\r\n\t\t\telse if (body.Materials.HasOverride(i))\r\n\t\t\t\tbody.Materials.SetOverride(i, null);\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Studio/StudioPanel.razor",
"FileName": "StudioPanel.razor",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "@using System\r\n@using System.Text.Json\r\n@using Sandbox\r\n@using Sandbox.UI\r\n@namespace CharacterEditor\r\n@inherits PanelComponent\r\n\r\n<root>\r\n@if(Session is not null && Session.View!=\"hidden\")\r\n{\r\n @if(Session.View==\"editor\")\r\n {\r\n <CharacterCreatorPanel [email protected] @ref=Creator Saved=@Save Closed=@RequestClose SaveLabel=@(Session.IsNew?\"Save character\":\"Save changes\") />\r\n @if(!Session.IsNew){<button class=\"savecopy\" onclick=@SaveCopy>Save as a copy</button>}\r\n @if(ConfirmClose){<div class=\"shade\"><div class=\"dialog\"><h2>Discard your edits?</h2><p>Your saved character will stay as it was.</p><div class=\"actions\"><button onclick=@(()=>ConfirmClose=false)>Keep editing</button><button class=\"primary\" onclick=@Discard>Discard edits</button></div></div></div>}\r\n }\r\n else if(Session.View==\"plaza\")\r\n {\r\n <div class=\"plazahead\"><div><div class=\"eyebrow\">CHARACTER EDITOR</div><h1>The gathering place</h1><p>Your characters, a little more alive.</p></div><button onclick=@(()=>Session.View=\"library\")>Back to collection</button></div>\r\n <div class=\"residents\">\r\n <div class=\"eyebrow\">IN THE PLAZA \u00b7 @Session.Plaza?.Residents.Count</div>\r\n @foreach(var resident in Session.Plaza?.Residents??new List<CharacterAvatar>())\r\n {<div class=\"resident\"><button onclick=@(()=>Session.Plaza.Focus(resident.Preset.Id))>@resident.GameObject.Name</button><button onclick=@(()=>Session.Edit(resident.Preset,resident))>Edit</button></div>}\r\n @if(Session.Library.Items.Count==0){<p>Save your first character to bring this place to life.</p><button class=\"primary\" onclick=@(()=>Session.Edit())>Create a character</button>}\r\n @if(Session.Plaza?.PageCount>1){<div class=\"actions\"><button onclick=@(()=>Session.Plaza.ChangePage(-1))>Previous</button><span>@(Session.Plaza.Page+1) / @Session.Plaza.PageCount</span><button onclick=@(()=>Session.Plaza.ChangePage(1))>Next</button></div>}\r\n </div>\r\n <div class=\"plazatools\"><button onclick=@(()=>Session.Plaza.Yaw-=25)>Turn left</button><button onclick=@(()=>Session.Plaza.Yaw+=25)>Turn right</button><button onclick=@(()=>Session.Plaza.Distance-=80)>Closer</button><button onclick=@(()=>Session.Plaza.Distance+=80)>Further</button><button onclick=@(()=>Session.Plaza.Overview())>Overview</button><button onclick=@(()=>Session.Plaza.Paused=!Session.Plaza.Paused)>@(Session.Plaza?.Paused==true?\"Let them wander\":\"Pause wandering\")</button></div>\r\n }\r\n else\r\n {\r\n <div class=\"collection\">\r\n <div class=\"topbar\"><div class=\"brand\"><span class=\"brandmark\">C</span><span>CHARACTER EDITOR</span></div><div class=\"actions\"><button onclick=@(()=>ImportOpen=true)>Import preset</button><button class=\"primary\" onclick=@(()=>Session.Edit())>+ Create character</button></div></div>\r\n <div class=\"intro\"><div><div class=\"eyebrow\">A PLACE FOR EVERY VERSION OF YOU</div><h1>Meet your characters.</h1><p>Shape a face. Find a personality. Build a little world of your own.</p></div>@if(Session.Plaza.IsValid()){<button class=\"plazalink\" [email protected]><span>Visit the plaza</span><span class=\"arrow\">\u2197</span></button>}</div>\r\n <div class=\"librarybar\"><div class=\"actions\"><button class=@(!Favorites?\"chosen\":\"\") onclick=@(()=>Favorites=false)>All characters \u00b7 @Session.Library.Items.Count</button><button class=@(Favorites?\"chosen\":\"\") onclick=@(()=>Favorites=true)>Favorites</button></div><TextEntry class=\"search\" Value=@Search OnTextEdited=@((string value)=>{Search=value;}) Placeholder=\"Find a character\" /></div>\r\n <div class=\"cards\">\r\n @foreach(var preset in Visible)\r\n {\r\n <div class=\"card\"><button class=\"portrait\" onclick=@(()=>Session.Edit(preset))><CharacterCreatorPreview [email protected] Portrait=@true /></button><div class=\"cardtitle\"><h2>@preset.Name</h2><button class=@($\"favorite {(preset.Favorite?\"chosen\":\"\")}\") onclick=@(()=>Session.Favorite(preset))>@(preset.Favorite?\"\u2605\":\"\u2606\")</button></div><div class=\"cardactions\"><button onclick=@(()=>Session.Edit(preset))>Edit</button><button onclick=@(()=>Export(preset))>Export</button><button class=\"delete\" onclick=@(()=>PendingDelete=preset)>Delete</button></div></div>\r\n }\r\n @if(!Visible.Any())\r\n {<div class=\"empty\"><div class=\"emptyicon\">\u2726</div><h2>@(Session.Library.Items.Count==0?\"Your cast starts here.\":\"No characters found.\")</h2><p>@(Session.Library.Items.Count==0?\"Start with a familiar face, or make someone entirely new. Every saved character gets a place in your plaza.\":\"Try another search or switch back to all characters.\")</p><button class=\"primary\" onclick=@(()=>Session.Edit())>Create your first look</button></div>}\r\n </div>\r\n <div class=\"bottomline\"><span>7 blendable faces \u00b7 Body shaping \u00b7 Your own wardrobe</span><span>Made to mix, match & make your own</span></div>\r\n </div>\r\n }\r\n @if(!string.IsNullOrWhiteSpace(Session.Message)){<div class=\"toast\" onclick=@(()=>Session.Message=null)>@Session.Message</div>}\r\n @if(Session.Library.Warnings.Count>0 && !DismissWarnings){<div class=\"warning\" onclick=@(()=>DismissWarnings=true)>@string.Join(\" \u00b7 \",Session.Library.Warnings) \u00b7 Click to dismiss</div>}\r\n @if(PendingDelete is not null){<div class=\"shade\"><div class=\"dialog\"><h2>Delete @PendingDelete.Name?</h2><p>This removes the saved character and its backup from your collection.</p><div class=\"actions\"><button onclick=@(()=>PendingDelete=null)>Keep character</button><button class=\"primary\" onclick=@Delete>Delete character</button></div></div></div>}\r\n @if(ImportOpen || ExportJson is not null)\r\n {<div class=\"shade\"><div class=\"dialog transfer\"><div class=\"eyebrow\">PORTABLE PRESETS</div><h2>@(ImportOpen?\"Bring a character along.\":\"Take this character with you.\")</h2><p>@(ImportOpen?\"Paste a Character Editor preset or raw Fate Pending appearance JSON. Import always creates a new copy.\":\"Copy this preset into another project using Character Kit. A copy is also saved in character_editor/exports in this game's data folder.\")</p><TextEntry class=\"json\" Multiline=@true Value=@(ImportOpen?ImportJson:ExportJson) OnTextEdited=@((string value)=>{ImportJson=value;}) /><div class=\"actions\"><button onclick=@CloseTransfer>Close</button>@if(ImportOpen){<button class=\"primary\" onclick=@Import>Import character</button>}else{<button class=\"primary\" onclick=@(()=>Clipboard.SetText(ExportJson))>Copy preset</button>}</div></div></div>}\r\n @if(Session.EnableEscapeMenu && !Session.Embedded){<StudioEscapeMenu @ref=EscapeMenu MainMenu=@ReturnToMainMenu OpenLab=@OpenFaceLab EnableExperiments=@(Session.EnableFaceLab && Session.View!=\"editor\") HasUnsavedEdits=@HasUnsavedEdits />}\r\n}\r\n</root>\r\n\r\n@code {\r\n\tpublic StudioSession Session => Components.Get<StudioSession>();\r\n\tpublic CharacterCreatorPanel Creator { get; set; }\r\n\tpublic StudioEscapeMenu EscapeMenu { get; set; }\r\n\r\n\tstring Search = \"\", ImportJson = \"\", ExportJson;\r\n\tbool Favorites, ImportOpen, ConfirmClose, DismissWarnings;\r\n\tCharacterPreset PendingDelete;\r\n\tIEnumerable<CharacterPreset> Visible => Session.Library.Items.Where(x => (!Favorites || x.Favorite) && x.Name.Contains(Search ?? \"\", StringComparison.OrdinalIgnoreCase)).OrderByDescending(x => x.Favorite).ThenBy(x => x.Name);\r\n\r\n\tprotected override void OnStart()\r\n\t{\r\n\t\tSession?.EnsureLoaded();\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tbase.OnUpdate();\r\n\t\tif (Session?.EnableEscapeMenu == true && !Session.Embedded && Session.View != \"hidden\" && EscapeMenu is not null && Input.EscapePressed)\r\n\t\t{\r\n\t\t\tInput.EscapePressed = false;\r\n\t\t\tEscapeMenu.Toggle();\r\n\t\t}\r\n\t}\r\n\r\n\tbool HasUnsavedEdits() => Session.View == \"editor\" && Creator is not null && Session.Draft is not null && (Creator.DraftCharacterName != Session.Draft.Name || JsonSerializer.Serialize(Creator.DraftAppearance) != JsonSerializer.Serialize(Session.Draft.Appearance));\r\n\tvoid ReturnToMainMenu() => StudioSession.OpenScene(\"scenes/main_menu.scene\");\r\n\tvoid OpenFaceLab() => StudioSession.OpenScene(\"scenes/face_lab.scene\");\r\n\tstring Save(string name, CharacterAppearanceData data) => Session.Save(name, data);\r\n\tvoid SaveCopy()\r\n\t{\r\n\t\tif (Creator?.AppearanceReady == true)\r\n\t\t{\r\n\t\t\tvar error = Session.Save(Creator.DraftCharacterName, Creator.DraftAppearance, true);\r\n\t\t\tif (error is not null)\r\n\t\t\t\tSession.Message = error;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid RequestClose()\r\n\t{\r\n\t\tif (HasUnsavedEdits())\r\n\t\t\tConfirmClose = true;\r\n\t\telse\r\n\t\t\tDiscard();\r\n\t}\r\n\r\n\tvoid Discard()\r\n\t{\r\n\t\tConfirmClose = false;\r\n\t\tSession.CloseEditor();\r\n\t\tCreator = null;\r\n\t}\r\n\r\n\tvoid Delete()\r\n\t{\r\n\t\tSession.Delete(PendingDelete);\r\n\t\tPendingDelete = null;\r\n\t}\r\n\r\n\tvoid Import()\r\n\t{\r\n\t\tSession.Import(ImportJson);\r\n\t\tif (Session.Message.StartsWith(\"Imported \"))\r\n\t\t\tCloseTransfer();\r\n\t}\r\n\r\n\tvoid Export(CharacterPreset preset)\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tExportJson = preset.ToJson();\r\n\t\t\tFileSystem.Data.CreateDirectory(\"character_editor/exports\");\r\n\t\t\tFileSystem.Data.WriteAllText($\"character_editor/exports/{preset.Id}.json\", ExportJson);\r\n\t\t}\r\n\t\tcatch (Exception e)\r\n\t\t{\r\n\t\t\tSession.Message = e.Message;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid CloseTransfer()\r\n\t{\r\n\t\tImportOpen = false;\r\n\t\tExportJson = null;\r\n\t\tImportJson = \"\";\r\n\t}\r\n\r\n\tprotected override int BuildHash() => HashCode.Combine(Session?.View, Session?.Revision, Session?.Library.Revision, Search, Favorites, HashCode.Combine(ImportOpen, ExportJson, PendingDelete, ConfirmClose, DismissWarnings, Session?.Message), Session?.Plaza?.Paused, HashCode.Combine(Session?.Plaza?.Page, Session?.Plaza?.Residents.Count, Session?.EnableEscapeMenu, Session?.EnableFaceLab, Session?.Embedded));\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/Studio/StudioPanel.razor.scss",
"FileName": "StudioPanel.razor.scss",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "StudioPanel {\n position: absolute; width: 100%; height: 100%; font-family: Inter; color: #f0eee6; pointer-events: none;\n button { pointer-events: all; cursor: pointer; padding: 11px 17px; border-radius: 6px; background-color: #ffffff0a; border: 1px solid #ffffff20; color: #e9ebe6; font-size: 14px; flex-shrink: 0; align-items: center; justify-content: center; gap: 10px; }\n button:hover { background-color: #ffffff20; border-color: #c5e4a7; }\n button.primary { background-color: #d6ebaf; border-color: #d6ebaf; color: #263425; font-weight: 600; }\n button.primary:hover { background-color: #eaf9cc; }\n button.chosen { color: #d6ebaf; border-color: #d6ebaf88; }\n h1, h2, p { margin: 0; flex-shrink: 0; }\n h1 { font-size: 48px; font-weight: 500; letter-spacing: -1.5px; }\n h2 { font-size: 22px; font-weight: 500; }\n p { font-size: 15px; line-height: 25px; color: #aeb8ab; white-space: normal; }\n .eyebrow { font-size: 11px; letter-spacing: 2px; color: #c6d9b2; margin-bottom: 13px; }\n .collection { position: absolute; width: 100%; height: 100%; background-color: #17221ff9; padding: 30px 54px 20px; flex-direction: column; pointer-events: all; }\n .topbar { justify-content: space-between; align-items: center; flex-shrink: 0; }\n .brand { gap: 13px; align-items: center; letter-spacing: 2px; font-size: 12px; font-weight: 600; }\n .brandmark { width: 37px; height: 37px; border-radius: 50%; border: 2px solid #d6ebaf; color: #d6ebaf; font-size: 25px; align-items: center; justify-content: center; }\n .actions { gap: 10px; align-items: center; flex-shrink: 0; }\n .intro { margin: 42px 0 30px; justify-content: space-between; align-items: center; flex-shrink: 0; }\n .intro > div { flex-direction: column; gap: 9px; }\n .intro .eyebrow { margin-bottom: 0; }\n .plazalink { width: 235px; height: 100px; background-color: #354b3e; justify-content: space-between; padding: 25px; font-size: 19px; border-color: #a5c08466; }\n .arrow { font-size: 36px; color: #d6ebaf; }\n .librarybar { justify-content: space-between; align-items: center; border-bottom: 1px solid #ffffff20; padding-bottom: 17px; flex-shrink: 0; }\n TextEntry.search { width: 240px; padding: 12px 15px; background-color: #0e1916; border: 1px solid #ffffff20; border-radius: 6px; color: #edf2e5; font-size: 14px; }\n .cards { flex-grow: 1; min-height: 0; overflow-y: auto; flex-wrap: wrap; align-content: flex-start; gap: 20px; padding: 22px 0; }\n .card { width: 220px; flex-shrink: 0; flex-direction: column; background-color: #25312c; border: 1px solid #ffffff18; border-radius: 9px; overflow: hidden; }\n button.portrait { width: 100%; height: 190px; padding: 0; border: 0; border-radius: 0; background-color: #435146; position: relative; }\n .portrait CharacterCreatorPreview { position: absolute; width: 100%; height: 100%; pointer-events: none; }\n .cardtitle { align-items: center; justify-content: space-between; padding: 10px 14px 4px; }\n .cardtitle h2 { font-size: 17px; max-width: 153px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }\n button.favorite { padding: 0; border: 0; background-color: transparent; font-size: 26px; }\n .cardactions { gap: 5px; padding: 8px 12px 14px; }\n .cardactions button { font-size: 11px; padding: 7px 10px; }\n button.delete { color: #d5aaa1; }\n .empty { width: 100%; padding: 45px 0; flex-direction: column; align-items: center; gap: 17px; }\n .empty p { max-width: 450px; text-align: center; }\n .emptyicon { font-size: 54px; color: #d6ebaf; }\n .bottomline { justify-content: space-between; color: #809487; font-size: 11px; padding-top: 14px; border-top: 1px solid #ffffff18; flex-shrink: 0; }\n .plazahead { position: absolute; left: 38px; right: 38px; top: 30px; align-items: flex-start; justify-content: space-between; text-shadow: 0 2px 4px #000a; }\n .plazahead > div { flex-direction: column; gap: 5px; }\n .plazahead h1 { font-size: 34px; }\n .plazahead button { background-color: #17221fee; }\n .plazahead p { color: #edf2e5; }\n .residents { position: absolute; left: 30px; top: 190px; width: 245px; max-height: 65%; overflow-y: auto; flex-direction: column; padding: 20px; background-color: #17221fe8; border: 1px solid #ffffff25; border-radius: 8px; pointer-events: all; gap: 8px; }\n .resident { gap: 6px; flex-shrink: 0; }\n .resident button { font-size: 12px; padding: 8px; }\n .resident button:first-child { flex-grow: 1; justify-content: flex-start; }\n .plazatools { position: absolute; bottom: 25px; right: 30px; gap: 6px; padding: 10px; background-color: #17221fe8; border-radius: 8px; }\n .plazatools button { font-size: 12px; padding: 10px; }\n .shade { position: absolute; width: 100%; height: 100%; background-color: #07140eea; align-items: center; justify-content: center; pointer-events: all; z-index: 50; }\n .dialog { width: 490px; padding: 35px; background-color: #25352b; border: 1px solid #acc79666; border-radius: 12px; flex-direction: column; gap: 22px; }\n .dialog .actions { justify-content: flex-end; }\n .transfer { width: 700px; }\n TextEntry.json { height: 260px; padding: 15px; background-color: #121f18; color: #d6ebaf; font-size: 12px; overflow-y: auto; white-space: pre-wrap; }\n .toast { position: absolute; bottom: 65px; left: 35%; max-width: 60%; padding: 14px 22px; background-color: #d6ebaf; color: #263425; border-radius: 8px; pointer-events: all; z-index: 100; font-size: 14px; }\n .warning { position: absolute; bottom: 110px; left: 30px; max-width: 70%; background-color: #633f24; padding: 15px; pointer-events: all; z-index: 101; }\n button.savecopy { position: absolute; right: 250px; bottom: 27px; z-index: 6; background-color: #25352b; }\n}\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "CharacterKit/SerializedMeshBones.cs",
"FileName": "SerializedMeshBones.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\nusing System.IO;\r\nusing System.Text;\r\n\r\nnamespace CharacterEditor;\r\n/// <summary>Reads the mesh-local bone order in the engine's SaveToVmdl metadata.</summary>\ninternal static class SerializedMeshBones\r\n{\r\n\t// The engine's own serializer currently emits KV3 v2 with LZ4. This reader\r\n\t// consumes that output, never arbitrary on-disk compressed clothing files.\r\n\tpublic static string[] Read(byte[] block)\r\n\t{\r\n\t\tusing var r = new BinaryReader(new MemoryStream(block));\r\n\t\tif (r.ReadUInt32() != 0x4b563302)\r\n\t\t\tthrow new InvalidDataException(\"Unsupported serialized mesh metadata version.\");\r\n\t\tr.ReadBytes(16);\r\n\t\tint compression = r.ReadInt32();\r\n\t\tr.ReadInt32();\r\n\t\tint bytes = r.ReadInt32(), ints = r.ReadInt32(), doubles = r.ReadInt32();\r\n\t\tr.ReadInt32();\r\n\t\tr.ReadInt32();\r\n\t\tint size = r.ReadInt32(), packed = r.ReadInt32();\r\n\t\tr.ReadInt32();\r\n\t\tr.ReadInt32();\r\n\t\tif (size < 0 || size > 32000000)\r\n\t\t\tthrow new InvalidDataException(\"Invalid mesh metadata size.\");\r\n\t\tvar payload = r.ReadBytes(packed);\r\n\t\tvar data = compression == 0 ? payload : compression == 1 ? Expand(payload, size) : throw new InvalidDataException(\"Unsupported mesh metadata compression.\");\r\n\t\tvar p4 = (bytes + 3) & ~3;\r\n\t\tvar p8 = (p4 + ints * 4 + 7) & ~7;\r\n\t\tvar stringsAt = p8 + doubles * 8;\r\n\t\tint strings = BitConverter.ToInt32(data, p4);\r\n\t\tvar text = new string[strings];\r\n\t\tint cursor = stringsAt;\r\n\t\tfor (int i = 0; i < strings; i++)\r\n\t\t{\r\n\t\t\tvar end = Array.IndexOf(data, (byte)0, cursor);\r\n\t\t\tif (end < 0)\r\n\t\t\t\tthrow new InvalidDataException(\"Missing metadata string terminator.\");\r\n\t\t\ttext[i] = Encoding.UTF8.GetString(data, cursor, end - cursor);\r\n\t\t\tcursor = end + 1;\r\n\t\t}\r\n\r\n\t\tvar reader = new Values(data, text, 0, p4 + 4, p8, cursor);\r\n\t\tvar root = reader.Value(reader.Type(), 0) as Dictionary<string, object>;\r\n\t\tif (root?.TryGetValue(\"m_skeleton\", out var skeleton) == true)\r\n\t\t\troot = skeleton as Dictionary<string, object>;\r\n\t\tif (root is null || !root.TryGetValue(\"m_bones\", out var boneValue) || boneValue is not List<object> bones)\r\n\t\t\tthrow new InvalidDataException(\"Serialized garment has no mesh bone table.\");\r\n\t\treturn bones.Cast<Dictionary<string, object>>().Select(b => (string)b[\"m_boneName\"]).ToArray();\r\n\t}\r\n\r\n\tstatic byte[] Expand(byte[] input, int length)\r\n\t{\r\n\t\tvar output = new byte[length];\r\n\t\tint i = 0, o = 0;\r\n\t\tint Extended(int n)\r\n\t\t{\r\n\t\t\tif (n != 15)\r\n\t\t\t\treturn n;\r\n\t\t\tint b;\r\n\t\t\tdo\r\n\t\t\t{\r\n\t\t\t\tb = input[i++];\r\n\t\t\t\tn += b;\r\n\t\t\t}\r\n\t\t\twhile (b == 255);\r\n\t\t\treturn n;\r\n\t\t}\r\n\r\n\t\twhile (i < input.Length)\r\n\t\t{\r\n\t\t\tint token = input[i++];\r\n\t\t\tint literal = Extended(token >> 4);\r\n\t\t\tif (i + literal > input.Length || o + literal > length)\r\n\t\t\t\tthrow new InvalidDataException(\"Invalid metadata literal.\");\r\n\t\t\tArray.Copy(input, i, output, o, literal);\r\n\t\t\ti += literal;\r\n\t\t\to += literal;\r\n\t\t\tif (i == input.Length)\r\n\t\t\t\tbreak;\r\n\t\t\tint offset = input[i++] | input[i++] << 8;\r\n\t\t\tint match = Extended(token & 15) + 4;\r\n\t\t\tif (offset <= 0 || offset > o || o + match > length)\r\n\t\t\t\tthrow new InvalidDataException(\"Invalid metadata match.\");\r\n\t\t\tfor (int j = 0; j < match; j++)\r\n\t\t\t\toutput[o + j] = output[o + j - offset];\r\n\t\t\to += match;\r\n\t\t}\r\n\r\n\t\tif (o != length)\r\n\t\t\tthrow new InvalidDataException(\"Truncated mesh metadata.\");\r\n\t\treturn output;\r\n\t}\r\n\r\n\tsealed class Values\r\n\t{\r\n\t\treadonly byte[] data;\r\n\t\treadonly string[] strings;\r\n\t\tint p1, p4, p8, types;\r\n\t\tpublic Values(byte[] data, string[] strings, int p1, int p4, int p8, int types)\r\n\t\t{\r\n\t\t\tthis.data = data;\r\n\t\t\tthis.strings = strings;\r\n\t\t\tthis.p1 = p1;\r\n\t\t\tthis.p4 = p4;\r\n\t\t\tthis.p8 = p8;\r\n\t\t\tthis.types = types;\r\n\t\t}\r\n\r\n\t\tint I()\r\n\t\t{\r\n\t\t\tvar value = BitConverter.ToInt32(data, p4);\r\n\t\t\tp4 += 4;\r\n\t\t\treturn value;\r\n\t\t}\r\n\r\n\t\tlong L()\r\n\t\t{\r\n\t\t\tvar value = BitConverter.ToInt64(data, p8);\r\n\t\t\tp8 += 8;\r\n\t\t\treturn value;\r\n\t\t}\r\n\r\n\t\tstring S(int i) => i < 0 ? \"\" : strings[i];\r\n\t\tpublic int Type()\r\n\t\t{\r\n\t\t\tint t = data[types++];\r\n\t\t\tif ((t & 128) != 0)\r\n\t\t\t{\r\n\t\t\t\tt &= 127;\r\n\t\t\t\ttypes++;\r\n\t\t\t}\r\n\r\n\t\t\treturn t;\r\n\t\t}\r\n\r\n\t\tpublic object Value(int type, int depth)\r\n\t\t{\r\n\t\t\tif (depth > 64)\r\n\t\t\t\tthrow new InvalidDataException(\"Excessive mesh metadata nesting.\");\r\n\t\t\tswitch (type)\r\n\t\t\t{\r\n\t\t\t\tcase 1:\r\n\t\t\t\t\treturn null;\r\n\t\t\t\tcase 2:\r\n\t\t\t\t\treturn data[p1++] != 0;\r\n\t\t\t\tcase 3:\r\n\t\t\t\tcase 4:\r\n\t\t\t\t\treturn L();\r\n\t\t\t\tcase 5:\r\n\t\t\t\t\tvar number = BitConverter.Int64BitsToDouble(L());\r\n\t\t\t\t\treturn number;\r\n\t\t\t\tcase 6:\r\n\t\t\t\t\treturn S(I());\r\n\t\t\t\tcase 8:\r\n\t\t\t\tcase 10:\r\n\t\t\t\tcase 24:\r\n\t\t\t\t\tint count = type == 24 ? data[p1++] : I();\r\n\t\t\t\t\tif (count < 0 || count > 2000000)\r\n\t\t\t\t\t\tthrow new InvalidDataException(\"Invalid metadata array.\");\r\n\t\t\t\t\tint element = type == 8 ? 0 : Type();\r\n\t\t\t\t\tvar array = new List<object>(count);\r\n\t\t\t\t\tfor (int i = 0; i < count; i++)\r\n\t\t\t\t\t\tarray.Add(Value(element == 0 ? Type() : element, depth + 1));\r\n\t\t\t\t\treturn array;\r\n\t\t\t\tcase 9:\r\n\t\t\t\t\tint members = I();\r\n\t\t\t\t\tif (members < 0 || members > 100000)\r\n\t\t\t\t\t\tthrow new InvalidDataException(\"Invalid metadata object.\");\r\n\t\t\t\t\tvar obj = new Dictionary<string, object>();\r\n\t\t\t\t\tfor (int i = 0; i < members; i++)\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tint t = Type();\r\n\t\t\t\t\t\tstring name = S(I());\r\n\t\t\t\t\t\tobj[name] = Value(t, depth + 1);\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\treturn obj;\r\n\t\t\t\tcase 11:\r\n\t\t\t\tcase 12:\r\n\t\t\t\t\treturn I();\r\n\t\t\t\tcase 13:\r\n\t\t\t\t\treturn true;\r\n\t\t\t\tcase 14:\r\n\t\t\t\t\treturn false;\r\n\t\t\t\tcase 15:\r\n\t\t\t\t\treturn 0L;\r\n\t\t\t\tcase 16:\r\n\t\t\t\t\treturn 1L;\r\n\t\t\t\tcase 17:\r\n\t\t\t\t\treturn 0d;\r\n\t\t\t\tcase 18:\r\n\t\t\t\t\treturn 1d;\r\n\t\t\t\tdefault:\r\n\t\t\t\t\tthrow new InvalidDataException(\"Unsupported mesh metadata node \" + type);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/CharacterAvatar.cs",
"FileName": "CharacterAvatar.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\nusing System.Threading.Tasks;\r\n\r\nnamespace CharacterEditor;\r\n/// <summary>Drop-in human prefab. The same portable JSON drives editor and world rendering.</summary>\r\npublic sealed class CharacterAvatar : Component, Component.ExecuteInEditor\r\n{\r\n\t[Property, TextArea]\r\n\tpublic string PresetJson { get; set; } = \"\";\r\n\r\n\t[Property]\r\n\tpublic bool ShowClothing { get; set; } = true;\r\n\r\n\t[Property]\r\n\tpublic bool AllowEditing { get; set; } = true;\r\n\r\n\t[Property]\r\n\tpublic bool ShowEditButton { get; set; }\r\n\r\n\t[Property]\r\n\tpublic SkinnedModelRenderer BodyTarget { get; set; }\r\n\r\n\t[Property]\r\n\tpublic Dresser Wardrobe { get; set; }\r\n\tpublic CharacterPreset Preset { get; private set; } = new();\r\n\r\n\t[Property, ReadOnly]\r\n\tpublic string Error { get; private set; }\r\n\tpublic SkinnedModelRenderer Body => BodyTarget.IsValid() ? BodyTarget : visual?.Body;\r\n\r\n\tCharacterVisual visual;\r\n\tstring received;\r\n\tGameObject editPrompt;\r\n\tTask wardrobeLoad;\r\n\tint wardrobeRevision;\r\n\tpublic void Apply(CharacterPreset preset)\r\n\t{\r\n\t\tif (preset.Validate()is string error)\r\n\t\t\tthrow new InvalidOperationException(error);\r\n\t\tPreset = preset.DeepCopy();\r\n\t\tif (BodyTarget.IsValid() && Preset.Appearance.BaseShape is null)\r\n\t\t\tUnifiedHuman.Enable(Preset.Appearance);\r\n\t\tPresetJson = Preset.ToJson();\r\n\t\treceived = PresetJson;\r\n\t\tSyncWardrobe();\r\n\t}\r\n\r\n\t[Button]\r\n\tpublic void Randomize()\r\n\t{\r\n\t\tvar previous = Preset.Appearance;\r\n\t\tvar a = AppearanceCatalog.Randomize(blendHeads: true);\r\n\t\ta.VoiceId = previous.VoiceId;\r\n\t\tif (previous.BaseShape is { } shape)\r\n\t\t{\r\n\t\t\ta.BaseShape.ClothingVariant = shape.ClothingVariant;\r\n\t\t\ta.BaseShape.PhotoFaces = shape.PhotoFaces.Select(f => f.DeepCopy()).ToList();\r\n\t\t\tforeach (var face in a.BaseShape.PhotoFaces)\r\n\t\t\t\tface.Influence = 0;\r\n\t\t}\r\n\r\n\t\tvar name = GameObject.Name;\r\n\t\tApply(new CharacterPreset { Name = string.IsNullOrWhiteSpace(name) ? \"New character\" : name[..Math.Min(name.Length, 40)], Appearance = a });\r\n\t}\r\n\r\n\t[Button]\r\n\tpublic void LoadJson()\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tApply(CharacterPreset.FromJson(PresetJson));\r\n\t\t\tError = null;\r\n\t\t}\r\n\t\tcatch (Exception e)\r\n\t\t{\r\n\t\t\tError = e.Message;\r\n\t\t}\r\n\t}\r\n\r\n\t[Button]\r\n\tpublic void RefreshAppearance()\r\n\t{\r\n\t\tvisual?.Invalidate();\r\n\t\tOnUpdate();\r\n\t}\r\n\r\n\t[Button]\r\n\tpublic void UpdateJson()\r\n\t{\r\n\t\tif (Wardrobe.IsValid())\r\n\t\t{\r\n\t\t\tvar items = Wardrobe.Clothing.Where(e => e.Clothing is not null).Select(e => e.Clothing).ToArray();\r\n\t\t\tvar a = Preset.Appearance;\r\n\t\t\ta.HairPath = items.FirstOrDefault(ClothingResources.IsHair) is { } hair ? ClothingResources.Reference(hair) : \"\";\r\n\t\t\ta.FacialHairPath = items.FirstOrDefault(ClothingResources.IsFacialHair) is { } facial ? ClothingResources.Reference(facial) : \"\";\r\n\t\t\ta.ClothingPaths = items.Where(c => !ClothingResources.IsBaseSkin(c) && !ClothingResources.IsHair(c) && !ClothingResources.IsFacialHair(c)).Select(ClothingResources.Reference).ToArray();\r\n\t\t}\r\n\r\n\t\tif (Preset.Validate()is string error)\r\n\t\t{\r\n\t\t\tError = error;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tPresetJson = Preset.ToJson();\r\n\t\treceived = PresetJson;\r\n\t\tError = null;\r\n\t}\r\n\r\n\tvoid SyncWardrobe()\r\n\t{\r\n\t\tif (!Wardrobe.IsValid())\r\n\t\t\treturn;\r\n\t\twardrobeLoad = SyncWardrobeAsync(Preset.Appearance.DeepCopy(), ++wardrobeRevision);\r\n\t}\r\n\r\n\tasync Task SyncWardrobeAsync(CharacterAppearanceData a, int revision)\r\n\t{\r\n\t\tawait CharacterVisual.Load(a);\r\n\t\tif (!IsValid || revision != wardrobeRevision || !Wardrobe.IsValid())\r\n\t\t\treturn;\r\n\t\tvar container = new ClothingContainer\r\n\t\t{\r\n\t\t\tPrefersHuman = true\r\n\t\t};\r\n\t\tvar paths = new[]\r\n\t\t{\r\n\t\t\tUnifiedHuman.PresentationSkin(a),\r\n\t\t\ta.HairPath,\r\n\t\t\ta.FacialHairPath\r\n\t\t}.Concat(WardrobeCatalog.Paths(a)).Where(p => !string.IsNullOrEmpty(p));\r\n\t\tforeach (var path in paths)\r\n\t\t{\r\n\t\t\tvar item = ClothingResources.Get(path);\r\n\t\t\tif (item is not null)\r\n\t\t\t\tcontainer.Add(item);\r\n\t\t}\r\n\r\n\t\tWardrobe.Source = Dresser.ClothingSource.Manual;\r\n\t\tWardrobe.BodyTarget = Body;\r\n\t\tWardrobe.ApplyHeightScale = false;\r\n\t\tWardrobe.Clothing = container.Clothing;\r\n\t\tWardrobe.Apply(container);\r\n\t}\r\n\r\n\tprotected override void OnUpdate()\r\n\t{\r\n\t\tif (received != PresetJson)\r\n\t\t{\r\n\t\t\ttry\r\n\t\t\t{\r\n\t\t\t\tbool initial = received is null;\r\n\t\t\t\tPreset = string.IsNullOrWhiteSpace(PresetJson) ? new CharacterPreset() : CharacterPreset.FromJson(PresetJson);\r\n\t\t\t\tif (BodyTarget.IsValid() && Preset.Appearance.BaseShape is null)\r\n\t\t\t\t\tUnifiedHuman.Enable(Preset.Appearance);\r\n\t\t\t\treceived = PresetJson;\r\n\t\t\t\tif (!string.IsNullOrWhiteSpace(PresetJson) && (!initial || !Wardrobe.IsValid() || Wardrobe.Clothing.Count == 0))\r\n\t\t\t\t\tSyncWardrobe();\r\n\t\t\t\tError = null;\r\n\t\t\t}\r\n\t\t\tcatch (Exception e)\r\n\t\t\t{\r\n\t\t\t\tError = e.Message;\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (wardrobeLoad?.IsFaulted == true)\r\n\t\t{\r\n\t\t\tError = wardrobeLoad.Exception.GetBaseException().Message;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif (wardrobeLoad?.IsCompleted == false)\r\n\t\t\treturn;\r\n\t\tvisual ??= new CharacterVisual(GameObject, BodyTarget, Wardrobe);\r\n\t\tvisual.Update(Preset.Appearance, ShowClothing);\r\n\t\tError = visual.Error;\r\n\t\tif (!Scene.IsEditor && ShowEditButton && AllowEditing && !editPrompt.IsValid())\r\n\t\t{\r\n\t\t\teditPrompt = new GameObject\r\n\t\t\t{\r\n\t\t\t\tParent = GameObject,\r\n\t\t\t\tName = \"Edit character button\",\r\n\t\t\t\tNetworkMode = NetworkMode.Never\r\n\t\t\t};\r\n\t\t\teditPrompt.Components.Create<ScreenPanel>();\r\n\t\t\teditPrompt.Components.Create<AvatarEditButton>().Avatar = this;\r\n\t\t}\r\n\r\n\t\tif (editPrompt.IsValid())\r\n\t\t\teditPrompt.Enabled = !Scene.IsEditor && ShowEditButton && AllowEditing;\r\n\t}\r\n\r\n\tpublic void OpenEditor()\r\n\t{\r\n\t\tif (!AllowEditing)\r\n\t\t\treturn;\r\n\t\tvar session = Scene.GetAllComponents<StudioSession>().FirstOrDefault();\r\n\t\tif (!session.IsValid())\r\n\t\t{\r\n\t\t\tusing var scope = Scene.Push();\r\n\t\t\tvar ui = new GameObject\r\n\t\t\t{\r\n\t\t\t\tName = \"Character editor\",\r\n\t\t\t\tNetworkMode = NetworkMode.Never\r\n\t\t\t};\r\n\t\t\tsession = ui.Components.Create<StudioSession>();\r\n\t\t\tsession.Embedded = true;\r\n\t\t\tui.Components.Create<ScreenPanel>();\r\n\t\t\tui.Components.Create<StudioPanel>();\r\n\t\t}\r\n\r\n\t\tsession.Edit(Preset, this);\r\n\t}\r\n\r\n\tprotected override void OnDisabled()\r\n\t{\r\n\t\tvisual?.Dispose();\r\n\t\tvisual = null;\r\n\t}\r\n\r\n\tprotected override void OnDestroy()\r\n\t{\r\n\t\tvisual?.Dispose();\r\n\t}\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/CharacterCreatorPanel.razor",
"FileName": "CharacterCreatorPanel.razor",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "@using Sandbox\r\n\r\n@using Sandbox.UI\r\n\r\n@using System\r\n\r\n@using System.Text.Json\r\n\r\n@using System.Threading.Tasks\r\n\r\n@namespace CharacterEditor\r\n\r\n@inherits Panel\r\n\r\n<root class=\"interactive creatoroverlay\">\r\n\r\n @if(true)\r\n\r\n {\r\n\r\n <div class=\"creatorwindow\">\r\n\r\n <div class=\"creatorheader\"><div><div class=\"creatorbreadcrumb\"><span>@ProductLabel</span><span class=\"material-icons\">chevron_right</span><span>@LocationLabel</span></div><div class=\"creatortitle\">@Title</div></div><button class=\"creatorbutton quiet cancelcreation\" onclick=@Back><span class=\"material-icons\">arrow_back</span>Back</button></div>\r\n\r\n <div class=\"creatorbody\">\r\n\r\n <div class=\"creatorcontrols\">\r\n\r\n <div class=\"creatortabs\">@foreach(var tab in Tabs){<button class=@($\"creatorbutton creatortab {(Tab==tab?\"chosen\":\"\")}\") onclick=@(()=>ChangeTab(tab))>@tab</button>}</div>\r\n\r\n <div class=@($\"creatoroptions {(Tab==\"Voice\"?\"voiceoptions\":\"\")}\")>\r\n\r\n <ReferenceFacePanel style=@(Tab==\"Photos\"?\"display: flex;\":\"display: none;\") Appearance=@Appearance Generated=@AddPhotoFace />\r\n @if(Tab==\"Presets\")\r\n\r\n {\r\n\r\n <div class=\"optionheading\">Choose a starting look</div><div class=\"optiondescription\">Start with a preset, then make it your own.</div><div class=\"presetgrid\">\r\n\r\n @foreach(var preset in AppearanceCatalog.Presets){<button class=@($\"creatorbutton presetchoice {(ChosenPreset==preset.Id?\"chosen\":\"\")}\") onclick=@(()=>ApplyPreset(preset.Id))><div class=\"presetportrait\"><CharacterCreatorPreview [email protected] Portrait=@true /></div><div class=\"presetlabel\">@preset.Label</div></button>}\r\n\r\n </div>\r\n\r\n }\r\n\r\n else if(Tab==\"Face & body\")\r\n\r\n {\r\n\r\n @if(Appearance.BaseShape is not null)\r\n {\r\n <div class=\"optionheading\">Blend your base face</div><div class=\"optiondescription\">Overall sets the whole face. In regional tabs, blend that result with individual faces for that area. Refine the result in Shape; choose textures in Skin.</div>\r\n <div class=\"blendtabs\">@foreach(var region in new[]{\"overall\"}.Concat(FaceRegions.Ids)){<button class=@($\"creatorbutton blendtab blend-tab-{region} {(BlendRegion==region?\"chosen\":\"\")}\") onclick=@(()=>BlendRegion=region)>@FaceRegions.Label(region)</button>}</div>\r\n @if(BlendRegion!=\"overall\")\r\n {\r\n <div class=\"regionstatus\"><span>Mix the live Overall blend with individual faces. Relative weights determine each share.</span><button class=\"creatorbutton quiet reset-region\" onclick=@ResetRegion>Reset region</button></div>\r\n <div class=\"overallblendrow\"><div class=\"shapelabel\"><span>Overall blend</span><span>@($\"{MathF.Round(OverallWeight*100)} influence \u00b7 {MathF.Round(FaceRegions.OverallShare(Appearance.BaseShape,BlendRegion)*100)}% of region\")</span></div><SliderControl class=\"shapeslider region-overall-weight\" Value=@OverallWeight OnValueChanged=@SetOverallWeight Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /><div class=\"optiondescription\">Follows changes made in the Overall tab.</div></div>\r\n }\r\n @for(var i=0;i<7;i++)\r\n {\r\n var index=i;\r\n <div class=\"shaperow\"><div class=\"shapelabel\"><span>Face @(index+1)</span><span>@BlendWeightLabel(index)</span></div><SliderControl class=@($\"shapeslider head-weight-{index+1}\") Value=@HeadWeight(index) OnValueChanged=@((float value)=>SetHeadWeight(index,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n }\r\n @foreach(var face in Appearance.BaseShape.PhotoFaces)\r\n {\r\n <div class=\"photoblendcard\">\r\n <div class=\"photoblendheader\"><span class=\"photoblendname\">@face.Name</span><button class=\"creatorbutton quiet delete-photo-face\" tooltip=\"Delete this custom face from the character and all regions\" onclick=@(()=>FaceRegions.RemovePhoto(Appearance.BaseShape,face))><span class=\"material-icons\">delete_outline</span></button></div>\r\n <div class=\"shapelabel\"><span>Blend influence</span><span>@($\"{MathF.Round(PhotoWeight(face)*100)} influence \u00b7 {MathF.Round(FaceRegions.PhotoEntryShare(Appearance.BaseShape,face,BlendRegion)*100)}% of blend\")</span></div>\r\n <SliderControl class=\"shapeslider photo-face-weight\" Value=@PhotoWeight(face) OnValueChanged=@((float value)=>SetPhotoWeight(face,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false />\r\n @if(BlendRegion==\"overall\")\r\n {\r\n <div class=\"shapelabel\"><span>Photo detail strength</span><span>@MathF.Round(face.Strength*100)%</span></div>\r\n <SliderControl class=\"shapeslider photo-face-strength\" [email protected] OnValueChanged=@((float value)=>{face.Strength=Math.Clamp(value,0,2);}) Min=@(0f) Max=@(2f) Step=@(.01f) ShowTextEntry=@false />\r\n <div class=\"optiondescription\">100% is the fitted shape. Higher values exaggerate its slider changes, within normal Shape limits. This also affects regions using this face.</div>\r\n }\r\n </div>\r\n }\r\n @if(Appearance.BaseShape.TotalInfluence==0){<div class=\"optiondescription\">All influences are zero: using Face 3 as the neutral base.</div>}\r\n <div class=\"optionheading\">Base body</div><div class=\"shapelabel\"><span>@MathF.Round((1-Appearance.BaseShape.MaleBody)*100)% female</span><span>@MathF.Round(Appearance.BaseShape.MaleBody*100)% male</span></div><SliderControl class=\"shapeslider base-body\" [email protected] OnValueChanged=@SetBodyBlend Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false />\r\n @foreach(var variant in new[]{\"male\",\"female\"})\r\n {\r\n <button class=@($\"creatorbutton quiet clothing-prefer-{variant}\") role=\"checkbox\" aria-checked=@(Appearance.BaseShape.ClothingVariant==variant?\"true\":\"false\") onclick=@(()=>ToggleClothingVariant(variant))><span class=\"material-icons\">@(Appearance.BaseShape.ClothingVariant==variant?\"check_box\":\"check_box_outline_blank\")</span>Prefer @variant clothing version</button>\r\n }\r\n <div class=\"optiondescription\">Neither selected: use the body blend. Available versions still fit your body shape.</div>\r\n <div class=\"optiondescription\">Body blend is independent of face and skin. Breast adjustments fade with the female amount; bulge adjustments fade with the male amount.</div>\r\n }\r\n else\r\n {\r\n <div class=\"optionheading\">Native base</div><div class=\"optiondescription\">This saved character uses its original native face and body.</div><button class=\"creatorbutton\" onclick=@EnableBlending>Enable weighted base blending</button>\r\n <div class=\"bodychoices\"><button class=\"creatorbutton\" onclick=@(()=>ChooseBody(\"male\"))>Masculine</button><button class=\"creatorbutton\" onclick=@(()=>ChooseBody(\"female\"))>Feminine</button></div><div class=\"skingrid\">@foreach(var skin in AppearanceCatalog.Skins.Where(x=>x.BodyType==BodyType)){<button class=\"creatorbutton skinchoice\" onclick=@(()=>ChooseSkin(skin))>@FaceLabel(skin)</button>}</div>\r\n }\r\n\r\n <div class=\"optionheading\">Eye color</div><div class=\"optiondescription\">Color the iris while keeping the pupil and whites of the eyes.</div>\r\n <div class=\"colorchoices\"><button class=@($\"creatorbutton colorchoice eyecolorchoice {(!Appearance.CustomEyeColor?\"chosen\":\"\")}\") onclick=@(()=>ChooseEyeColor(null))>Native</button>@foreach(var color in EyeColors){<button class=@($\"creatorbutton colorchoice eyecolorchoice {(Appearance.CustomEyeColor && Appearance.EyeColor==color.Color?\"chosen\":\"\")}\") onclick=@(()=>ChooseEyeColor(color.Color))><div class=\"colorswatch\" style=@($\"background-color: {ColorCss(color.Color)};\")></div><span>@color.Label</span></button>}</div>\r\n <button class=\"creatorbutton quiet eyecustomtoggle\" onclick=@(()=>ChooseEyeColor(Appearance.EyeColor))><span class=\"material-icons\">palette</span>Custom eye color</button>\r\n @if(Appearance.CustomEyeColor){@foreach(var channel in new[]{\"Red\",\"Green\",\"Blue\"}){<div class=\"skinchannel\"><div class=\"shapelabel\">@channel</div><SliderControl class=@($\"shapeslider eye-{channel.ToLowerInvariant()}\") Value=@EyeChannel(channel) OnValueChanged=@((float value)=>SetEyeChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}}\r\n }\r\n\r\n else if(Tab==\"Skin\")\r\n\r\n {\r\n\r\n @if(Appearance.BaseShape is not null){<div class=\"optionheading\">Skin details</div><div class=\"optiondescription\">Choose the native texture set: pigment, pores, freckles and other details. This does not change your face weights or body shape.</div><div class=\"skingrid\">@foreach(var skin in AppearanceCatalog.Skins){<button class=@($\"creatorbutton skinchoice {(Appearance.SkinPath==skin.ResourcePath?\"chosen\":\"\")}\") onclick=@(()=>ChooseSkin(skin))>Skin @skin.Id.Replace(\"human\",\"\")</button>}</div>}\r\n <div class=\"optionheading\">Skin color</div><div class=\"optiondescription\">Choose a complexion independently of face and body. Native restores the selected skin's original color.</div>\r\n\r\n <div class=\"shapecategories\"><button class=@($\"creatorbutton skinpalette {(SkinPalette==\"Human\"?\"chosen\":\"\")}\") onclick=@(()=>SkinPalette=\"Human\")>Human</button><button class=@($\"creatorbutton skinpalette {(SkinPalette==\"Fantasy\"?\"chosen\":\"\")}\") onclick=@(()=>SkinPalette=\"Fantasy\")>Fantasy & custom</button></div>\r\n\r\n <div class=\"colorchoices\">@foreach(var color in SkinPalette==\"Human\"?AppearanceCatalog.HumanSkinColors:AppearanceCatalog.FantasySkinColors){<button class=@($\"creatorbutton colorchoice skincolorchoice skincolor-{color.Id} {(Appearance.SkinColor==color.Color && Appearance.NormalizeSkinTone==color.NormalizeSkinTone?\"chosen\":\"\")}\") onclick=@(()=>ChooseSkinColor(color.Color,color.NormalizeSkinTone)) [email protected]><div class=\"colorswatch\" style=@($\"background-color: {ColorCss(color.Color)};\")></div><span>@color.Label</span></button>}</div>\r\n\r\n @if(SkinPalette==\"Fantasy\")\r\n\r\n {\r\n\r\n <div class=\"fieldlabel colorlabel\">Custom color</div><div class=\"skincolorpreview\"><div class=\"colorswatch\" style=@($\"background-color: {ColorCss(Appearance.SkinColor)};\")></div><span>@($\"RGB {MathF.Round(Appearance.SkinColorR*255)} \u00b7 {MathF.Round(Appearance.SkinColorG*255)} \u00b7 {MathF.Round(Appearance.SkinColorB*255)}\")</span></div>\r\n\r\n @foreach(var channel in new[]{\"Red\",\"Green\",\"Blue\"}){<div class=\"skinchannel\"><div class=\"shapelabel\">@channel</div><SliderControl class=@($\"shapeslider skin-{channel.ToLowerInvariant()}\") Value=@SkinChannel(channel) OnValueChanged=@((float value)=>SetSkinChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}\r\n\r\n }\r\n\r\n <button class=\"creatorbutton quiet resetshape resetskincolor\" onclick=@(()=>ChooseSkinColor(Color.White))><span class=\"material-icons\">restart_alt</span>Reset skin color</button>\r\n\r\n }\r\n\r\n else if(Tab==\"Makeup\")\r\n\r\n {\r\n\r\n <div class=\"optionheading\">Makeup & markings</div><div class=\"optiondescription\">Make it yours. Choose None to leave any makeup layer bare.</div>\r\n\r\n <div class=\"shapecategories\">@foreach(var layer in MakeupCatalog.Layers){<button class=@($\"creatorbutton makeuplayer {(MakeupLayer==layer?\"chosen\":\"\")}\") onclick=@(()=>ChooseMakeupLayer(layer))>@(layer==\"Tattoo\"?\"Markings\":layer)</button>}</div>\r\n\r\n @if(MakeupLayer==\"Tattoo\")\r\n\r\n {\r\n\r\n <div class=\"markinglayers\"><div class=\"markinglist\">@for(var i=0;i<Appearance.Makeup.Markings.Count;i++){var index=i;var mark=Appearance.Makeup.Markings[i];<button class=@($\"creatorbutton markinglayer {(SelectedMarking==i?\"chosen\":\"\")}\") onclick=@(()=>SelectMarking(index))>@($\"{i+1} \u00b7 {MarkingCatalog.Style(mark.Style).Label}\")</button>}</div><button class=\"creatorbutton addmarking\" onclick=@AddMarking tooltip=\"Add an independent marking layer\"><span class=\"material-icons\">add</span>Add marking</button></div>\r\n\r\n @if(CurrentMarking is null)\r\n\r\n {<div class=\"markingempty\"><span class=\"material-icons\">brush</span><div class=\"fieldlabel\">None</div><div class=\"optiondescription\">Add a marking to choose its design, location and ink. Combine up to six.</div></div>}\r\n\r\n else\r\n\r\n {\r\n\r\n <div class=\"markingeditorheading\"><button class=\"creatorbutton quiet inspectmarking\" onclick=@InspectMarking tooltip=\"Focus the preview on this marking\"><span class=\"material-icons\">center_focus_strong</span>@MarkingCatalog.Region(CurrentMarking.Region).Label</button><button class=\"creatorbutton quiet deletemarking\" onclick=@DeleteMarking tooltip=\"Delete this marking layer\"><span class=\"material-icons\">delete_outline</span>Delete layer</button></div>\r\n <div class=\"markingsections\">@foreach(var section in new[]{\"Design\",\"Placement\",\"Ink\"}){<button class=@($\"creatorbutton markingsection {(MarkingSection==section?\"chosen\":\"\")}\") onclick=@(()=>MarkingSection=section)>@section</button>}</div>\r\n\r\n @if(MarkingSection==\"Design\")\r\n\r\n {\r\n\r\n <div class=\"markingdesigns\">@foreach(var style in MarkingCatalog.Styles){<button class=@($\"creatorbutton markingstyle style-{style.Id} {(CurrentMarking.Style==style.Id?\"chosen\":\"\")}\") onclick=@(()=>ChooseMarkingStyle(style.Id))><span class=\"material-icons\">@MarkingSymbol(style.Id)</span><span>@style.Label</span></button>}</div>\r\n\r\n <div class=\"fieldlabel markinglocationlabel\">Location</div><div class=\"markinglocations\">@foreach(var region in MarkingCatalog.Locations(CurrentMarking.Style)){<button class=@($\"creatorbutton markinglocation location-{region.Id} {(CurrentMarking.Region==region.Id?\"chosen\":\"\")}\") onclick=@(()=>ChooseMarkingRegion(region.Id))>@region.Label</button>}</div>\r\n\r\n }\r\n\r\n else if(MarkingSection==\"Placement\")\r\n\r\n {\r\n\r\n <div class=\"optiondescription\">@(MarkingCatalog.Style(CurrentMarking.Style).Band?\"This band wraps around the selected region. Slide it along the body and adjust its size and barb spacing.\":\"Move, turn and resize this marking within the selected region.\")</div>\r\n\r\n <div class=\"markingplacement\">@foreach(var control in MarkingCatalog.Style(CurrentMarking.Style).Band?new[]{\"Vertical\",\"Scale\",\"Spacing\"}:new[]{\"Horizontal\",\"Vertical\",\"Scale\",\"Rotation\"}){<div class=\"skinchannel\"><div class=\"shapelabel\">@(control==\"Vertical\" && MarkingCatalog.Style(CurrentMarking.Style).Band?\"Position along region\":control==\"Scale\"?\"Size\":control==\"Spacing\"?\"Barb spacing\":control)</div><SliderControl class=@($\"shapeslider marking-{control.ToLowerInvariant()}\") Value=@MarkingValue(control) OnValueChanged=@((float value)=>SetMarkingValue(control,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>}</div><button class=\"creatorbutton quiet resetmarkingplacement\" onclick=@ResetMarkingPlacement><span class=\"material-icons\">restart_alt</span>Reset placement</button>\r\n\r\n }\r\n\r\n }\r\n\r\n }\r\n\r\n @if(MakeupLayer!=\"Tattoo\" || CurrentMarking is not null && MarkingSection==\"Ink\")\r\n\r\n {\r\n\r\n <div class=\"colorchoices\">\r\n\r\n @if(MakeupLayer!=\"Tattoo\"){<button class=@($\"creatorbutton colorchoice makeupnone {(!CurrentMakeupActive?\"chosen\":\"\")}\") onclick=@ChooseNoMakeup><span class=\"material-icons\">block</span><span>None</span></button>}\r\n\r\n @foreach(var color in MakeupCatalog.Colors(MakeupLayer)){<button class=@($\"creatorbutton colorchoice makeupcolor makeup-{color.Id} {(CurrentMakeupActive && CurrentMakeup.Color==color.Color?\"chosen\":\"\")}\") onclick=@(()=>ChooseMakeupColor(color.Color)) [email protected]><div class=\"colorswatch\" style=@($\"background-color: {ColorCss(color.Color)};\")></div><span>@color.Label</span></button>}\r\n\r\n </div>\r\n\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>@(MakeupLayer==\"Eyeliner\"?\"Upper eyelid strength\":\"Strength\")</span><span class=\"shapevalue\">@(MathF.Round(CurrentMakeup.Amount*100))%</span></div><SliderControl class=\"shapeslider makeupamount\" [email protected] OnValueChanged=@SetMakeupAmount Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n @if(MakeupLayer==\"Eyeliner\")\r\n {\r\n <div class=\"optiondescription\">Upper and lower liner share one color. Wings follow the upper liner strength; inner-corner liner is optional.</div>\r\n @foreach(var part in new[]{\"Lower eyelid\",\"Inner corner\"})\r\n {\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>@part strength</span><span class=\"shapevalue\">@(MathF.Round(EyelinerStrength(part)*100))%</span></div><SliderControl class=@($\"shapeslider eyeliner-{(part==\"Lower eyelid\"?\"lower\":\"inner\")}\") Value=@EyelinerStrength(part) OnValueChanged=@((float value)=>SetEyelinerStrength(part,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n }\r\n <div class=\"fieldlabel colorlabel\">Wing</div>\r\n <div class=\"choicegrid\">@foreach(var wing in new[]{\"none\",\"wing\"}){<button class=@($\"creatorbutton eyelinerwing {((wing==\"none\")== (Appearance.Makeup.EyelinerWing==\"none\")?\"chosen\":\"\")}\") onclick=@(()=>ChooseEyelinerWing(wing))>@(wing==\"none\"?\"No wing\":\"Wing\")</button>}</div>\r\n @if(Appearance.Makeup.EyelinerWing!=\"none\")\r\n {\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>Wing strength</span><span class=\"shapevalue\">@(MathF.Round(Appearance.Makeup.EyelinerWingStrength*100))%</span></div><SliderControl class=\"shapeslider eyeliner-wing\" [email protected] OnValueChanged=@((float value)=>SetEyelinerStrength(\"Wing\",value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>Wing tip horizontal</span><span class=\"shapevalue\">@(MathF.Round(Appearance.Makeup.WingTipHorizontal*100))%</span></div><SliderControl class=\"shapeslider eyeliner-wing-horizontal\" [email protected] OnValueChanged=@((float value)=>SetWingTip(false,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>Wing tip vertical</span><span class=\"shapevalue\">@(Appearance.Makeup.WingTipVertical==0?\"Level\":$\"{MathF.Round(MathF.Abs(Appearance.Makeup.WingTipVertical)*100)}% {(Appearance.Makeup.WingTipVertical>0?\"up\":\"down\")}\")</span></div><SliderControl class=\"shapeslider eyeliner-wing-vertical\" [email protected] OnValueChanged=@((float value)=>SetWingTip(true,value)) Min=@(-1f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n <div class=\"skinchannel makeupstrength\"><div class=\"shapelabel\"><span>Wing thickness</span><span class=\"shapevalue\">@(MathF.Round(Appearance.Makeup.EyelinerWingThickness*100))%</span></div><SliderControl class=\"shapeslider eyeliner-wing-thickness\" [email protected] OnValueChanged=@SetWingThickness Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>\r\n }\r\n }\r\n\r\n <button class=\"creatorbutton quiet makeupcustomtoggle\" onclick=@(()=>CustomMakeup=!CustomMakeup)><span class=\"material-icons\">palette</span>@(CustomMakeup?\"Hide custom color\":\"Custom color\")</button>\r\n\r\n @if(CustomMakeup){<div class=\"makeupcustomgrid\">@foreach(var channel in new[]{\"Red\",\"Green\",\"Blue\"}){<div class=\"skinchannel\"><div class=\"shapelabel\">@channel</div><SliderControl class=@($\"shapeslider makeup-{channel.ToLowerInvariant()}\") Value=@MakeupChannel(channel) OnValueChanged=@((float value)=>SetMakeupChannel(channel,value)) Min=@(0f) Max=@(1f) Step=@(1f/255) ShowTextEntry=@false /></div>}</div>}\r\n\r\n }\r\n\r\n <div class=\"makeupresets\"><button class=\"creatorbutton quiet clearmakeup\" onclick=@ClearMakeup>Clear all makeup & markings</button></div>\r\n\r\n }\r\n\r\n else if(Tab==\"Clothing\")\r\n {\r\n <div class=\"optionheading\">Your wardrobe</div><div class=\"optiondescription\">Click an item to wear it, click again to remove it. Outfits replace tops and bottoms; jackets and accessories can be layered.</div>\r\n <button class=\"creatorbutton quiet browsecloud\" onclick=@(()=>CloudWardrobeOpen=!CloudWardrobeOpen)>@(CloudWardrobeOpen?\"Close cloud browser\":\"Browse cloud clothing & purchased cosmetics\")</button>\r\n @if(CloudWardrobeOpen){<CloudClothingBrowser Selected=@ChooseCloudClothing Appearance=@Appearance />}\r\n <TextEntry class=\"wardrobesearch\" Value=@WardrobeSearch OnTextEdited=@((string value)=>{WardrobeSearch=value;}) Placeholder=\"Search clothing\" />\r\n <div class=\"shapecategories\">@foreach(var category in WardrobeCatalog.Categories){<button class=@($\"creatorbutton {(WardrobeCategory==category?\"chosen\":\"\")}\") onclick=@(()=>WardrobeCategory=category)>@category</button>}</div>\r\n <div class=\"fieldlabel\">Wearing @WardrobeCatalog.Paths(Appearance).Count() items</div><div class=\"choicegrid\">@foreach(var path in WardrobeCatalog.Paths(Appearance).ToArray()){var worn=ClothingResources.Available.FirstOrDefault(x=>x.Path==path);@if(worn is not null){<button class=\"creatorbutton chosen\" onclick=@(()=>ChooseClothing(worn))>@($\"{worn.Name} \u00d7\")</button>}}</div>\r\n <div class=\"fieldlabel\">@($\"{WardrobeCategory} \u00b7 {VisibleWardrobe.Count()} choices\")</div><div class=\"choicegrid thumbnailgrid\">@foreach(var item in VisibleWardrobe){<button class=@($\"creatorbutton wardrobechoice {((WardrobeCatalog.Paths(Appearance).Contains(item.Path)||Appearance.HairPath==item.Path||Appearance.FacialHairPath==item.Path)?\"chosen\":\"\")}\") onclick=@(()=>ChooseClothing(item))><div class=\"thumbnailframe\">@if(ClothingThumbnails.For(item.Path) is { Length: > 0 } icon){<div class=\"itemthumbnail\" style=@($\"background-image: url('{icon}');\")></div>}else{<span class=\"material-icons thumbnailplaceholder\">block</span>}</div><span class=\"itemlabel\">@item.Name</span></button>}</div>\r\n @if(!VisibleWardrobe.Any()){<div class=\"optiondescription\">No clothing matches your search in this category.</div>}\r\n <div class=\"choicegrid\"><button class=\"creatorbutton quiet\" onclick=@ClearClothing>Remove clothing</button><button class=\"creatorbutton quiet\" onclick=@ResetClothing>Default outfit</button></div>\r\n }\r\n else if(Tab==\"Hair\")\r\n\r\n {\r\n\r\n <div class=\"optionheading\">Hair</div><div class=\"choicegrid thumbnailgrid\">@foreach(var hair in AvailableHair){<button class=@($\"creatorbutton hairchoice {(Appearance.HairPath==hair.ResourcePath?\"chosen\":\"\")}\") onclick=@(()=>ChooseHair(hair))><div class=\"thumbnailframe\">@if(ClothingThumbnails.For(hair.ResourcePath) is { Length: > 0 } icon){<div class=\"itemthumbnail\" style=@($\"background-image: url('{icon}');\")></div>}else{<span class=\"material-icons thumbnailplaceholder\">block</span>}</div><span class=\"itemlabel\">@hair.Label</span></button>}</div><div class=\"fieldlabel colorlabel\">Hair color</div><div class=\"colorchoices\">@foreach(var color in AppearanceCatalog.Colors){<button class=@($\"creatorbutton colorchoice {(SameColor(color.Color)?\"chosen\":\"\")}\") onclick=@(()=>ChooseColor(color)) [email protected]><div class=\"colorswatch\" style=@($\"background-color: {ColorCss(color.Color)};\")></div><span>@color.Label</span></button>}</div><div class=\"fieldlabel colorlabel\">Facial hair</div><div class=\"choicegrid thumbnailgrid\">@foreach(var facial in AvailableFacialHair.Where(x=>x.BodyType==\"\"||x.BodyType==BodyType)){<button class=@($\"creatorbutton facialchoice {(Appearance.FacialHairPath==facial.ResourcePath?\"chosen\":\"\")}\") onclick=@(()=>ChooseFacial(facial))><div class=\"thumbnailframe\">@if(ClothingThumbnails.For(facial.ResourcePath) is { Length: > 0 } icon){<div class=\"itemthumbnail\" style=@($\"background-image: url('{icon}');\")></div>}else{<span class=\"material-icons thumbnailplaceholder\">block</span>}</div><span class=\"itemlabel\">@facial.Label</span></button>}</div>\r\n\r\n }\r\n\r\n else if(Tab==\"Shape\")\r\n\r\n {\r\n\r\n <div class=\"optionheading\">Refine your look</div><div class=\"optiondescription\">Neutral is 50%; ear taper and belly start at 0%.</div>\r\n\r\n <div class=\"shapecategories\">@foreach(var category in AppearanceCatalog.Sliders.Select(x=>x.Group).Distinct()){<button class=@($\"creatorbutton shapecategory {(ShapeCategory==category?\"chosen\":\"\")}\") onclick=@(()=>ChangeShapeCategory(category))>@category</button>}</div>\r\n\r\n @if(ShapeCategory==\"Body\" && Appearance.BaseShape is not null)\r\n {\r\n @foreach(var breast in new[]{true,false}){<div class=\"shaperow\"><div class=\"shapelabel\"><span>@(breast?\"Breast size\":\"Bulge size\")</span><span>@MathF.Round((breast?1-Appearance.BaseShape.MaleBody:Appearance.BaseShape.MaleBody)*100)% effect</span><button class=\"creatorbutton quiet\" onclick=@(()=>SetBodyFeature(breast,.5f))>Reset</button></div><SliderControl class=@($\"shapeslider {(breast?\"breast\":\"bulge\")}-size\") Value=@(breast?Appearance.BaseShape.BreastSize:Appearance.BaseShape.BulgeSize) OnValueChanged=@((float value)=>SetBodyFeature(breast,value)) Min=@(0f) Max=@(1f) Step=@(.01f) ShowTextEntry=@false /></div>}\r\n }\r\n <div class=\"shapegrid\">@foreach(var slider in AppearanceCatalog.Sliders.Where(x=>x.Group==ShapeCategory && (Appearance.BaseShape is null || x.Id!=\"bodyfeature\")))\r\n\r\n {\r\n\r\n <div class=@($\"shaperow shape-{slider.Id}\")><div class=\"shapelabel\"><span>@slider.LabelFor(Appearance)</span><div class=\"shapeactions\"><span class=\"shapevalue\">@(MathF.Round(slider.Get(Appearance)*100))%</span><button class=\"creatorbutton quiet resetslider\" onclick=@(()=>SetShape(slider,slider.Neutral)) tooltip=@($\"Reset {slider.LabelFor(Appearance).ToLowerInvariant()} to {MathF.Round(slider.Neutral*100)}%\")><span class=\"material-icons\">restart_alt</span></button></div></div><SliderControl class=\"shapeslider\" [email protected](Appearance) OnValueChanged=@((float value)=>SetShape(slider,value)) [email protected] [email protected] Step=@(0.01f) ShowTextEntry=@false />@if(slider.Id==\"columella\"){<div class=\"fitnesshint\">Width between the nostrils</div>}@if(slider.Id==\"philtrum\"){<div class=\"fitnesshint\">Width between nose and upper lip</div>}@if(slider.Id==\"eartaper\"){<div class=\"fitnesshint\">Normal \u00b7 Pointed</div>}@if(slider.Id==\"fitness\"){<div class=\"fitnesshint\">Soft \u00b7 Neutral \u00b7 Muscular</div>}@if(slider.Id==\"nostrils\"){<div class=\"fitnesshint\">Width of the nostril wings</div>}@if(slider.Id==\"nostrilfullness\"){<div class=\"fitnesshint\">Smaller \u00b7 Neutral \u00b7 Fuller wings</div>}@if(slider.Id==\"noseheight\" || slider.Id==\"chinheight\"){<div class=\"fitnesshint\">Lower \u00b7 Neutral \u00b7 Higher</div>}</div>\r\n\r\n }</div>\r\n\r\n <button class=\"creatorbutton quiet resetshape\" onclick=@ResetShapes>Reset body & face</button>\r\n\r\n }\r\n\r\n @if(ExtraTabs.Contains(Tab) && ExtraContent is not null){@ExtraContent(this)}\r\n\r\n </div>\r\n\r\n <div class=\"creatorutility\"><button class=\"creatorbutton quiet\" onclick=@Randomize><span class=\"material-icons\">shuffle</span>Randomize</button><button class=\"creatorbutton quiet\" onclick=@Reset><span class=\"material-icons\">restart_alt</span>Reset appearance</button></div>\r\n\r\n </div>\r\n\r\n <div class=\"creatorpresentation\"><div class=\"previewheading\"><div class=\"previewname\">@(string.IsNullOrWhiteSpace(CharacterName)?\"Your character\":CharacterName.Trim())</div><div class=\"previewcaption\">@(PreviewPan.Length>0.01f || MathF.Abs(PreviewPitch)>0.01f?\"Free inspection\":Zoom>CharacterCreatorPreview.FaceZoom+0.001f?\"Face detail\":Portrait?\"Face preview\":\"Character preview\")</div></div><CharacterCreatorPreview @ref=Preview class=\"mainpreview\" Hero=@true Appearance=@Appearance Portrait=@Portrait Zoom=@Zoom Yaw=@Yaw Pitch=@PreviewPitch PitchChanged=@SetPitch PanOffset=@PreviewPan PanChanged=@SetPan Interactive=@(!LoadingCatalog) OrbitChanged=@SetYaw ZoomChanged=@SetZoom ShowClothing=@ShowClothing /><div class=\"previewcontrols\"><div class=\"previewviewrow\"><button class=\"creatorbutton quiet rotateleft\" onclick=@(()=>Rotate(-30)) tooltip=\"Turn left\"><span class=\"material-icons\">rotate_left</span></button><button class=@($\"creatorbutton bodyview {(Zoom==0?\"chosen\":\"\")}\") onclick=@(()=>SetView(0))>Full body</button><button class=@($\"creatorbutton previewzoom {(MathF.Abs(Zoom-CharacterCreatorPreview.FaceZoom)<0.001f?\"chosen\":\"\")}\") onclick=@(()=>SetView(CharacterCreatorPreview.FaceZoom))>Face view</button><button class=\"creatorbutton quiet rotateright\" onclick=@(()=>Rotate(30)) tooltip=\"Turn right\"><span class=\"material-icons\">rotate_right</span></button></div><button class=\"creatorbutton quiet clothingtoggle\" onclick=@ToggleClothing><span class=\"material-icons\">@(ShowClothing?\"visibility_off\":\"visibility\")</span>@(ShowClothing?\"Hide clothing\":\"Show clothing\")</button></div><div class=\"previewzoomrow\"><span>Zoom</span><SliderControl class=\"customzoom\" Value=@Zoom OnValueChanged=@SetZoom Min=@(0f) [email protected] Step=@(0.005f) ShowTextEntry=@false /></div><div class=\"previewrecenter\"><button class=\"creatorbutton quiet recenterpreview\" onclick=@RecenterPreview><span class=\"material-icons\">center_focus_strong</span>Recenter preview</button></div><div class=\"previewhint\">Left drag: turn \u00b7 Right: tilt \u00b7 Middle: pan \u00b7 Scroll: zoom</div></div>\r\n\r\n </div>\r\n\r\n <div class=\"creatorfooter\"><div class=\"creatoridentity\"><div class=\"identityfields\"><div class=\"namefield\"><div class=\"fieldlabel\">Character name</div><TextEntry class=\"charactername\" Value=@CharacterName OnTextEdited=@SetCharacterName MaxLength=@(40) Placeholder=\"Your character's name\" /></div>@FooterContent</div></div><div class=\"creatorfeedback\">@(Feedback??(LoadingCatalog?\"Loading character options\u2026\":Preview?.Error)??(\"\"))</div><button class=\"creatorbutton primary startplaythrough\" disabled=@Disabled onclick=@Start><span class=\"material-icons\">play_arrow</span>@(SaveLabel)</button></div>\r\n\r\n </div>\r\n\r\n }\r\n\r\n</root>\r\n\r\n@code {\r\n\t/// <summary>Consumer-specific pages; built-in categories remain owned by Character Kit.</summary>\r\n\tpublic IReadOnlyList<string> ExtraTabs { get; set; } = Array.Empty<string>();\r\n\tpublic IReadOnlyList<string> HiddenTabs { get; set; } = Array.Empty<string>();\r\n\tpublic RenderFragment<CharacterCreatorPanel> ExtraContent { get; set; }\r\n\tpublic RenderFragment FooterContent { get; set; }\r\n\tpublic Action StopPreview { get; set; }\r\n\tpublic bool Disabled { get; set; }\r\n\tpublic string ProductLabel { get; set; } = \"Character Editor\";\r\n\tpublic string LocationLabel { get; set; } = \"Studio\";\r\n\tpublic string Title { get; set; } = \"Create your character\";\r\n\tpublic CharacterAppearanceData EditableAppearance => Appearance;\r\n\tpublic SkinnedModelRenderer PreviewRenderer => Preview?.Body;\r\n\tpublic string SelectedTab => Tab;\r\n\tpublic string SaveLabel { get; set; } = \"Save character\";\r\n\tpublic Func<string, CharacterAppearanceData, string> Saved { get; set; }\r\n\tpublic Action Closed { get; set; }\r\n\tpublic CharacterPreset InitialPreset { get; set; }\r\n\r\n\tCharacterPreset loadedPreset;\r\n\tpublic CharacterAppearanceData DraftAppearance => Appearance.DeepCopy();\r\n\tpublic string DraftCharacterName => CharacterName;\r\n\r\n\tpublic void LoadDraft(string name, CharacterAppearanceData data)\r\n\t{\r\n\t\tCharacterName = name;\r\n\t\tAppearance = data.DeepCopy();\r\n\t\tChosenPreset = null;\r\n\t\tTab = \"Face & body\";\r\n\t}\r\n\r\n\tpublic bool AppearanceReady => CatalogReady && Preview?.Loading != true && CharacterAppearance.ValidateForApply(Appearance)is null;\r\n\r\n\tIReadOnlyList<string> Tabs => new[]\r\n\t{\r\n\t\t\"Presets\",\r\n\t\t\"Skin\",\r\n\t\t\"Face & body\",\r\n\t\t\"Photos\",\r\n\t\t\"Hair\",\r\n\t\t\"Clothing\",\r\n\t\t\"Shape\",\r\n\t\t\"Makeup\"\r\n\t}.Where(x => !HiddenTabs.Contains(x)).Concat(ExtraTabs).Distinct().ToArray();\r\n\r\n\tbool CloudWardrobeOpen;\r\n\tstring WardrobeSearch = \"\", WardrobeCategory = \"Tops\";\r\n\tIEnumerable<WardrobeItem> VisibleWardrobe => ClothingResources.Available.Where(x => x.Category == WardrobeCategory && (string.IsNullOrWhiteSpace(WardrobeSearch) || x.Name.Contains(WardrobeSearch, StringComparison.OrdinalIgnoreCase))).OrderBy(x => x.Name);\r\n\r\n\tvoid ChooseCloudClothing(WardrobeItem item)\r\n\t{\r\n\t\tWardrobeCategory = item.Category;\r\n\t\tWardrobeSearch = \"\";\r\n\t\tChooseClothing(item);\r\n\t\tAppearance.Version = 2;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t\tStateHasChanged();\r\n\t}\r\n\r\n\tvoid ChooseClothing(WardrobeItem item)\r\n\t{\r\n\t\tvar clothing = ClothingResources.Get(item.Path);\r\n\t\tif (clothing is not null && ClothingResources.IsHair(clothing))\r\n\t\t\tAppearance.HairPath = Appearance.HairPath == item.Path || ClothingResources.Get(Appearance.HairPath) == clothing ? \"\" : item.Path;\r\n\t\telse if (clothing is not null && ClothingResources.IsFacialHair(clothing))\r\n\t\t\tAppearance.FacialHairPath = Appearance.FacialHairPath == item.Path || ClothingResources.Get(Appearance.FacialHairPath) == clothing ? \"\" : item.Path;\r\n\t\telse\r\n\t\t\tWardrobeCatalog.Toggle(Appearance, item);\r\n\t\tAppearance.Version = 2;\r\n\t\tShowClothing = true;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ClearClothing()\r\n\t{\r\n\t\tAppearance.ClothingPaths = Array.Empty<string>();\r\n\t\tAppearance.Version = 2;\r\n\t\tShowClothing = true;\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid ResetClothing()\r\n\t{\r\n\t\tAppearance.ClothingPaths = WardrobeCatalog.DefaultPaths.ToArray();\r\n\t\tAppearance.Version = 2;\r\n\t\tShowClothing = true;\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tCharacterAppearanceData Appearance = AppearanceCatalog.CreateDefault();\r\n\tstring CharacterName = \"Rowan\", PlaythroughName = \"A new adventure\", Tab = \"Presets\", ShapeCategory = \"Body\", ChosenPreset = \"traveller\", Feedback;\r\n\tstring BlendRegion = \"overall\";\r\n\tbool CatalogReady, LoadingCatalog;\r\n\tstring SkinPalette = \"Human\";\r\n\tstring MakeupLayer = \"Lipstick\";\r\n\tbool CustomMakeup;\r\n\tint SelectedMarking;\r\n\tstring MarkingSection = \"Design\";\r\n\tCharacterMarkingData CurrentMarking => Appearance.Makeup.Markings.Count == 0 ? null : Appearance.Makeup.Markings[Math.Clamp(SelectedMarking, 0, Appearance.Makeup.Markings.Count - 1)];\r\n\r\n\tMakeupLayerData CurrentMakeup => MakeupLayer == \"Tattoo\" ? CurrentMarking?.Ink ?? Appearance.Makeup.Tattoo : MakeupCatalog.Layer(Appearance.Makeup, MakeupLayer);\r\n\r\n\tvoid ChooseMakeupLayer(string layer)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tMakeupLayer = layer;\r\n\t\tif (layer == \"Tattoo\")\r\n\t\t\tAppearance.Makeup.EditMarkings();\r\n\t}\r\n\r\n\tvoid SelectMarking(int index)\r\n\t{\r\n\t\tif (LoadingCatalog || index < 0 || index >= Appearance.Makeup.Markings.Count)\r\n\t\t\treturn;\r\n\t\tSelectedMarking = index;\r\n\t}\r\n\r\n\tvoid AddMarking()\r\n\t{\r\n\t\tif (LoadingCatalog || Appearance.Makeup.Markings.Count >= MarkingCatalog.Maximum)\r\n\t\t{\r\n\t\t\tFeedback = \"You can combine up to six marking layers.\";\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar marking = new CharacterMarkingData();\r\n\t\tMarkingCatalog.ResetPosition(marking);\r\n\t\tAppearance.Makeup.Markings.Add(marking);\r\n\t\tSelectedMarking = Appearance.Makeup.Markings.Count - 1;\r\n\t\tMarkingSection = \"Design\";\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid DeleteMarking()\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null)\r\n\t\t\treturn;\r\n\t\tAppearance.Makeup.Markings.Remove(CurrentMarking);\r\n\t\tSelectedMarking = Math.Clamp(SelectedMarking, 0, Math.Max(0, Appearance.Makeup.Markings.Count - 1));\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tstring MarkingSymbol(string style) => style switch\r\n\t{\r\n\t\t\"crescent\" => \"brightness_2\",\r\n\t\t\"teardrop\" => \"water_drop\",\r\n\t\t\"star\" => \"star_outline\",\r\n\t\t\"rune\" => \"gesture\",\r\n\t\t\"barbed-band\" => \"link\",\r\n\t\t_ => \"block\"\r\n\t};\r\n\tvoid ChooseMarkingStyle(string style)\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null)\r\n\t\t\treturn;\r\n\t\tMarkingCatalog.ChangeStyle(CurrentMarking, style);\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid ChooseMarkingRegion(string region)\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null)\r\n\t\t\treturn;\r\n\t\tMarkingCatalog.ChangeRegion(CurrentMarking, region);\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tfloat MarkingValue(string control) => control switch\r\n\t{\r\n\t\t\"Horizontal\" => CurrentMarking.Horizontal,\r\n\t\t\"Vertical\" => CurrentMarking.Vertical,\r\n\t\t\"Scale\" => CurrentMarking.Scale,\r\n\t\t\"Spacing\" => CurrentMarking.Spacing,\r\n\t\t_ => CurrentMarking.Rotation\r\n\t};\r\n\tvoid SetMarkingValue(string control, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tvalue = Math.Clamp(value, 0, 1);\r\n\t\tif (MarkingCatalog.Style(CurrentMarking.Style).Band && control is not \"Vertical\" and not \"Scale\" and not \"Spacing\")\r\n\t\t\treturn;\r\n\t\tif (control == \"Horizontal\")\r\n\t\t\tCurrentMarking.Horizontal = value;\r\n\t\telse if (control == \"Vertical\")\r\n\t\t\tCurrentMarking.Vertical = value;\r\n\t\telse if (control == \"Scale\")\r\n\t\t\tCurrentMarking.Scale = value;\r\n\t\telse if (control == \"Spacing\")\r\n\t\t\tCurrentMarking.Spacing = value;\r\n\t\telse if (control == \"Rotation\")\r\n\t\t\tCurrentMarking.Rotation = value;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid ResetMarkingPlacement()\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null)\r\n\t\t\treturn;\r\n\t\tMarkingCatalog.ResetPosition(CurrentMarking);\r\n\t\tCurrentMarking.Scale = .5f;\r\n\t\tCurrentMarking.Rotation = .5f;\r\n\t\tCurrentMarking.Spacing = .5f;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid InspectMarking()\r\n\t{\r\n\t\tif (LoadingCatalog || CurrentMarking is null)\r\n\t\t\treturn;\r\n\t\tvar region = MarkingCatalog.Region(CurrentMarking.Region);\r\n\t\tvar center = MarkingCatalog.Focus(CurrentMarking);\r\n\t\tvar outward = MarkingCatalog.Outward(CurrentMarking);\r\n\t\tYaw = -MathF.Atan2(outward.y, outward.x) * 180 / MathF.PI;\r\n\t\tSetZoom(region.Head ? 1.55f : 1.15f);\r\n\t\tvar rotated = Rotation.FromYaw(Yaw) * center;\r\n\t\tvar targetHeight = 62 + 1.5f * MathF.Max(0, Zoom - 1);\r\n\t\tPreviewPan = new(rotated.x, rotated.y, rotated.z - targetHeight);\r\n\t\tPreviewPitch = 0;\r\n\t}\r\n\r\n\tvoid ChangedCosmetics()\r\n\t{\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ChooseNoMakeup()\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tCurrentMakeup.Amount = 0;\r\n\t\tif (MakeupLayer == \"Eyeliner\") MakeupCatalog.Reset(Appearance.Makeup, \"Eyeliner\");\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid ChooseMakeupColor(Color color)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tCurrentMakeup.Color = color;\r\n\t\tif (!CurrentMakeupActive)\r\n\t\t\tCurrentMakeup.Amount = MakeupLayer == \"Eyeliner\" ? 1f : .65f;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid SetMakeupAmount(float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tCurrentMakeup.Amount = Math.Clamp(value, 0, 1);\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tbool CurrentMakeupActive => MakeupLayer == \"Eyeliner\" ? Appearance.Makeup.EyelinerActive : CurrentMakeup.Amount > 0;\r\n\r\n\tfloat EyelinerStrength(string part) => part == \"Lower eyelid\" ? Appearance.Makeup.EyelinerLowerStrength : part == \"Wing\" ? Appearance.Makeup.EyelinerWingStrength : Appearance.Makeup.EyelinerInnerStrength;\r\n\tvoid SetEyelinerStrength(string part, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value)) return;\r\n\t\tvalue = Math.Clamp(value, 0, 1);\r\n\t\tif (part == \"Lower eyelid\") Appearance.Makeup.EyelinerLowerStrength = value;\r\n\t\telse if (part == \"Wing\") Appearance.Makeup.EyelinerWingStrength = value;\r\n\t\telse Appearance.Makeup.EyelinerInnerStrength = value;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid PrepareAdjustableWing()\r\n\t{\r\n\t\tvar makeup = Appearance.Makeup;\r\n\t\tmakeup.EyelinerWingHorizontal = makeup.WingTipHorizontal;\r\n\t\tmakeup.EyelinerWingVertical = makeup.WingTipVertical;\r\n\t\tmakeup.EyelinerWing = \"wing\";\r\n\t}\r\n\r\n\tvoid SetWingTip(bool vertical, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value)) return;\r\n\t\tPrepareAdjustableWing();\r\n\t\tif (vertical) Appearance.Makeup.EyelinerWingVertical = Math.Clamp(value, -1, 1);\r\n\t\telse Appearance.Makeup.EyelinerWingHorizontal = Math.Clamp(value, 0, 1);\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid SetWingThickness(float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value)) return;\r\n\t\tPrepareAdjustableWing();\r\n\t\tAppearance.Makeup.EyelinerWingThickness = Math.Clamp(value, 0, 1);\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid ChooseEyelinerWing(string wing)\r\n\t{\r\n\t\tif (LoadingCatalog) return;\r\n\t\tif (wing == \"wing\") PrepareAdjustableWing();\r\n\t\telse Appearance.Makeup.EyelinerWing = \"none\";\r\n\t\tif (wing != \"none\" && CurrentMakeup.Amount == 0) CurrentMakeup.Amount = 1;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tfloat MakeupChannel(string channel) => channel == \"Red\" ? CurrentMakeup.Red : channel == \"Green\" ? CurrentMakeup.Green : CurrentMakeup.Blue;\r\n\tvoid SetMakeupChannel(string channel, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tvalue = Math.Clamp(value, 0, 1);\r\n\t\tif (channel == \"Red\")\r\n\t\t\tCurrentMakeup.Red = value;\r\n\t\telse if (channel == \"Green\")\r\n\t\t\tCurrentMakeup.Green = value;\r\n\t\telse\r\n\t\t\tCurrentMakeup.Blue = value;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tvoid ClearMakeup()\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.Makeup = new();\r\n\t\tSelectedMarking = 0;\r\n\t\tChangedCosmetics();\r\n\t}\r\n\r\n\tbool Portrait = true, ShowClothing = true;\r\n\tfloat Yaw, Zoom = CharacterCreatorPreview.FaceZoom;\r\n\tCharacterCreatorPreview Preview;\r\n\tVector3 PreviewPan;\r\n\tfloat PreviewPitch;\r\n\tstring BodyType => AppearanceCatalog.IsFemale(Appearance) ? \"female\" : \"male\";\r\n\r\n\tIEnumerable<AppearanceOption> AvailableHair => AppearanceCatalog.Hair.Concat(ClothingResources.Available.Where(x=>x.Category==\"Hair\").Select(x=>new AppearanceOption(x.Path,x.Name,x.Path))).DistinctBy(x=>x.ResourcePath);\r\n IEnumerable<AppearanceOption> AvailableFacialHair => AppearanceCatalog.FacialHair.Concat(ClothingResources.Available.Where(x=>x.Category==\"Facial hair\").Select(x=>new AppearanceOption(x.Path,x.Name,x.Path))).DistinctBy(x=>x.ResourcePath);\r\n string HairLabel => AppearanceCatalog.Hair.FirstOrDefault(x => x.ResourcePath == Appearance.HairPath)?.Label ?? \"Hair\";\r\n\r\n\tstring FaceLabel(AppearanceOption option) => option.Label.Replace(\" \u00b7 male\", \"\").Replace(\" \u00b7 female\", \"\");\r\n\tstring ColorCss(Color color) => $\"rgb({(int)(color.r * 255)},{(int)(color.g * 255)},{(int)(color.b * 255)})\";\r\n\tvoid SetCharacterName(string value)\r\n\t{\r\n\t\tCharacterName = value;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid SetPlaythroughName(string value)\r\n\t{\r\n\t\tPlaythroughName = value;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tbool SameColor(Color color) => MathF.Abs(Appearance.HairColorR - color.r) < 0.001f && MathF.Abs(Appearance.HairColorG - color.g) < 0.001f && MathF.Abs(Appearance.HairColorB - color.b) < 0.001f;\r\n\tvoid ChangeTab(string tab)\r\n\t{\r\n\t\tif (tab != Tab)\n\t\t\tStopVoicePreview();\r\n\t\tTab = tab;\r\n\t\tif (tab == \"Clothing\")\r\n\t\t\tSetZoom(0);\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid StopVoicePreview()\r\n\t{\r\n\t\tStopPreview?.Invoke();\r\n\t}\r\n\r\n\tvoid ChangeShapeCategory(string category)\r\n\t{\r\n\t\tShapeCategory = category;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ApplyPreset(string id)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tvar voice = Appearance.VoiceId;\r\n\t\tbool unified = Appearance.BaseShape is not null;\r\n\t\tAppearance = AppearanceCatalog.ApplyPreset(id);\r\n\t\tif (unified)\r\n\t\t\tUnifiedHuman.Enable(Appearance);\r\n\t\tAppearance.VoiceId = voice;\r\n\t\tChosenPreset = id;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid EnableBlending()\r\n\t{\r\n\t\tUnifiedHuman.Enable(Appearance);\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid AddPhotoFace(PhotoFace face)\r\n\t{\r\n\t\tif (!face.Valid)\r\n\t\t\treturn;\r\n\t\tUnifiedHuman.Enable(Appearance);\r\n\t\tif (Appearance.BaseShape.PhotoFaces.Count >= 4)\r\n\t\t\treturn;\r\n\t\tArray.Fill(Appearance.BaseShape.HeadWeights, 0f);\r\n\t\tforeach (var old in Appearance.BaseShape.PhotoFaces)\r\n\t\t\told.Influence = 0;\r\n\t\tAppearance.BaseShape.PhotoFaces.Add(face);\r\n\t\tChosenPreset = null;\r\n\t\tTab = \"Face & body\";\r\n\t\tBlendRegion = \"overall\";\r\n\t\tSetView(CharacterCreatorPreview.FaceZoom);\r\n\t\tFeedback = \"Custom base added. Blend it here or refine it in Shape, then save your character.\";\r\n\t}\r\n\r\n\tFaceRegionBlend Region(bool create = false)\r\n\t{\r\n\t\tif (BlendRegion == \"overall\")\r\n\t\t\treturn null;\r\n\t\tif (!Appearance.BaseShape.Regions.TryGetValue(BlendRegion, out var value) && create)\r\n\t\t{\r\n\t\t\tvalue = new()\r\n\t\t\t{\r\n\t\t\t\tOverallInfluence = .5f\r\n\t\t\t};\r\n\t\t\tAppearance.BaseShape.Regions.Add(BlendRegion, value);\r\n\t\t}\r\n\r\n\t\treturn value;\r\n\t}\r\n\r\n\tfloat OverallWeight => Region()?.OverallInfluence ?? .5f;\r\n\r\n\tvoid SetOverallWeight(float value)\r\n\t{\r\n\t\tif (!float.IsFinite(value) || BlendRegion == \"overall\")\r\n\t\t\treturn;\r\n\t\tRegion(true).OverallInfluence = Math.Clamp(value, 0, 1);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tfloat HeadWeight(int index) => BlendRegion == \"overall\" ? Appearance.BaseShape.HeadWeights[index] : Region()?.HeadWeights[index] ?? 0;\r\n\tfloat PhotoWeight(PhotoFace face) => BlendRegion == \"overall\" ? face.Influence : Region()?.PhotoWeights.GetValueOrDefault(face.Id) ?? 0;\r\n\tstring BlendWeightLabel(int index)\r\n\t{\r\n\t\tfloat share = FaceRegions.NativeShare(Appearance.BaseShape, index, BlendRegion);\r\n\t\treturn $\"{MathF.Round(HeadWeight(index) * 100)} influence \u00b7 {MathF.Round(share * 100)}% of blend\";\r\n\t}\r\n\r\n\tvoid SetHeadWeight(int index, float value)\r\n\t{\r\n\t\tif (Appearance.BaseShape is null || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\t(BlendRegion == \"overall\" ? Appearance.BaseShape.HeadWeights : Region(true).HeadWeights)[index] = Math.Clamp(value, 0, 1);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid SetPhotoWeight(PhotoFace face, float value)\r\n\t{\r\n\t\tif (!float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tif (BlendRegion == \"overall\")\r\n\t\t\tface.Influence = Math.Clamp(value, 0, 1);\r\n\t\telse\r\n\t\t\tRegion(true).PhotoWeights[face.Id] = Math.Clamp(value, 0, 1);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid ResetRegion()\r\n\t{\r\n\t\tif (BlendRegion != \"overall\")\r\n\t\t\tAppearance.BaseShape.Regions.Remove(BlendRegion);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid SetBodyBlend(float value)\r\n\t{\r\n\t\tif (Appearance.BaseShape is null || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tAppearance.BaseShape.MaleBody = Math.Clamp(value, 0, 1);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid ToggleClothingVariant(string variant)\r\n\t{\r\n\t\tif (Appearance.BaseShape is null)\r\n\t\t\treturn;\r\n\t\tAppearance.BaseShape.ClothingVariant = Appearance.BaseShape.ClothingVariant == variant ? null : variant;\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid SetBodyFeature(bool breast, float value)\r\n\t{\r\n\t\tif (Appearance.BaseShape is null || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tif (breast)\r\n\t\t\tAppearance.BaseShape.BreastSize = Math.Clamp(value, 0, 1);\r\n\t\telse\r\n\t\t\tAppearance.BaseShape.BulgeSize = Math.Clamp(value, 0, 1);\r\n\t\tChosenPreset = null;\r\n\t}\r\n\r\n\tvoid ChooseBody(string body)\r\n\t{\r\n\t\tvar skin = AppearanceCatalog.Skins.FirstOrDefault(x => x.BodyType == body);\r\n\t\tif (skin is not null)\r\n\t\t\tChooseSkin(skin);\r\n\t}\r\n\r\n\tvoid ChooseSkin(AppearanceOption skin)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.SkinPath = skin.ResourcePath;\r\n\t\tif (Appearance.BaseShape is null && !AppearanceCatalog.FacialHair.Any(x => x.ResourcePath == Appearance.FacialHairPath && (x.BodyType == \"\" || x.BodyType == skin.BodyType)))\r\n\t\t\tAppearance.FacialHairPath = \"\";\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ChooseHair(AppearanceOption hair)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.HairPath = hair.ResourcePath;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ChooseFacial(AppearanceOption facial)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.FacialHairPath = facial.ResourcePath;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ChooseColor(AppearanceColorOption color)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.HairColor = color.Color;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ChooseSkinColor(Color color, bool normalize = false)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.SkinColor = color;\r\n\t\tAppearance.NormalizeSkinTone = normalize;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tstatic IReadOnlyList<AppearanceColorOption> EyeColors => new[]\r\n\t{\r\n\t\tnew AppearanceColorOption(\"brown\", \"Brown\", new(.24f, .12f, .055f)),\r\n\t\tnew(\"hazel\", \"Hazel\", new(.34f, .3f, .11f)),\r\n\t\tnew(\"amber\", \"Amber\", new(.58f, .34f, .075f)),\r\n\t\tnew(\"green\", \"Green\", new(.13f, .34f, .19f)),\r\n\t\tnew(\"blue\", \"Blue\", new(.09f, .3f, .55f)),\r\n\t\tnew(\"grey\", \"Grey\", new(.32f, .36f, .4f)),\r\n\t\tnew(\"violet\", \"Violet\", new(.32f, .16f, .46f))\r\n\t};\r\n\r\n\tvoid ChooseEyeColor(Color? color)\r\n\t{\r\n\t\tif (LoadingCatalog || LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.CustomEyeColor = color.HasValue;\r\n\t\tif (color.HasValue)\r\n\t\t\tAppearance.EyeColor = color.Value;\r\n\t}\r\n\r\n\tfloat EyeChannel(string channel) => channel == \"Red\" ? Appearance.EyeColorR : channel == \"Green\" ? Appearance.EyeColorG : Appearance.EyeColorB;\r\n\tvoid SetEyeChannel(string channel, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearance.CustomEyeColor = true;\r\n\t\tvalue = Math.Clamp(value, 0, 1);\r\n\t\tif (channel == \"Red\")\r\n\t\t\tAppearance.EyeColorR = value;\r\n\t\telse if (channel == \"Green\")\r\n\t\t\tAppearance.EyeColorG = value;\r\n\t\telse\r\n\t\t\tAppearance.EyeColorB = value;\r\n\t}\r\n\r\n\tfloat SkinChannel(string channel) => channel == \"Red\" ? Appearance.SkinColorR : channel == \"Green\" ? Appearance.SkinColorG : Appearance.SkinColorB;\r\n\tvoid SetSkinChannel(string channel, float value)\r\n\t{\r\n\t\tif (LoadingCatalog || !float.IsFinite(value))\r\n\t\t\treturn;\r\n\t\tvalue = Math.Clamp(value, 0, 1);\r\n\t\tAppearance.NormalizeSkinTone = true;\r\n\t\tif (channel == \"Red\")\r\n\t\t\tAppearance.SkinColorR = value;\r\n\t\telse if (channel == \"Green\")\r\n\t\t\tAppearance.SkinColorG = value;\r\n\t\telse\r\n\t\t\tAppearance.SkinColorB = value;\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid SetShape(AppearanceSlider slider, float value)\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tslider.Set(Appearance, value);\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid ResetShapes()\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tAppearanceCatalog.ResetShapes(Appearance);\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid Randomize()\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tvar voice = Appearance.VoiceId;\r\n\t\tvar variant = Appearance.BaseShape?.ClothingVariant;\r\n\t\tvar photos = Appearance.BaseShape?.PhotoFaces.Select(x => x.DeepCopy()).ToList();\r\n\t\tAppearance = AppearanceCatalog.Randomize(blendHeads: Appearance.BaseShape is not null);\r\n\t\tAppearance.VoiceId = voice;\r\n\t\tif (Appearance.BaseShape is not null)\r\n\t\t{\r\n\t\t\tAppearance.BaseShape.ClothingVariant = variant;\r\n\t\t\tif (photos is not null)\r\n\t\t\t{\r\n\t\t\t\tforeach (var face in photos)\r\n\t\t\t\t\tface.Influence = 0;\r\n\t\t\t\tAppearance.BaseShape.PhotoFaces = photos;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tChosenPreset = null;\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid Reset()\r\n\t{\r\n\t\tif (LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tvar voice = Appearance.VoiceId;\r\n\t\tbool unified = Appearance.BaseShape is not null;\r\n\t\tAppearance = AppearanceCatalog.CreateDefault();\r\n\t\tif (unified)\r\n\t\t\tUnifiedHuman.Enable(Appearance);\r\n\t\tAppearance.VoiceId = voice;\r\n\t\tChosenPreset = \"traveller\";\r\n\t\tYaw = 0;\r\n\t\tRecenterPreview();\r\n\t\tSetZoom(CharacterCreatorPreview.FaceZoom);\r\n\t\tFeedback = null;\r\n\t}\r\n\r\n\tvoid Rotate(float delta)\r\n\t{\r\n\t\tYaw = (Yaw + delta) % 360;\r\n\t}\r\n\r\n\tvoid SetYaw(float value)\r\n\t{\r\n\t\tYaw = value;\r\n\t}\r\n\r\n\tvoid SetView(float value)\r\n\t{\r\n\t\tRecenterPreview();\r\n\t\tSetZoom(value);\r\n\t}\r\n\r\n\tvoid SetZoom(float value)\r\n\t{\r\n\t\tif (float.IsFinite(value))\r\n\t\t{\r\n\t\t\tZoom = Math.Clamp(value, 0, CharacterCreatorPreview.MaxZoom);\r\n\t\t\tPortrait = Zoom >= CharacterCreatorPreview.FaceZoom - 0.001f;\r\n\t\t}\r\n\t}\r\n\r\n\tvoid SetPitch(float value)\r\n\t{\r\n\t\tPreviewPitch = value;\r\n\t}\r\n\r\n\tvoid SetPan(Vector3 value)\r\n\t{\r\n\t\tPreviewPan = value;\r\n\t}\r\n\r\n\tvoid RecenterPreview()\r\n\t{\r\n\t\tYaw = 0;\r\n\t\tPreviewPan = Vector3.Zero;\r\n\t\tPreviewPitch = 0;\r\n\t\tPreview?.Recenter();\r\n\t}\r\n\r\n\tvoid ToggleClothing()\r\n\t{\r\n\t\tShowClothing = !ShowClothing;\r\n\t}\r\n\r\n\tvoid Back()\r\n\t{\r\n\t\tif (!LoadingCatalog)\r\n\t\t{\r\n\t\t\tStopVoicePreview();\r\n\t\t\tClosed?.Invoke();\r\n\t\t}\r\n\t}\r\n\r\n\tvoid Start()\r\n\t{\r\n\t\tif (Disabled || LoadingCatalog || Preview?.Loading == true)\r\n\t\t\treturn;\r\n\t\tif (!CatalogReady)\r\n\t\t{\r\n\t\t\tFeedback = LoadingCatalog ? \"Loading character options\u2026\" : \"Character options could not be loaded.\";\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif (CharacterAppearance.ValidateForApply(Appearance)is string error)\r\n\t\t{\r\n\t\t\tFeedback = error;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tStopVoicePreview();\r\n\t\tFeedback = Saved?.Invoke(CharacterName, Appearance.DeepCopy());\r\n\t}\r\n\r\n\tpublic override void Delete(bool immediate = false)\r\n\t{\r\n\t\tStopVoicePreview();\r\n\t\tbase.Delete(immediate);\r\n\t}\r\n\r\n\tprotected override async Task OnParametersSetAsync()\r\n\t{\r\n\t\tif (InitialPreset is not null && !ReferenceEquals(loadedPreset, InitialPreset))\r\n\t\t{\r\n\t\t\tLoadDraft(InitialPreset.Name, InitialPreset.Appearance);\r\n\t\t\tloadedPreset = InitialPreset;\r\n\t\t}\r\n\r\n\t\tif (CatalogReady || LoadingCatalog)\r\n\t\t\treturn;\r\n\t\tLoadingCatalog = true;\r\n\t\ttry\r\n\t\t{\r\n\t\t\tawait AppearanceCatalog.EnsureLoadedAsync();\r\n\t\t\tCatalogReady = true;\r\n\t\t}\r\n\t\tcatch (Exception)\r\n\t\t{\r\n\t\t\tFeedback = \"Character options could not be loaded. Please try again.\";\r\n\t\t}\r\n\t\tfinally\r\n\t\t{\r\n\t\t\tLoadingCatalog = false;\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override int BuildHash() => HashCode.Combine(CharacterName, PlaythroughName, HashCode.Combine(Tab, ShapeCategory, SkinPalette, MakeupLayer, CustomMakeup, SelectedMarking, MarkingSection, BlendRegion), ChosenPreset, JsonSerializer.Serialize(Appearance) + Appearance.Makeup.PresentationKey(), HashCode.Combine(Portrait, Yaw, Feedback, LoadingCatalog, SaveLabel, Preview?.Error, CatalogReady, HashCode.Combine(LoadingCatalog, Disabled, ProductLabel, LocationLabel, Title)), Zoom, HashCode.Combine(ShowClothing, PreviewPan, PreviewPitch, WardrobeSearch, WardrobeCategory, ClothingResources.Revision, CloudWardrobeOpen));\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/HumanMakeupPresentation.cs",
"FileName": "HumanMakeupPresentation.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "using System;\r\n\r\nnamespace CharacterEditor;\r\n/// <summary>Local GPU albedo composition retains the native skin shader and geometry.</summary>\r\npublic sealed class HumanMakeupPresentation : IDisposable\r\n{\r\n\tstatic ComputeShader compositor;\r\n\tTexture result;\r\n\tMaterial original;\r\n\tCharacterMakeupData makeup;\r\n\tstring skinPath;\r\n\tbool head;\r\n\tint armSide;\r\n\treadonly MakeupRefreshWindow refresh = new();\r\n\tTexture source;\r\n\tTexture baseColorOverride;\r\n\tint sourceWidth, sourceHeight;\r\n\tTexture[] inputs = new Texture[8];\n\tdouble lastRefreshTime = double.NaN;\r\n\tpublic Texture Apply(Material original, CharacterMakeupData makeup, string skinPath, bool head = true, int armSide = 0, Texture baseColorOverride = null)\r\n\t{\r\n\t\tthis.original = original;\r\n\t\tthis.makeup = makeup.DeepCopy();\r\n\t\tthis.skinPath = skinPath;\r\n\t\tthis.head = head;\r\n\t\tthis.armSide = armSide;\r\n\t\tthis.baseColorOverride = baseColorOverride;\r\n\t\tsource = baseColorOverride ?? original.GetTexture(\"g_tColor\");\r\n\t\trefresh.Reset();\r\n\t\tlastRefreshTime = double.NaN;\r\n\t\t// A material can exist while its native albedo is still loading. Keep that\r\n\t\t// original bound until the compute inputs are ready instead of baking its fallback.\r\n\t\treturn Refresh();\r\n\t}\r\n\r\n\tpublic Texture Refresh()\r\n\t{\r\n\t\tvar input = baseColorOverride ?? original.GetTexture(\"g_tColor\");\r\n\t\tif (input != source || input?.Width != sourceWidth || input?.Height != sourceHeight)\r\n\t\t{\r\n\t\t\tsource = input;\r\n\t\t\tsourceWidth = input?.Width ?? 0;\r\n\t\t\tsourceHeight = input?.Height ?? 0;\r\n\t\t\trefresh.Reset();\r\n\t\t}\r\n\r\n\t\tvar inputs = Inputs();\r\n\t\tforeach (var texture in inputs)\r\n\t\t\tif (texture is not null)\r\n\t\t\t\ttexture.MarkUsed(Math.Max(texture.Width, texture.Height));\r\n\t\tbool ready = true;\r\n\t\tforeach (var texture in inputs)\r\n\t\t\tif (texture is not { IsValid: true, IsError: false, IsLoaded: true })\r\n\t\t\t{\r\n\t\t\t\tready = false;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\r\n\t\tvar now = Time.NowDouble;\r\n\t\tvar delta = now == lastRefreshTime ? 0 : Math.Clamp(Time.Delta, 0, .1f);\r\n\t\tlastRefreshTime = now;\r\n\t\t// Count actual presentation updates, not wall time spent in the client's load screen.\r\n\t\t// Streaming refresh is finite once the real inputs become ready.\r\n\t\tif (refresh.Step(ready, delta))\r\n\t\t\tCompose();\r\n\t\treturn ready && result is not null ? result : source;\r\n\t}\r\n\r\n\tTexture[] Inputs()\n\t{\n\t\t// Live hotload can retain an older presentation's input array.\n\t\tif (inputs is null || inputs.Length != 8) inputs = new Texture[8];\n\t\tinputs[0] = source;\r\n\t\tinputs[1] = HumanMakeupAssets.Mask(skinPath);\r\n\t\tinputs[2] = HumanMakeupAssets.Marking(skinPath);\r\n\t\tinputs[3] = HumanMakeupAssets.EyeDetail(skinPath);\r\n\t\tinputs[4] = HumanMakeupAssets.Surface(skinPath, \"position_hi\", head);\r\n\t\tinputs[5] = HumanMakeupAssets.Surface(skinPath, \"position_lo\", head);\r\n\t\tinputs[6] = HumanMakeupAssets.Surface(skinPath, \"normal\", head);\n\t\tinputs[7] = HumanMakeupAssets.LinerDetail;\n\t\treturn inputs;\r\n\t}\r\n\r\n\tTexture Compose()\r\n\t{\r\n\t\tsource.MarkUsed(Math.Max(source.Width, source.Height));\r\n\t\tif (result is null || result.Width != source.Width || result.Height != source.Height)\r\n\t\t{\r\n\t\t\tresult?.Dispose();\r\n\t\t\tresult = Texture.Create(source.Width, source.Height).WithFormat(ImageFormat.RGBA8888).WithUAVBinding().WithName(\"Local human makeup\").Finish();\r\n\t\t}\r\n\r\n\t\tvar mask = HumanMakeupAssets.Mask(skinPath);\r\n\t\tvar marking = HumanMakeupAssets.Marking(skinPath);\r\n\t\tmask.MarkUsed(Math.Max(mask.Width, mask.Height));\r\n\t\tmarking.MarkUsed(Math.Max(marking.Width, marking.Height));\r\n\t\tvar attributes = new RenderAttributes();\r\n\t\tattributes.Set(\"BaseColor\", source);\r\n\t\tattributes.Set(\"Coverage\", mask);\r\n\t\tattributes.Set(\"Marking\", marking);\r\n\t\tattributes.Set(\"Result\", result);\r\n\t\tvar eyeDetail = HumanMakeupAssets.EyeDetail(skinPath);\r\n\t\teyeDetail.MarkUsed(Math.Max(eyeDetail.Width, eyeDetail.Height));\r\n\t\tattributes.Set(\"EyeDetail\", eyeDetail);\n\t\tattributes.Set(\"LinerDetail\", HumanMakeupAssets.LinerDetail);\n\t\tattributes.Set(\"LinerOptions\", new Vector4(makeup.EyelinerLowerStrength, makeup.EyelinerInnerStrength, makeup.EyelinerWing == \"wing\" ? 4f : makeup.EyelinerWing == \"downward\" ? 3f : makeup.EyelinerWing == \"cat-eye\" ? 2f : makeup.EyelinerWing == \"flick\" ? 1f : 0f, makeup.EyelinerWingStrength));\n\t\tattributes.Set(\"WingTip\", new Vector4(makeup.WingTipHorizontal, makeup.WingTipVertical, makeup.EyelinerWingThickness, 0));\n\t\tattributes.Set(\"Liner\", new Vector4(makeup.Eyeliner.Red, makeup.Eyeliner.Green, makeup.Eyeliner.Blue, makeup.Eyeliner.Amount));\r\n\t\tattributes.Set(\"LowerEye\", new Vector4(makeup.LowerEyeshadow.Red, makeup.LowerEyeshadow.Green, makeup.LowerEyeshadow.Blue, makeup.LowerEyeshadow.Amount));\r\n\t\tattributes.Set(\"Lip\", new Vector4(makeup.Lipstick.Red, makeup.Lipstick.Green, makeup.Lipstick.Blue, makeup.Lipstick.Amount));\r\n\t\tattributes.Set(\"Eye\", new Vector4(makeup.Eyeshadow.Red, makeup.Eyeshadow.Green, makeup.Eyeshadow.Blue, makeup.Eyeshadow.Amount));\r\n\t\tattributes.Set(\"Blush\", new Vector4(makeup.Blush.Red, makeup.Blush.Green, makeup.Blush.Blue, makeup.Blush.Amount));\r\n\t\tattributes.Set(\"Tattoo\", new Vector4(makeup.Tattoo.Red, makeup.Tattoo.Green, makeup.Tattoo.Blue, makeup.TattooStyle == \"none\" ? 0 : makeup.Tattoo.Amount));\r\n\t\tattributes.Set(\"IsHead\", head ? 1f : 0f);\r\n\t\tattributes.Set(\"ArmSide\", (float)armSide);\r\n\t\tforeach (var kind in new[]\r\n\t\t{\r\n\t\t\t\"position_hi\",\r\n\t\t\t\"position_lo\",\r\n\t\t\t\"normal\"\r\n\t\t}\r\n\r\n\t\t)\r\n\t\t{\r\n\t\t\tvar texture = HumanMakeupAssets.Surface(skinPath, kind, head);\r\n\t\t\ttexture.MarkUsed(Math.Max(texture.Width, texture.Height));\r\n\t\t\tattributes.Set(kind == \"position_hi\" ? \"PositionHigh\" : kind == \"position_lo\" ? \"PositionLow\" : \"SurfaceNormal\", texture);\r\n\t\t}\r\n\r\n\t\tvar layers = makeup.Markings.Where(x => head ? MarkingCatalog.OnHead(x) : MarkingCatalog.OnMaterial(x, armSide)).ToArray();\r\n\t\tfor (var i = 0; i < MarkingCatalog.Maximum; i++)\r\n\t\t{\r\n\t\t\tvar name = i.ToString();\r\n\t\t\tif (i >= layers.Length)\r\n\t\t\t{\r\n\t\t\t\tattributes.Set(\"Ink\" + name, new Vector4(0, 0, 0, 0));\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tvar layer = layers[i];\r\n\t\t\tvar region = MarkingCatalog.Region(layer.Region);\r\n\t\t\tvar style = MarkingCatalog.Style(layer.Style);\r\n\t\t\tvar frame = MarkingCatalog.Frame(layer);\r\n\t\t\tvar center = frame.Center;\r\n\t\t\tvar scale = region.Size * MarkingCatalog.StampScale(layer);\r\n\t\t\tvar width = style.Band ? MarkingCatalog.BandScale(layer) : .38f * scale;\r\n\t\t\tvar height = style.Band ? MarkingCatalog.BandSpacing(layer) : .45f * scale;\r\n\t\t\tattributes.Set(\"Center\" + name, new Vector4(center.x, center.y, center.z, style.ShaderId));\r\n\t\t\tattributes.Set(\"Right\" + name, new Vector4(frame.Right.x, frame.Right.y, frame.Right.z, width));\r\n\t\t\tattributes.Set(\"Up\" + name, new Vector4(frame.Up.x, frame.Up.y, frame.Up.z, height));\r\n\t\t\tattributes.Set(\"Normal\" + name, new Vector4(frame.Normal.x, frame.Normal.y, frame.Normal.z, (layer.Rotation - .5f) * MathF.PI * 2));\r\n\t\t\tattributes.Set(\"Min\" + name, new Vector4(region.Min.x, region.Min.y, region.Min.z, frame.Projection));\r\n\t\t\tattributes.Set(\"Max\" + name, new Vector4(region.Max.x, region.Max.y, region.Max.z, style.Band ? Math.Clamp(MathF.Round(MathF.PI * 2 * region.Radius / (1.6f * height)), 3, 36) : frame.Phase));\r\n\t\t\tattributes.Set(\"Ink\" + name, new Vector4(layer.Ink.Red, layer.Ink.Green, layer.Ink.Blue, style.ShaderId == 0 ? 0 : layer.Ink.Amount));\r\n\t\t}\r\n\r\n\t\tcompositor ??= new(HumanMakeupAssets.ShaderPath);\r\n\t\tcompositor.DispatchWithAttributes(attributes, source.Width, source.Height, 1);\r\n\t\treturn result;\r\n\t}\r\n\r\n\tpublic void Dispose()\r\n\t{\r\n\t\tresult?.Dispose();\r\n\t\tresult = null;\r\n\t}\r\n}\r\n\r\n/// <summary>Finite mip refresh starts when native input textures are ready, measured in presentation updates.</summary>\npublic sealed class MakeupRefreshWindow\r\n{\r\n\tbool ready;\r\n\tfloat elapsed, next;\r\n\tpublic void Reset()\r\n\t{\r\n\t\tready = false;\r\n\t\telapsed = 0;\r\n\t\tnext = 0;\r\n\t}\r\n\r\n\tpublic bool Step(bool inputsReady, float delta)\r\n\t{\r\n\t\tif (!inputsReady)\r\n\t\t{\r\n\t\t\tReset();\r\n\t\t\treturn false;\r\n\t\t}\r\n\r\n\t\tif (!ready)\r\n\t\t{\r\n\t\t\tready = true;\r\n\t\t\telapsed = 0;\r\n\t\t\tnext = 0;\r\n\t\t}\r\n\r\n\t\telapsed += Math.Clamp(delta, 0, .1f);\r\n\t\tif (elapsed > 3 || elapsed < next)\r\n\t\t\treturn false;\r\n\t\tnext = elapsed + .25f;\r\n\t\treturn true;\r\n\t}\r\n}\r\n"
},
{
"Ident": "vendetta.characterkit",
"Path": "Code/CharacterKit/UnifiedWardrobe.cs",
"FileName": "UnifiedWardrobe.cs",
"PackageType": "library",
"CodeKind": "Game",
"AssetVersionId": 421600,
"IsPrivate": false,
"Code": "namespace CharacterEditor;\r\n/// <summary>Authored human cuts fitted automatically to both unified body endpoints.</summary>\r\npublic static partial class UnifiedWardrobe\r\n{\r\n\t/// <summary>Head replacements can retain skin or deliberately replace the entire neck.</summary>\r\n\tpublic static bool RetainsHumanNeck(Clothing item)\r\n\t{\r\n\t\tif (string.IsNullOrEmpty(item.HumanSkinModel)) return true;\r\n\t\tvar skin = Model.Load(item.HumanSkinModel);\r\n\t\t// Use the replacement's native skin presentation. A transparent/non-skin\r\n\t\t// head material must not be replaced with the unified human's skin neck.\r\n\t\treturn skin.MeshInfo.Meshes.Where(m => m.Name == \"head_01\")\r\n\t\t\t.SelectMany(m => m.DrawCalls).Any(d => d.Material?.ShaderName == \"shaders/skin.shader\");\r\n\t}\r\n\r\n\tpublic static string ModelPath(Clothing item, IEnumerable<Clothing> selected, TagSet tags, CharacterAppearanceData data)\r\n\t{\r\n\t\tvar native = item.GetModel(selected.ToArray(), tags);\r\n\t\tif (data.BaseShape is null)\r\n\t\t\treturn native;\r\n\t\t// Preserve conditional model overrides selected by native clothing metadata.\r\n\t\tif (native != item.Model && native != item.HumanAltModel && native != item.HumanAltFemaleModel)\r\n\t\t\treturn native;\r\n\t\t// Explicit preference selects only the authored cut; the morph still uses\r\n\t\t// the actual body blend. Single-variant items retain their usual fallback.\r\n\t\tif (data.BaseShape.ClothingVariant is not null && !string.IsNullOrEmpty(item.HumanAltModel) && !string.IsNullOrEmpty(item.HumanAltFemaleModel))\r\n\t\t{\r\n\t\t\tvar choice = data.BaseShape.ClothingVariant == \"male\" ? item.HumanAltModel : item.HumanAltFemaleModel;\r\n\t\t\treturn choice;\r\n\t\t}\r\n\r\n\t\t// A single authored human variant is usable across the full body range.\r\n\t\tvar preferred = data.BaseShape.MaleBody >= .5f ? item.HumanAltModel : item.HumanAltFemaleModel;\r\n\t\tif (string.IsNullOrEmpty(preferred))\r\n\t\t\tpreferred = string.IsNullOrEmpty(item.HumanAltModel) ? item.HumanAltFemaleModel : item.HumanAltModel;\r\n\t\treturn !string.IsNullOrEmpty(preferred) ? preferred : native;\r\n\t}\r\n\r\n\tpublic static Model Resolve(Clothing item, IEnumerable<Clothing> selected, TagSet tags, CharacterAppearanceData data)\r\n\t{\r\n\t\tvar path = ModelPath(item, selected, tags, data);\r\n\t\tvar native = Model.Load(path);\r\n\t\tif (data.BaseShape is null || string.IsNullOrEmpty(path) || (string.IsNullOrEmpty(item.HumanAltModel) && string.IsNullOrEmpty(item.HumanAltFemaleModel)))\r\n\t\t\treturn native;\r\n\t\tif (item.Category.ToString().StartsWith(\"FacialHair\") || item.Category.ToString().StartsWith(\"Hair\"))\r\n\t\t\treturn native;\r\n\t\tbool female = path == item.HumanAltFemaleModel && path != item.HumanAltModel;\r\n\t\tif (path != item.HumanAltModel && path != item.HumanAltFemaleModel)\r\n\t\t\tfemale = tags.Has(\"female\");\r\n\t\treturn AutomaticWardrobeFit.Fit(native, female, fitFace: (item.HideBody & Clothing.BodyGroups.Head) == 0);\r\n\t}\r\n\r\n\tpublic static void Apply(SkinnedModelRenderer renderer, CharacterAppearanceData data)\r\n\t{\r\n\t\tif (data.BaseShape is not null && AutomaticWardrobeFit.IsFitted(renderer.Model))\r\n\t\t\trenderer.Morphs.Set(\"ce_body_female\", 1 - data.BaseShape.MaleBody);\r\n\t}\r\n}// Keep hair outside body fitting.\r\n\r\n"
}
]
}