Helper/ClothingModelSuffixStripper.cs

Component attached to a dresser that ensures clothing SkinnedModelRenderer instances use the clothing items' Model assets. It waits until dressing is finished, reads clothing entries from manual, owner connection, or local user, finds the corresponding clothing GameObject children by name, loads the Model asset and assigns it to the renderer.

File Access
namespace Skateboard;

public sealed class ClothingModelSuffixStripper : Component
{
	[Property] public Dresser Dresser { get; set; }
	private bool _applied;

	protected override void OnStart()
	{
		Dresser ??= Components.Get<Dresser>();
	}

	protected override void OnUpdate()
	{
		if ( Dresser is null )
			return;

		if ( Dresser.IsDressing )
		{
			_applied = false;
			return;
		}

		if ( _applied )
			return;

		OverrideClothingModels();
	}

	private void OverrideClothingModels()
	{
		var target = Dresser.BodyTarget;
		if ( target is null || !target.IsValid() )
			return;

		var clothingEntries = GetClothingEntries();
		if ( clothingEntries is null || clothingEntries.Count == 0 )
		{
			_applied = true;
			return;
		}

		var foundClothingObject = false;
		foreach ( var entry in clothingEntries )
		{
			var clothing = entry?.Clothing;
			if ( clothing is null )
				continue;

			var modelPath = clothing.Model;
			if ( string.IsNullOrWhiteSpace( modelPath ) )
				continue;

			var clothingObject = FindClothingObject( target.GameObject, clothing.ResourceName );
			if ( clothingObject is null )
				continue;

			foundClothingObject = true;

			var renderer = clothingObject.Components.Get<SkinnedModelRenderer>();
			if ( renderer is null )
				continue;

			var model = Model.Load( modelPath );
			if ( model is null || !model.IsValid() || model.IsError )
				continue;

			if ( renderer.Model == model )
				continue;

			renderer.Model = model;
		}

		if ( foundClothingObject )
			_applied = true;
	}

	private List<ClothingContainer.ClothingEntry> GetClothingEntries()
	{
		switch ( Dresser.Source )
		{
			case Dresser.ClothingSource.Manual:
				return Dresser.Clothing;
			case Dresser.ClothingSource.OwnerConnection:
				return GetOwnerClothingEntries();
			case Dresser.ClothingSource.LocalUser:
				return ClothingContainer.CreateFromLocalUser()?.Clothing;
		}

		return null;
	}

	private List<ClothingContainer.ClothingEntry> GetOwnerClothingEntries()
	{
		var owner = Dresser.Network?.Owner;
		if ( owner is null )
			return null;

		var clothingData = owner.GetUserData( "avatar" );
		if ( string.IsNullOrEmpty( clothingData ) )
			return null;

		var container = new ClothingContainer();
		container.Deserialize( clothingData );
		container.Normalize();
		return container.Clothing;
	}

	private static GameObject FindClothingObject( GameObject root, string resourceName )
	{
		if ( root is null || string.IsNullOrWhiteSpace( resourceName ) )
			return null;

		var name = $"Clothing - {resourceName}";
		foreach ( var child in root.Children )
		{
			if ( child.Name == name )
				return child;
		}

		foreach ( var child in root.GetAllObjects( false ) )
		{
			if ( child.Name == name )
				return child;
		}

		return null;
	}
}