k/Extensions/ClothingContainerExtensions.cs
// author: LIMESTA
// https://sbox.game/f/code/335/1

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Sandbox.k.Extensions;

public static class ClothingContainerExtensions
{
	/// <summary>
	/// Not working and I'm lazy to see why
	/// Dress a skinned model renderer with an outfit
	/// </summary>
	public static async Task ApplyAsync( this ClothingContainer container, SkinnedModelRenderer body )
	{
		var isHuman = DetermineHuman( body );

		// remove out outfit
		body.Reset();
		container.Normalize();

		// apply indicentals
		body.Set( "scale_height", container.Height );

		// Clean the clothing. Remove any invalid items, any items with broken models
		// any items that can't be worn with other items.
		var set = (await GameTask.WhenAll(
				container.Clothing?.Select( async x => new { Item = x, IsValid = await IsValidClothing( x, isHuman ) } ) 
			)).Where(x => x.IsValid)
			.Select(x => x.Item)
			.ToList();

		set ??= new List<ClothingContainer.ClothingEntry>();

		TagSet tags = new();

		// apply alternate human skin, if we have one
		if ( isHuman )
		{
			tags.Add( "human" );

			var humanskin = set.Where( x => x.Clothing.HasHumanSkin ).FirstOrDefault();
			if ( humanskin is not null && await Model.LoadAsync( humanskin.Clothing.HumanSkinModel ) is Model model && model.IsValid() )
			{
				body.Model = model;
				tags.Add( humanskin.Clothing.HumanSkinTags );

				body.BodyGroups = humanskin.Clothing.HumanSkinBodyGroups;
				body.MaterialGroup = humanskin.Clothing.HumanSkinMaterialGroup;

			}
			else
			{
				// restore to default, somehow?
				//body.Model = Model.Load( humanskin.Clothing.HumanSkinModel );
			}
		}

		body.SetMaterialOverride( null, "skin" );
		body.SetMaterialOverride( null, "eyes" );

		var skinMaterial = set.Select( x => x.Clothing.SkinMaterial ).Where( x => !string.IsNullOrWhiteSpace( x ) ).Select( x => Material.Load( x ) ).FirstOrDefault();
		var eyesMaterial = set.Select( x => x.Clothing.EyesMaterial ).Where( x => !string.IsNullOrWhiteSpace( x ) ).Select( x => Material.Load( x )).FirstOrDefault();

		if ( !isHuman )
		{
			body.SetMaterialOverride( skinMaterial, "skin" );
			body.SetMaterialOverride( eyesMaterial, "eyes" );
		}

		// Create clothes models
		foreach ( var entry in set )
		{
			var c = entry.Clothing;

			var modelPath = c.GetModel( set.Select( x => x.Clothing ).Except( new[] { c } ), tags );

			if ( string.IsNullOrEmpty( modelPath ) || !string.IsNullOrEmpty( c.SkinMaterial ) )
				continue;

			var model = await Model.LoadAsync( modelPath );
			if ( !model.IsValid() || model.IsError )
				continue;

			var go = new GameObject( false, $"Clothing - {c.ResourceName}" );
			go.Parent = body.GameObject;
			go.Tags.Add( "clothing" );

			var r = go.Components.Create<SkinnedModelRenderer>();
			r.Model = model;
			r.BoneMergeTarget = body;

			if ( !isHuman )
			{
				if ( skinMaterial is not null ) r.SetMaterialOverride( skinMaterial, "skin" );
				if ( eyesMaterial is not null ) r.SetMaterialOverride( eyesMaterial, "eyes" );
			}

			if ( !string.IsNullOrEmpty( c.MaterialGroup ) )
				r.MaterialGroup = c.MaterialGroup;

			if ( c.AllowTintSelect )
			{
				var tintValue = entry.Tint?.Clamp( 0, 1 ) ?? c.TintDefault;
				var tintColor = c.TintSelection.Evaluate( tintValue );
				r.Tint = tintColor;
			}

			go.Enabled = true;
		}

		// Set body groups
		foreach ( var (name, value) in container.GetBodyGroups( set.Select( x => x.Clothing ) ) )
		{
			if ( value == 0 ) continue;

			body.SetBodyGroup( name, value );
		}
	}

	private static bool DetermineHuman( SkinnedModelRenderer b, bool defaultValue = false )
    {
        if ( b is null ) return defaultValue;
        if ( b.Model is null ) return defaultValue;
        if ( b.Model is null ) return defaultValue;

        if ( b.Model.Name.Contains( "citizen.vmdl", System.StringComparison.OrdinalIgnoreCase ) ) return false;
        return true;
    }

	private static async Task<bool> IsValidModel( string modelName )
    {
        if ( string.IsNullOrWhiteSpace( modelName ) )
            return false;

        var model = await Model.LoadAsync( modelName );

        if ( !model.IsValid() ) return false;
        if ( model.IsError ) return false;

        return true;
    }

    private static async Task<bool> IsValidClothing( ClothingContainer.ClothingEntry e, bool targetIsHuman )
    {
        if ( e is null ) return false;
        if ( e.Clothing is null ) return false;
        if ( targetIsHuman && e.Clothing.HasHumanSkin ) return true;

        var model = e.Clothing.Model;

        if ( targetIsHuman )
        {
            model = e.Clothing.HumanAltModel;

            // If we have a citizen model, but not a human model, make clothing invalid
            if ( string.IsNullOrEmpty( model ) && !string.IsNullOrEmpty( e.Clothing.Model ) )
                return false;
        }

        if ( string.IsNullOrEmpty( model ) )
            return true;

        if ( ! await IsValidModel( model ) )
        {
            Log.Warning( $"Clothing model '{model}' in {e.Clothing} is invalid, removing" );
            return false;
        }

        return true;
    }
}