CharacterKit/UnderwearPresentation.cs
using System;
using System.Threading.Tasks;

namespace CharacterEditor;

/// <summary>Updated native fabric artwork on the existing, morphable underwear meshes.</summary>
public sealed class UnderwearPresentation : IDisposable
{
	public const string UpdatedMaterialPath = "materials/makeup/underwear/updated.vmat";
	public const string OriginalMaterialPath = "models/citizen_human/bodies/underwear.vmat";
	public static bool IsUnderwearMaterial(Material material) => material?.Name is UpdatedMaterialPath or OriginalMaterialPath;
	public static bool CoversTop(Clothing item)
	{
		if (item is null) return false;
		if (item.HideBody.ToString().Split(',').Any(x => x.Trim() == "Chest")) return true;
		// Cloud tops often retain the visible body mesh instead of hiding Chest.
		// That does not mean the built-in bra should render through their fabric.
		return item.Category.ToString() is "Tops" or "TShirt" or "Sweatshirt" or "Hoodie" or "Shirt" or "Knitwear" or "Dress" or "Jumpsuit";
	}

	static Material updated, original;
	SkinnedModelRenderer target;
	Model model;
	readonly HashSet<int> overrides = new();

	public static async Task LoadAsync(CharacterAppearanceData data)
	{
		if (data?.BaseShape is null)
			return;
		if (data.UnderwearStyle == "original")
		{
			if (original is { IsError: false }) return;
			original = await Material.LoadAsync(OriginalMaterialPath);
			if (original is null || original.IsError)
				throw new InvalidOperationException("The original underwear material is unavailable.");
			return;
		}
		if (updated is not null) return;
		var carrier = await Material.LoadAsync(UpdatedMaterialPath);
		var source = await Material.LoadAsync("models/citizen_human/bodies/female/female_body_mode_light.vmat");
		if (carrier is null || carrier.IsError || source is null || source.IsError)
			throw new InvalidOperationException("The updated underwear material is unavailable.");
		updated = carrier.CreateCopy("characterkit-updated-underwear.vmat");
		updated.Set("g_tNormal", source.GetTexture("g_tNormal"));
	}

	public void Apply(SkinnedModelRenderer body, CharacterAppearanceData data)
	{
		if (!body.IsValid() || body.Model is null || data is null)
			return;
		if (target != body || model != body.Model)
		{
			Dispose();
			target = body;
			model = body.Model;
		}
		bool useOriginal = data.UnderwearStyle == "original";
		if (!useOriginal && updated is null)
			return;
		for (int i = 0; i < body.Materials.Count; i++)
		{
			var slot = body.Materials.GetOriginal(i);
			if (!IsUnderwearMaterial(slot))
				continue;
			// Unified meshes now compile with the cutout as their source material.
			// Original remains an explicit opaque override; legacy source slots can
			// still restore their own original without retaining any override.
			var desired = useOriginal ? slot.Name == UpdatedMaterialPath ? original : null : updated;
			if (useOriginal && slot.Name == UpdatedMaterialPath && original is null) continue;
			if (body.Materials.GetOverride(i) != desired)
				body.Materials.SetOverride(i, desired);
			if (desired is null) overrides.Remove(i);
			else overrides.Add(i);
		}
	}

	public void Dispose()
	{
		if (target.IsValid() && target.Model == model)
			foreach (var i in overrides)
				target.Materials.SetOverride(i, null);
		overrides.Clear();
	}
}