Character/TintedSkin.cs

Static helper that runs after a Dresser applies clothing, and fixes tinted garments that also render body parts. For each tinted clothing child with material slots pointing at bodies, it hides those skin slots with an invisible material and spawns a duplicate SkinnedModelRenderer that shows only the skin, untinted, with the dresser's skin_tint and skin_age attributes and bone merge onto the body.

namespace HorizonRP;

/// <summary>
/// La peau sous un vêtement teint. Un t-shirt, un polo ou un short de s&box porte dans son
/// modèle le morceau de corps qu'il laisse voir (bras, cuisses : matériau « bodies/… »), et la
/// teinte du vêtement (variantes de couleur, CharacterRules.Variants) colore tout le modèle :
/// un t-shirt rouge donnait des bras rouges. Copier le matériau de peau sans teinte ne va pas :
/// la copie perd le teint du personnage.
///
/// Après chaque habillage, pour chaque vêtement teint qui porte de la peau : la peau du
/// vêtement devient invisible, et un second exemplaire du modèle, non teint, ne montre que
/// cette peau (le reste invisible), avec le teint et l'âge que le Dresser pose sur chaque
/// vêtement (attributs skin_tint et skin_age). Le Dresser recrée ses vêtements à chaque
/// habillage : la copie, rangée sous le vêtement, part avec lui.
/// </summary>
public static class TintedSkin
{
	const string Name = "Peau sous la teinte";

	static Material invisible;

	public static async Task DressAsync( Dresser dresser, ClothingContainer container )
	{
		await dresser.ApplyAsync( container );
		Apply( dresser );
		PoliceUniform.Apply( dresser );
	}

	public static void Apply( Dresser dresser )
	{
		var body = dresser.IsValid() ? dresser.BodyTarget : null;
		if ( !body.IsValid() )
			return;
		invisible ??= Material.Load( "materials/horizon/invisible.vmat" );

		foreach ( var child in body.GameObject.Children.Where( x => x.Tags.Has( "clothing" ) ).ToList() )
		{
			var cloth = child.GetComponent<SkinnedModelRenderer>();
			if ( !cloth.IsValid() || cloth.Tint == Color.White || child.Children.Any( x => x.Name == Name ) )
				continue;

			var skinSlots = Enumerable.Range( 0, cloth.Materials.Count )
				.Where( i => cloth.Materials.GetOriginal( i )?.ResourcePath?.Contains( "/bodies/" ) == true )
				.ToHashSet();
			if ( skinSlots.Count == 0 )
				continue;

			var go = new GameObject( child, true, Name );
			go.Flags |= GameObjectFlags.NotNetworked | GameObjectFlags.NotSaved;
			var skin = go.Components.Create<SkinnedModelRenderer>();
			skin.Model = cloth.Model;
			skin.BoneMergeTarget = body;
			skin.DeformationMode = cloth.DeformationMode;
			skin.MaterialGroup = cloth.MaterialGroup;
			skin.Attributes.Set( "skin_age", dresser.ManualAge );
			skin.Attributes.Set( "skin_tint", dresser.ManualTint );

			for ( var i = 0; i < cloth.Materials.Count; i++ )
			{
				if ( skinSlots.Contains( i ) )
				{
					skin.Materials.SetOverride( i, cloth.Materials.HasOverride( i ) ? cloth.Materials.GetOverride( i ) : cloth.Materials.GetOriginal( i ) );
					cloth.Materials.SetOverride( i, invisible );
				}
				else
					skin.Materials.SetOverride( i, invisible );
			}
			skin.Materials.Apply();
			cloth.Materials.Apply();
		}
	}
}