// Fill out your copyright notice in the Description page of Project Settings.


#include "CharacterAppearanceComponent.h"

#include "Components/SkeletalMeshComponent.h"
#include "CharacterPartsDataAsset.h"
#include "Engine/SkeletalMesh.h"
#include "GameFramework/Character.h"
#include "Materials/MaterialInstanceDynamic.h"

namespace
{
	/**
	 * Vrai si les deux apparences designent exactement les memes PIECES.
	 *
	 * Les couleurs en sont volontairement exclues : elles se posent sur des
	 * materiaux deja en place, sans toucher au montage. C'est cette distinction
	 * qui evite qu'un curseur de couleur glisse reengendre six meshes par image.
	 *
	 * Prefixe du fichier oblige : le build unity concatene les .cpp du module,
	 * un namespace anonyme n'isole rien.
	 */
	bool AppearanceHasSameParts(const FCharacterAppearance& A, const FCharacterAppearance& B)
	{
		return A.BodyType == B.BodyType
			&& A.HeadIndex == B.HeadIndex
			&& A.HairIndex == B.HairIndex
			&& A.EyebrowsIndex == B.EyebrowsIndex
			&& A.BeardIndex == B.BeardIndex
			&& A.MustacheIndex == B.MustacheIndex
			&& A.EyesIndex == B.EyesIndex;
	}

	/** Traduit l'argument d'une commande console en categorie. */
	bool AppearanceParsePartCategory(const FString& Name, ECharacterPartCategory& OutCategory)
	{
		if (Name.Equals(TEXT("head"), ESearchCase::IgnoreCase))      { OutCategory = ECharacterPartCategory::Head;      return true; }
		if (Name.Equals(TEXT("hair"), ESearchCase::IgnoreCase))      { OutCategory = ECharacterPartCategory::Hair;      return true; }
		if (Name.Equals(TEXT("eyebrows"), ESearchCase::IgnoreCase))  { OutCategory = ECharacterPartCategory::Eyebrows;  return true; }
		if (Name.Equals(TEXT("beard"), ESearchCase::IgnoreCase))     { OutCategory = ECharacterPartCategory::Beard;     return true; }
		if (Name.Equals(TEXT("mustache"), ESearchCase::IgnoreCase))  { OutCategory = ECharacterPartCategory::Mustache;  return true; }
		return false;
	}

	/** L'index de piece stocke dans l'apparence, par categorie. */
	uint8* AppearancePartIndexPtr(FCharacterAppearance& Target, ECharacterPartCategory Category)
	{
		switch (Category)
		{
		case ECharacterPartCategory::Head:     return &Target.HeadIndex;
		case ECharacterPartCategory::Hair:     return &Target.HairIndex;
		case ECharacterPartCategory::Eyebrows: return &Target.EyebrowsIndex;
		case ECharacterPartCategory::Beard:    return &Target.BeardIndex;
		case ECharacterPartCategory::Mustache: return &Target.MustacheIndex;
		default:                               return nullptr;
		}
	}
}

UCharacterAppearanceComponent::UCharacterAppearanceComponent()
{
	// Aucun Tick : le montage est evenementiel, il ne se declenche qu'a
	// l'application d'une apparence.
	PrimaryComponentTick.bCanEverTick = false;

	PartComponents.SetNum(static_cast<int32>(ECharacterPartCategory::Count));
}

void UCharacterAppearanceComponent::BeginPlay()
{
	Super::BeginPlay();

	// L'apparence par defaut, faute de mieux. Le lot reseau branchera ici la
	// lecture du PlayerState -- ce BeginPlay restera le repli du premier
	// lancement, quand aucun reglage n'a encore ete sauvegarde.
	ApplyAppearance(DefaultAppearance);
}

void UCharacterAppearanceComponent::OnUnregister()
{
	// Les composants engendres appartiennent a l'acteur, pas a nous : ils
	// disparaissent avec lui. On lache seulement nos references, pour ne pas
	// retenir des objets que le GC doit pouvoir reprendre si le composant est
	// retire d'un acteur qui, lui, survit.
	PartComponents.Reset();
	PartComponents.SetNum(static_cast<int32>(ECharacterPartCategory::Count));
	ExtraBodyComponents.Reset();
	bBuilt = false;

	Super::OnUnregister();
}

void UCharacterAppearanceComponent::SetTargetMesh(USkeletalMeshComponent* InTargetMesh)
{
	if (TargetMesh == InTargetMesh)
	{
		return;
	}

	TargetMesh = InTargetMesh;

	// Les pieces deja montees suivaient l'ancien porteur : les laisser en place
	// donnerait une tete qui garde la pose d'un corps dont on ne se sert plus.
	bBuilt = false;
}

USkeletalMeshComponent* UCharacterAppearanceComponent::ResolveTargetMesh()
{
	if (TargetMesh)
	{
		return TargetMesh;
	}

	AActor* Owner = GetOwner();
	if (!Owner)
	{
		return nullptr;
	}

	// L'ACharacter d'abord, et surtout pas « le premier SkeletalMesh trouve » :
	// AFpsPlayer porte aussi FirstPersonArms, et l'ordre des composants n'est
	// garanti par rien.
	if (const ACharacter* OwningCharacter = Cast<ACharacter>(Owner))
	{
		TargetMesh = OwningCharacter->GetMesh();
	}

	if (!TargetMesh)
	{
		TargetMesh = Owner->FindComponentByClass<USkeletalMeshComponent>();
	}

	return TargetMesh;
}

USkeletalMeshComponent* UCharacterAppearanceComponent::CreateFollowerComponent(FName ComponentName)
{
	AActor* Owner = GetOwner();
	if (!Owner || !TargetMesh)
	{
		return nullptr;
	}

	USkeletalMeshComponent* Follower = NewObject<USkeletalMeshComponent>(Owner, ComponentName);
	if (!Follower)
	{
		return nullptr;
	}

	Follower->SetupAttachment(TargetMesh);

	// Recopiees du porteur plutot que posees en dur : sur le pawn elles valent
	// « invisible pour moi, mais je garde mon ombre », sur le mannequin du menu
	// elles valent l'inverse. Le composant n'a pas a savoir dans lequel il vit.
	Follower->SetOwnerNoSee(TargetMesh->bOwnerNoSee);
	Follower->SetOnlyOwnerSee(TargetMesh->bOnlyOwnerSee);
	Follower->SetCastHiddenShadow(TargetMesh->bCastHiddenShadow);

	// Une piece d'apparence n'est pas une surface de jeu : la capsule porte la
	// collision, et une tete qui bloque un trace d'interaction se remarquerait
	// tout de suite.
	Follower->SetCollisionEnabled(ECollisionEnabled::NoCollision);

	Follower->RegisterComponent();

	// Apres l'enregistrement : c'est la que le composant a son monde et sa
	// hierarchie, donc que la table de correspondance des os peut se batir.
	// La correspondance se fait PAR NOM D'OS, ce qui la rend indifferente au
	// fait que les tetes du pack de customisation soient sur SK_BodyA_empty
	// quand le corps est sur SK_BodyA.
	Follower->SetLeaderPoseComponent(TargetMesh);

	return Follower;
}

USkeletalMeshComponent* UCharacterAppearanceComponent::EnsurePartComponent(ECharacterPartCategory Category)
{
	const int32 Index = static_cast<int32>(Category);
	if (!PartComponents.IsValidIndex(Index))
	{
		return nullptr;
	}

	if (PartComponents[Index])
	{
		return PartComponents[Index];
	}

	const FName ComponentName(*FString::Printf(TEXT("AppearancePart_%d"), Index));
	PartComponents[Index] = CreateFollowerComponent(ComponentName);

	return PartComponents[Index];
}

void UCharacterAppearanceComponent::ApplyAppearance(const FCharacterAppearance& InAppearance)
{
	if (!Catalog)
	{
		UE_LOG(LogTemp, Warning, TEXT("UCharacterAppearanceComponent : aucun catalogue assigne sur %s, rien n'est monte."),
			*GetNameSafe(GetOwner()));
		return;
	}

	if (!ResolveTargetMesh())
	{
		UE_LOG(LogTemp, Warning, TEXT("UCharacterAppearanceComponent : aucun mesh porteur sur %s."),
			*GetNameSafe(GetOwner()));
		return;
	}

	const FCharacterAppearance Previous = Appearance;

	Appearance = InAppearance;

	// Systematique, et pas seulement pour ce qui vient du reseau : un catalogue
	// ampute entre deux versions du jeu laisserait des index qui ne designent
	// plus rien, y compris dans les reglages sauvegardes du joueur.
	Catalog->Sanitize(Appearance);

	const FCharacterBodySet* BodySet = Catalog->GetBodySet(Appearance.BodyType);
	if (!BodySet)
	{
		UE_LOG(LogTemp, Warning, TEXT("UCharacterAppearanceComponent : le catalogue %s n'a aucune silhouette."),
			*GetNameSafe(Catalog));
		return;
	}

	if (!bBuilt || !AppearanceHasSameParts(Previous, Appearance))
	{
		RebuildMeshes(*BodySet);
		bBuilt = true;
	}

	ApplyColors(*BodySet);

	OnAppearanceApplied.Broadcast();
}

void UCharacterAppearanceComponent::RebuildMeshes(const FCharacterBodySet& BodySet)
{
	// ------------------------------------------------------------------
	// Le corps. La case 0 est le porteur, les suivantes des composants
	// enfants -- aujourd'hui aucune, demain les segments d'armure.
	// ------------------------------------------------------------------

	if (BodySet.BodyMeshes.Num() > 0 && BodySet.BodyMeshes[0])
	{
		SetPartMesh(TargetMesh, BodySet.BodyMeshes[0]);
	}

	if (BodySet.AnimClass)
	{
		// Reposee a chaque changement de silhouette : l'AnimBP est lie a un
		// squelette, et passer de A a B sans le changer figerait le corps.
		TargetMesh->SetAnimInstanceClass(BodySet.AnimClass);
	}

	const int32 ExtraCount = FMath::Max(0, BodySet.BodyMeshes.Num() - 1);
	for (int32 i = 0; i < ExtraCount; ++i)
	{
		if (!ExtraBodyComponents.IsValidIndex(i))
		{
			const FName ComponentName(*FString::Printf(TEXT("AppearanceBody_%d"), i));
			ExtraBodyComponents.Add(CreateFollowerComponent(ComponentName));
		}

		if (USkeletalMeshComponent* Segment = ExtraBodyComponents[i])
		{
			SetPartMesh(Segment, BodySet.BodyMeshes[i + 1]);
		}
	}

	// Les segments en trop apres un changement de silhouette : on les vide au
	// lieu de les detruire, pour ne pas payer une reconstruction a chaque
	// aller-retour entre deux corps dans l'ecran de customisation.
	for (int32 i = ExtraCount; i < ExtraBodyComponents.Num(); ++i)
	{
		if (USkeletalMeshComponent* Segment = ExtraBodyComponents[i])
		{
			SetPartMesh(Segment, nullptr);
		}
	}

	// ------------------------------------------------------------------
	// Les pieces. Un index a NoPart, ou une liste vide, laisse simplement le
	// composant sans mesh -- rien a detruire, rien a recreer au retour.
	// ------------------------------------------------------------------

	auto ApplyPart = [this](ECharacterPartCategory Category, const TArray<FCharacterPartEntry>& Entries, uint8 PartIndex)
	{
		USkeletalMesh* Mesh = nullptr;
		if (PartIndex != FCharacterAppearance::NoPart && Entries.IsValidIndex(PartIndex))
		{
			Mesh = Entries[PartIndex].Mesh;
		}

		// On n'engendre pas le composant pour rien : une silhouette sans barbe
		// n'a aucune raison de porter un SkeletalMeshComponent vide.
		const int32 Index = static_cast<int32>(Category);
		if (!Mesh && (!PartComponents.IsValidIndex(Index) || !PartComponents[Index]))
		{
			return;
		}

		if (USkeletalMeshComponent* PartComponent = EnsurePartComponent(Category))
		{
			SetPartMesh(PartComponent, Mesh);
		}
	};

	ApplyPart(ECharacterPartCategory::Head, BodySet.Heads, Appearance.HeadIndex);
	ApplyPart(ECharacterPartCategory::Hair, BodySet.Hairstyles, Appearance.HairIndex);
	ApplyPart(ECharacterPartCategory::Eyebrows, BodySet.Eyebrows, Appearance.EyebrowsIndex);
	ApplyPart(ECharacterPartCategory::Beard, BodySet.Beards, Appearance.BeardIndex);
	ApplyPart(ECharacterPartCategory::Mustache, BodySet.Mustaches, Appearance.MustacheIndex);

	// ------------------------------------------------------------------
	// Les yeux : un MATERIAU sur un slot du mesh de tete, pas un mesh. Il se
	// pose ici et pas dans ApplyColors parce qu'un SetMaterial jette le
	// materiau dynamique en place -- le refaire a chaque image en engendrerait
	// un par frame, exactement ce que la separation des deux etapes evite.
	// ------------------------------------------------------------------

	const int32 HeadSlot = static_cast<int32>(ECharacterPartCategory::Head);
	if (PartComponents.IsValidIndex(HeadSlot) && PartComponents[HeadSlot]
		&& Catalog->EyeMaterials.IsValidIndex(Appearance.EyesIndex))
	{
		if (UMaterialInterface* EyeMaterial = Catalog->EyeMaterials[Appearance.EyesIndex])
		{
			USkeletalMeshComponent* HeadComponent = PartComponents[HeadSlot];
			const int32 EyeSlot = FindEyeMaterialSlot(HeadComponent, BodySet.EyeMaterialSlotHint);
			if (EyeSlot != INDEX_NONE)
			{
				HeadComponent->SetMaterial(EyeSlot, EyeMaterial);
			}
		}
	}
}

int32 UCharacterAppearanceComponent::FindEyeMaterialSlot(const USkeletalMeshComponent* HeadComponent, FName SlotHint)
{
	if (!HeadComponent || SlotHint.IsNone())
	{
		return INDEX_NONE;
	}

	// Contains et pas une egalite : les six tetes du pack nomment leur slot
	// differemment (`M_Eye_A_01`, `M_Eye`, `M_Eyes_02`), mais toutes contiennent
	// le fragment -- et aucun slot de peau ne le contient.
	const FString Hint = SlotHint.ToString();
	const TArray<FName> SlotNames = HeadComponent->GetMaterialSlotNames();

	for (int32 i = 0; i < SlotNames.Num(); ++i)
	{
		if (SlotNames[i].ToString().Contains(Hint, ESearchCase::IgnoreCase))
		{
			return i;
		}
	}

	// Rien pose plutot qu'un index au hasard : ecraser le slot de la peau
	// repeindrait le visage entier de la couleur des yeux, et on chercherait
	// longtemps d'ou ca vient. Le log, lui, se lit tout de suite.
	UE_LOG(LogTemp, Warning,
		TEXT("UCharacterAppearanceComponent : aucun slot de materiau ne contient '%s' sur le mesh de tete. ")
		TEXT("Les yeux ne seront pas poses. Slots disponibles : %s"),
		*Hint,
		*FString::JoinBy(SlotNames, TEXT(", "), [](const FName& Name) { return Name.ToString(); }));

	return INDEX_NONE;
}

void UCharacterAppearanceComponent::ApplyColors(const FCharacterBodySet& BodySet)
{
	// Chaque parametre est pose sur TOUS les elements de chaque mesh, sans
	// chercher lequel le porte : un SetVectorParameterValue dont le parametre
	// n'existe pas dans le materiau est simplement ignore. Le slot des yeux
	// ignore donc « Skin Base color », et celui de la peau « Eye color ». Cela
	// evite de tenir a jour une table d'index de slots qui casserait au premier
	// mesh dont l'ordre des materiaux differe.

	// Le blanc en repli et pas le noir : un materiau teinte par une couleur
	// manquante doit rester lisible. Le blanc rend la texture telle qu'elle est,
	// le noir donnerait un personnage en silhouette qu'on prendrait pour un bug
	// de rendu plutot que pour une palette vide.
	auto PickColor = [](const TArray<FLinearColor>& Palette, uint8 Index)
	{
		return Palette.IsValidIndex(Index) ? Palette[Index] : FLinearColor::White;
	};

	const FLinearColor SkinColor = PickColor(Catalog->SkinColors, Appearance.SkinColorIndex);
	const FLinearColor HairColor = PickColor(Catalog->HairColors, Appearance.HairColorIndex);
	const FLinearColor EyeColor = PickColor(Catalog->EyeColors, Appearance.EyeColorIndex);
	const FLinearColor UnderwearColor = PickColor(Catalog->UnderwearColors, Appearance.UnderwearColorIndex);

	// Le corps : peau et sous-vetement partagent le meme master.
	TintAllElements(TargetMesh, Catalog->SkinColorParameter, SkinColor);
	TintAllElements(TargetMesh, Catalog->UnderwearColorParameter, UnderwearColor);

	for (USkeletalMeshComponent* Segment : ExtraBodyComponents)
	{
		TintAllElements(Segment, Catalog->SkinColorParameter, SkinColor);
		TintAllElements(Segment, Catalog->UnderwearColorParameter, UnderwearColor);
	}

	// La tete porte la peau ET les yeux, sur deux masters differents.
	const int32 HeadSlot = static_cast<int32>(ECharacterPartCategory::Head);
	if (PartComponents.IsValidIndex(HeadSlot))
	{
		TintAllElements(PartComponents[HeadSlot], Catalog->HeadSkinColorParameter, SkinColor);
		TintAllElements(PartComponents[HeadSlot], Catalog->EyeColorParameter, EyeColor);
	}

	// Cheveux, sourcils, barbe et moustache descendent tous de M_Hairstyle_Base :
	// un seul parametre les teint ensemble, ce qui est aussi le comportement
	// qu'on veut -- une barbe blonde sur des cheveux bruns se remarque.
	static const ECharacterPartCategory HairLikeCategories[] = {
		ECharacterPartCategory::Hair,
		ECharacterPartCategory::Eyebrows,
		ECharacterPartCategory::Beard,
		ECharacterPartCategory::Mustache
	};

	for (const ECharacterPartCategory Category : HairLikeCategories)
	{
		const int32 Index = static_cast<int32>(Category);
		if (PartComponents.IsValidIndex(Index))
		{
			TintAllElements(PartComponents[Index], Catalog->HairColorParameter, HairColor);
		}
	}
}

void UCharacterAppearanceComponent::SetPartMesh(USkeletalMeshComponent* MeshComponent, USkeletalMesh* NewMesh)
{
	if (!MeshComponent || MeshComponent->GetSkeletalMeshAsset() == NewMesh)
	{
		// Rien a faire, et surtout : on ne jette pas les MID en place pour
		// rien. ApplyColors les reutilise, c'est ce qui rend le changement de
		// couleur seul aussi bon marche.
		return;
	}

	// LE point du montage a ne pas oublier : les materiaux surcharges d'un
	// composant vivent dans OverrideMaterials, un tableau qui lui appartient et
	// que changer de mesh ne touche PAS. Sans ce vidage, passer du corps A au
	// corps B garde le MID derive de MI_BodyA_a sur l'element 0 -- on obtient
	// une silhouette feminine peinte avec la peau et les abdominaux du modele
	// masculin. Le symptome trompe, parce que le mesh, lui, a bien change.
	MeshComponent->EmptyOverrideMaterials();
	MeshComponent->SetSkeletalMeshAsset(NewMesh);
}

void UCharacterAppearanceComponent::TintAllElements(USkeletalMeshComponent* MeshComponent, FName Parameter, const FLinearColor& Color)
{
	if (!MeshComponent || Parameter.IsNone() || !MeshComponent->GetSkeletalMeshAsset())
	{
		return;
	}

	const int32 ElementCount = MeshComponent->GetNumMaterials();
	for (int32 Element = 0; Element < ElementCount; ++Element)
	{
		// Rend le materiau dynamique DEJA en place s'il y en a un, et n'en cree
		// un que la premiere fois (verifie dans PrimitiveComponent.cpp). C'est
		// ce qui rend cette fonction sure a appeler par image.
		if (UMaterialInstanceDynamic* Dynamic = MeshComponent->CreateAndSetMaterialInstanceDynamic(Element))
		{
			Dynamic->SetVectorParameterValue(Parameter, Color);
		}
	}
}

// ----------------------------------------------------------------------
// Reglage par nom, appele par les commandes console d'AFpsPlayer
// ----------------------------------------------------------------------

void UCharacterAppearanceComponent::ApplyPartByName(const FString& Part, int32 Index)
{
	if (!Catalog)
	{
		UE_LOG(LogTemp, Warning, TEXT("EmberPart : aucun catalogue assigne."));
		return;
	}

	FCharacterAppearance Next = Appearance;

	if (Part.Equals(TEXT("body"), ESearchCase::IgnoreCase))
	{
		Next.BodyType = static_cast<ECharacterBodyType>(FMath::Clamp(Index, 0, static_cast<int32>(ECharacterBodyType::Count) - 1));
	}
	else if (Part.Equals(TEXT("eyes"), ESearchCase::IgnoreCase))
	{
		Next.EyesIndex = static_cast<uint8>(FMath::Max(0, Index));
	}
	else
	{
		ECharacterPartCategory Category;
		if (!AppearanceParsePartCategory(Part, Category))
		{
			UE_LOG(LogTemp, Warning, TEXT("EmberPart : categorie inconnue '%s'. Attendu : body, head, hair, eyebrows, beard, mustache, eyes."), *Part);
			return;
		}

		if (uint8* Target = AppearancePartIndexPtr(Next, Category))
		{
			// Un index negatif retire la piece : c'est la seule facon de se
			// raser la barbe depuis la console, NoPart valant 255.
			*Target = (Index < 0) ? FCharacterAppearance::NoPart : static_cast<uint8>(Index);
		}
	}

	ApplyAppearance(Next);

	// Apres l'application, donc apres Sanitize : on affiche ce qui a REELLEMENT
	// ete pose, pas ce qui a ete demande. Un index hors bornes se lit alors tout
	// de suite, au lieu de laisser croire que la commande n'a rien fait.
	UE_LOG(LogTemp, Log, TEXT("EmberPart %s %d -> applique."), *Part, Index);
	LogAppearance();
}

void UCharacterAppearanceComponent::ApplyColorByName(const FString& Part, int32 ColorIndex)
{
	FCharacterAppearance Next = Appearance;

	const uint8 Index = static_cast<uint8>(FMath::Max(0, ColorIndex));

	if (Part.Equals(TEXT("skin"), ESearchCase::IgnoreCase))
	{
		Next.SkinColorIndex = Index;
	}
	else if (Part.Equals(TEXT("hair"), ESearchCase::IgnoreCase))
	{
		Next.HairColorIndex = Index;
	}
	else if (Part.Equals(TEXT("eye"), ESearchCase::IgnoreCase))
	{
		Next.EyeColorIndex = Index;
	}
	else if (Part.Equals(TEXT("underwear"), ESearchCase::IgnoreCase))
	{
		Next.UnderwearColorIndex = Index;
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("EmberColor : categorie inconnue '%s'. Attendu : skin, hair, eye, underwear."), *Part);
		return;
	}

	ApplyAppearance(Next);
	LogAppearance();
}

void UCharacterAppearanceComponent::LogAppearance() const
{
	if (!Catalog)
	{
		UE_LOG(LogTemp, Warning, TEXT("EmberAppearanceDump : aucun catalogue assigne."));
		return;
	}

	const ECharacterBodyType Body = Appearance.BodyType;

	UE_LOG(LogTemp, Log, TEXT("--- Apparence de %s ---"), *GetNameSafe(GetOwner()));
	UE_LOG(LogTemp, Log, TEXT("  body      = %d  (0..%d)"), static_cast<int32>(Body), static_cast<int32>(ECharacterBodyType::Count) - 1);
	UE_LOG(LogTemp, Log, TEXT("  head      = %d  (%d options)"), Appearance.HeadIndex, Catalog->GetPartCount(Body, ECharacterPartCategory::Head));
	UE_LOG(LogTemp, Log, TEXT("  hair      = %d  (%d options)"), Appearance.HairIndex, Catalog->GetPartCount(Body, ECharacterPartCategory::Hair));
	UE_LOG(LogTemp, Log, TEXT("  eyebrows  = %d  (%d options)"), Appearance.EyebrowsIndex, Catalog->GetPartCount(Body, ECharacterPartCategory::Eyebrows));
	UE_LOG(LogTemp, Log, TEXT("  beard     = %d  (%d options)"), Appearance.BeardIndex, Catalog->GetPartCount(Body, ECharacterPartCategory::Beard));
	UE_LOG(LogTemp, Log, TEXT("  mustache  = %d  (%d options)"), Appearance.MustacheIndex, Catalog->GetPartCount(Body, ECharacterPartCategory::Mustache));
	UE_LOG(LogTemp, Log, TEXT("  eyes      = %d  (%d options)"), Appearance.EyesIndex, Catalog->EyeMaterials.Num());
	UE_LOG(LogTemp, Log, TEXT("  skin      = couleur %d  (%d couleurs)"), Appearance.SkinColorIndex, Catalog->SkinColors.Num());
	UE_LOG(LogTemp, Log, TEXT("  hairColor = couleur %d  (%d couleurs)"), Appearance.HairColorIndex, Catalog->HairColors.Num());
	UE_LOG(LogTemp, Log, TEXT("  eyeColor  = couleur %d  (%d couleurs)"), Appearance.EyeColorIndex, Catalog->EyeColors.Num());
	UE_LOG(LogTemp, Log, TEXT("  underwear = couleur %d  (%d couleurs)"), Appearance.UnderwearColorIndex, Catalog->UnderwearColors.Num());
	UE_LOG(LogTemp, Log, TEXT("  255 = aucune piece (barbe rasee, crane nu)"));
}
