// Portage UE 5.5 - voir LPHairstyleSubsystem.h #include "LPHairstyleSubsystem.h" #include "AssetRegistry/IAssetRegistry.h" #include "Engine/GameInstance.h" #include "Algo/Find.h" #include "Engine/SkeletalMesh.h" #include "Engine/Texture2D.h" #include "HAL/FileManager.h" #include "HAL/PlatformTime.h" #include "LifePlay.h" #include "Misc/CommandLine.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "UObject/UnrealType.h" namespace { const FName HairstyleTableName(TEXT("HairstyleTable")); const FName MorphableHairsName(TEXT("MorphableHairs")); const FName ModEnabledsName(TEXT("ModEnableds")); constexpr double CheckInterval = 2.0; /** Listes de pilosite : cle du fichier, propriete de GI, dossier des textures (prefixe de Person.setBHairstyle). */ struct FTextureList { const TCHAR* Key; const TCHAR* Property; const TCHAR* Folder; }; const FTextureList TextureLists[] = { { TEXT("FACIAL_HAIR"), TEXT("FHairstyles"), TEXT("/Game/Heavy/Daz/Michael/hairs/other/") }, { TEXT("BODY_HAIR_M"), TEXT("BHairstyles_M"), TEXT("/Game/Heavy/Daz/Michael/hairs/other/") }, { TEXT("BODY_HAIR_F"), TEXT("BHairstyles"), TEXT("/Game/Heavy/Daz/Bridget/Hairs/Pubic/") }, }; /** Champ d'une structure Blueprint par son nom d'auteur (les noms internes portent un suffixe GUID). */ const FProperty* FindField(const UStruct* Struct, const TCHAR* AuthoredName) { for (TFieldIterator It(Struct); It; ++It) { if (It->GetAuthoredName() == AuthoredName) { return *It; } } return nullptr; } /** Affecte un champ a partir de son texte (FName, FString, bool, flottant : meme syntaxe que les valeurs par defaut). */ bool SetField(const UStruct* Struct, void* Data, const TCHAR* AuthoredName, const FString& Text) { const FProperty* Prop = FindField(Struct, AuthoredName); return Prop && Prop->ImportText_InContainer(*Text, Data, nullptr, PPF_None) != nullptr; } /** Vide un champ texte (chaine vide, nom nul) : ImportText ne sait pas produire une chaine vide. */ bool ClearField(const UStruct* Struct, void* Data, const TCHAR* AuthoredName) { const FProperty* Prop = FindField(Struct, AuthoredName); if (const FStrProperty* Str = CastField(Prop)) { Str->SetPropertyValue_InContainer(Data, FString()); return true; } if (const FNameProperty* Name = CastField(Prop)) { Name->SetPropertyValue_InContainer(Data, NAME_None); return true; } return false; } FString GetFieldText(const UStruct* Struct, const void* Data, const TCHAR* AuthoredName) { FString Out; if (const FProperty* Prop = FindField(Struct, AuthoredName)) { Prop->ExportTextItem_InContainer(Out, Data, nullptr, nullptr, PPF_None); } return Out.TrimQuotes(); } const FArrayProperty* StructArray(const UObject* GI, FName Name, const UScriptStruct*& OutStruct) { const FArrayProperty* Prop = GI ? CastField(GI->GetClass()->FindPropertyByName(Name)) : nullptr; const FStructProperty* Inner = Prop ? CastField(Prop->Inner) : nullptr; OutStruct = Inner ? Inner->Struct : nullptr; return OutStruct ? Prop : nullptr; } FString ExportProperty(const FProperty* Prop, const UObject* GI) { FString Out; Prop->ExportTextItem_Direct(Out, Prop->ContainerPtrToValuePtr(GI), nullptr, const_cast(GI), PPF_None); return Out; } /** Entrees d'une map "cle=valeur", triees : comparaison independante de l'ordre interne. */ FString ExportMapSorted(const FMapProperty* MapProp, const UObject* GI) { TArray Lines; FScriptMapHelper Map(MapProp, MapProp->ContainerPtrToValuePtr(GI)); for (FScriptMapHelper::FIterator It(Map); It; ++It) { FString Key, Value; MapProp->KeyProp->ExportTextItem_Direct(Key, Map.GetKeyPtr(It), nullptr, nullptr, PPF_None); MapProp->ValueProp->ExportTextItem_Direct(Value, Map.GetValuePtr(It), nullptr, nullptr, PPF_None); Lines.Add(Key + TEXT("=") + Value); } Lines.Sort(); return FString::Join(Lines, TEXT("\n")); } bool AddMorph(const FMapProperty* MapProp, UObject* GI, const FString& Key, const FString& Morph, const FString& Value) { const FStructProperty* ValueProp = CastField(MapProp->ValueProp); if (!ValueProp) { return false; } FScriptMapHelper Map(MapProp, MapProp->ContainerPtrToValuePtr(GI)); for (FScriptMapHelper::FIterator It(Map); It; ++It) { FString Existing; MapProp->KeyProp->ExportTextItem_Direct(Existing, Map.GetKeyPtr(It), nullptr, nullptr, PPF_None); if (Existing.TrimQuotes().Equals(Key, ESearchCase::IgnoreCase)) { return false; } } const int32 Pair = Map.AddDefaultValue_Invalid_NeedsRehash(); MapProp->KeyProp->ImportText_Direct(*Key, Map.GetKeyPtr(Pair), nullptr, PPF_None); if (!Morph.IsEmpty()) { SetField(ValueProp->Struct, Map.GetValuePtr(Pair), TEXT("Name"), Morph); SetField(ValueProp->Struct, Map.GetValuePtr(Pair), TEXT("Float"), Value); } Map.Rehash(); return true; } } void ULPHairstyleSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); ScanFiles(); if (Defs.Num() == 0 && BaseDefs.Num() == 0 && Textures.Num() == 0 && BaseTextures.Num() == 0) { return; } EnsureDefaultEnabledModules(); LastSignature = EnabledSignature(); Sync(); TickHandle = FTSTicker::GetCoreTicker().AddTicker(FTickerDelegate::CreateUObject(this, &ULPHairstyleSubsystem::Tick)); } void ULPHairstyleSubsystem::Deinitialize() { FTSTicker::GetCoreTicker().RemoveTicker(TickHandle); Super::Deinitialize(); } bool ULPHairstyleSubsystem::Tick(float DeltaTime) { const double Now = FPlatformTime::Seconds(); if (Now < NextCheck) { return true; } NextCheck = Now + CheckInterval; // GI.LoadSettings remplace GI.ModEnableds par la liste sauvegardee : les modules actifs par defaut y sont remis. EnsureDefaultEnabledModules(); const FString Signature = EnabledSignature(); if (Signature != LastSignature) { LastSignature = Signature; UE_LOG(LogLifePlay, Log, TEXT("Coiffures : liste des modules actifs modifiee, reconstruction")); Sync(); } return true; } void ULPHairstyleSubsystem::ScanFiles() { const FString ModulesDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir() / TEXT("Modules")); // Modules actifs par defaut : Modules//.lpmod avec MODULE_DEFAULT_ENABLED: true. TArray ModFiles; IFileManager::Get().FindFilesRecursive(ModFiles, *ModulesDir, TEXT("*.lpmod"), true, false); for (const FString& ModFile : ModFiles) { FString ModText; if (!FFileHelper::LoadFileToString(ModText, *ModFile)) { continue; } FString UniqueId; bool bDefault = false; TArray ModLines; ModText.ParseIntoArrayLines(ModLines, true); for (const FString& ModLine : ModLines) { FString Key, Value; if (ModLine.Split(TEXT(":"), &Key, &Value)) { Key = Key.TrimStartAndEnd().ToUpper(); Value = Value.TrimStartAndEnd(); if (Key == TEXT("MODULE_UNIQUEID")) { UniqueId = Value; } else if (Key == TEXT("MODULE_DEFAULT_ENABLED")) { bDefault = Value.Equals(TEXT("true"), ESearchCase::IgnoreCase); } } } if (bDefault && !UniqueId.IsEmpty()) { DefaultEnabledModules.AddUnique(UniqueId); } } TArray Files; IFileManager::Get().FindFilesRecursive(Files, *ModulesDir, TEXT("*.lphair"), true, false); Files.Sort(); for (const FString& File : Files) { FString Relative = File; FPaths::MakePathRelativeTo(Relative, *(ModulesDir + TEXT("/"))); FString Module, Rest; if (!Relative.Split(TEXT("/"), &Module, &Rest)) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s ignore (doit etre dans un dossier de module)"), *Relative); continue; } FString Text; if (!FFileHelper::LoadFileToString(Text, *File)) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : lecture impossible de %s"), *Relative); continue; } ParseFile(Relative, Module, Text); } if (Files.Num() > 0) { UE_LOG(LogLifePlay, Display, TEXT("Coiffures : %d fichier(s) .lphair, base %s (%d coiffure(s), %d morph(s), %d pilosite(s)), modules : %d coiffure(s), %d pilosite(s)"), Files.Num(), BaseFile.IsEmpty() ? TEXT("du Blueprint") : *BaseFile, BaseDefs.Num(), BaseMorphs.Num(), BaseTextures.Num(), Defs.Num(), Textures.Num()); } } void ULPHairstyleSubsystem::ParseFile(const FString& Relative, const FString& Module, const FString& Text) { TArray FileDefs; TArray FileMorphs; TArray FileTextures; bool bBase = false; TArray Lines; Text.ParseIntoArrayLines(Lines, true); for (const FString& RawLine : Lines) { const FString Line = RawLine.TrimStartAndEnd(); FString Key, Value; if (Line.StartsWith(TEXT("//")) || !Line.Split(TEXT(":"), &Key, &Value)) { continue; } Key = Key.TrimStartAndEnd().ToUpper(); Value = Value.TrimStartAndEnd(); if (Key == TEXT("MODE")) { bBase = Value.Equals(TEXT("BASE"), ESearchCase::IgnoreCase); } else if (Key == TEXT("HAIRSTYLE")) { FHairDef& Def = FileDefs.AddDefaulted_GetRef(); Def.File = Relative; Def.Module = Module; Def.bNone = Value.Equals(TEXT("NONE"), ESearchCase::IgnoreCase); Def.Path = Def.bNone ? FString() : Value; // Un chemin de paquet sans nom d'objet (/Game/X/Nom) designe l'objet du meme nom. if (!Def.bNone && !Def.Path.Contains(TEXT("."))) { Def.Path += TEXT(".") + FPaths::GetBaseFilename(Def.Path); } } else if (Key == TEXT("MORPHABLE")) { TArray Parts; Value.ParseIntoArrayWS(Parts); if (Parts.Num() > 0) { FileMorphs.Add({ Parts[0], Parts.IsValidIndex(1) ? Parts[1] : FString(), Parts.IsValidIndex(2) ? Parts[2] : FString() }); } } else if (const FTextureList* List = Algo::FindByPredicate(TextureLists, [&Key](const FTextureList& L) { return Key == L.Key; })) { FileTextures.Add({ int32(List - TextureLists), Value, Relative, Module }); } else if (FileDefs.Num() == 0) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s, ligne '%s' avant toute ligne HAIRSTYLE, ignoree"), *Relative, *Line); } else if (Key == TEXT("SEX")) { FileDefs.Last().bMale = Value.StartsWith(TEXT("M"), ESearchCase::IgnoreCase); } else if (Key == TEXT("DAZ")) { FileDefs.Last().bDaz = !Value.Equals(TEXT("false"), ESearchCase::IgnoreCase); } else if (Key == TEXT("FIT_MORPH")) { Value.Split(TEXT(" "), &FileDefs.Last().FitMorph, &FileDefs.Last().FitValue); FileDefs.Last().FitValue.TrimStartAndEndInline(); } } if (bBase) { if (!BaseFile.IsEmpty()) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s ignore, la base est deja %s"), *Relative, *BaseFile); return; } BaseFile = Relative; BaseDefs = MoveTemp(FileDefs); BaseMorphs = MoveTemp(FileMorphs); BaseTextures = MoveTemp(FileTextures); return; } if (FileDefs.Num() == 0 && FileTextures.Num() == 0) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s sans ligne HAIRSTYLE, ignore"), *Relative); } Defs.Append(MoveTemp(FileDefs)); Textures.Append(MoveTemp(FileTextures)); } FString ULPHairstyleSubsystem::EnabledSignature() const { const UObject* GI = GetGameInstance(); const UScriptStruct* Struct = nullptr; const FArrayProperty* Prop = StructArray(GI, ModEnabledsName, Struct); if (!Prop) { return FString(); } FString Signature; FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr(GI)); for (int32 i = 0; i < Helper.Num(); ++i) { Signature += GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("UniqueID")) + TEXT("=") + GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("IsEnabled")) + TEXT(";"); } return Signature; } bool ULPHairstyleSubsystem::IsModuleEnabled(const FString& Module) const { const UObject* GI = GetGameInstance(); const UScriptStruct* Struct = nullptr; const FArrayProperty* Prop = StructArray(GI, ModEnabledsName, Struct); if (!Prop) { return true; } FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr(GI)); for (int32 i = 0; i < Helper.Num(); ++i) { if (GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("UniqueID")).Equals(Module, ESearchCase::IgnoreCase)) { return GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("IsEnabled")).Equals(TEXT("True"), ESearchCase::IgnoreCase); } } return false; // module absent de la liste : inactif, comme GI.IsModEnabled (le gestionnaire l'ajoute decoche) } bool ULPHairstyleSubsystem::EnsureDefaultEnabledModules() { UObject* GI = GetGameInstance(); const UScriptStruct* Struct = nullptr; const FArrayProperty* Prop = StructArray(GI, ModEnabledsName, Struct); if (!Prop || DefaultEnabledModules.Num() == 0) { return false; } FScriptArrayHelper Helper(Prop, Prop->ContainerPtrToValuePtr(GI)); bool bChanged = false; for (const FString& Module : DefaultEnabledModules) { bool bFound = false; for (int32 i = 0; i < Helper.Num() && !bFound; ++i) { bFound = GetFieldText(Struct, Helper.GetRawPtr(i), TEXT("UniqueID")).Equals(Module, ESearchCase::IgnoreCase); } if (!bFound) { const int32 Index = Helper.AddValue(); SetField(Struct, Helper.GetRawPtr(Index), TEXT("UniqueID"), Module); SetField(Struct, Helper.GetRawPtr(Index), TEXT("IsEnabled"), TEXT("True")); UE_LOG(LogLifePlay, Display, TEXT("Modules : %s actif par defaut (MODULE_DEFAULT_ENABLED), ajoute a la liste des mods"), *Module); bChanged = true; } } return bChanged; } void ULPHairstyleSubsystem::Sync() { UObject* GI = GetGameInstance(); const UScriptStruct* Struct = nullptr; const FArrayProperty* TableProp = StructArray(GI, HairstyleTableName, Struct); const FMapProperty* MapProp = GI ? CastField(GI->GetClass()->FindPropertyByName(MorphableHairsName)) : nullptr; if (!TableProp || !MapProp) { UE_LOG(LogLifePlay, Error, TEXT("Coiffures : GI.HairstyleTable ou GI.MorphableHairs introuvable sur %s, table laissee telle quelle"), *GetNameSafe(GI)); return; } if (!bCaptured) { BlueprintTableText = ExportProperty(TableProp, GI); BlueprintMorphsText = ExportProperty(MapProp, GI); bCaptured = true; } FScriptArrayHelper Table(TableProp, TableProp->ContainerPtrToValuePtr(GI)); FScriptMapHelper Map(MapProp, MapProp->ContainerPtrToValuePtr(GI)); // 1. Base : fichier MODE: BASE reconstruit dans son ordre, sinon valeurs du Blueprint. if (BaseDefs.Num() > 0) { const FString BlueprintMorphsSorted = ExportMapSorted(MapProp, GI); Table.EmptyValues(); Map.EmptyValues(); int32 BadFields = 0; for (const FHairDef& Def : BaseDefs) { void* Entry = Table.GetRawPtr(Table.AddValue()); // Entree "sans cheveux" (NONE) : chemin vide ; sexe et indicateur Daz repris tels quels (le Blueprint en a trois). if (!(SetField(Struct, Entry, TEXT("Bool"), Def.bMale ? TEXT("True") : TEXT("False")) && SetField(Struct, Entry, TEXT("Daz"), Def.bDaz ? TEXT("True") : TEXT("False")) // Le Blueprint stocke un chemin explicitement vide (""), distinct de la valeur par defaut du champ. && (Def.bNone ? ClearField(Struct, Entry, TEXT("SkeletalMeshPath")) : SetField(Struct, Entry, TEXT("SkeletalMeshPath"), Def.Path)))) { ++BadFields; } } for (const FMorphDef& Morph : BaseMorphs) { AddMorph(MapProp, GI, Morph.Key, Morph.Morph, Morph.Value); } if (BadFields > 0) { UE_LOG(LogLifePlay, Error, TEXT("Coiffures : %d entree(s) de base incompletes (structure de HairstyleTable inattendue)"), BadFields); } static const bool bVerify = FParse::Param(FCommandLine::Get(), TEXT("LPVerifyHairstyles")); if (bVerify) { const bool bSameTable = ExportProperty(TableProp, GI) == BlueprintTableText; const bool bSameMorphs = ExportMapSorted(MapProp, GI) == BlueprintMorphsSorted; UE_LOG(LogLifePlay, Display, TEXT("Coiffures, verification de la base %s : table %s (%d entrees), morphs %s (%d entrees)"), *BaseFile, bSameTable ? TEXT("IDENTIQUE au Blueprint") : TEXT("DIFFERENTE du Blueprint"), Table.Num(), bSameMorphs ? TEXT("IDENTIQUES") : TEXT("DIFFERENTS"), Map.Num()); if (!bSameTable) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : Blueprint = %s"), *BlueprintTableText.Left(2000)); UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : fichier = %s"), *ExportProperty(TableProp, GI).Left(2000)); } } } else { TableProp->ImportText_Direct(*BlueprintTableText, TableProp->ContainerPtrToValuePtr(GI), GI, PPF_None); MapProp->ImportText_Direct(*BlueprintMorphsText, MapProp->ContainerPtrToValuePtr(GI), GI, PPF_None); } const int32 BaseCount = Table.Num(); // 2. Coiffures des modules actifs, en fin de table. TSet Known; for (int32 i = 0; i < Table.Num(); ++i) { Known.Add(GetFieldText(Struct, Table.GetRawPtr(i), TEXT("SkeletalMeshPath"))); } IAssetRegistry& Registry = IAssetRegistry::GetChecked(); int32 Added = 0, Skipped = 0; for (const FHairDef& Def : Defs) { if (!IsModuleEnabled(Def.Module)) { ++Skipped; continue; } if (Def.bNone || Known.Contains(Def.Path)) { UE_LOG(LogLifePlay, Log, TEXT("Coiffures : %s deja dans la table ou vide (%s)"), *Def.Path, *Def.File); ++Skipped; continue; } const FAssetData Asset = Registry.GetAssetByObjectPath(FSoftObjectPath(Def.Path)); if (!Asset.IsValid() || !Asset.IsInstanceOf(USkeletalMesh::StaticClass())) { UE_LOG(LogLifePlay, Warning, TEXT("Coiffures : %s introuvable ou n'est pas un SkeletalMesh (%s), ignore"), *Def.Path, *Def.File); ++Skipped; continue; } const int32 Index = Table.AddValue(); void* Entry = Table.GetRawPtr(Index); if (!(SetField(Struct, Entry, TEXT("SkeletalMeshPath"), Def.Path) && SetField(Struct, Entry, TEXT("Bool"), Def.bMale ? TEXT("True") : TEXT("False")) && SetField(Struct, Entry, TEXT("Daz"), Def.bDaz ? TEXT("True") : TEXT("False")))) { Table.RemoveValues(Index, 1); UE_LOG(LogLifePlay, Error, TEXT("Coiffures : structure de HairstyleTable inattendue, %s non ajoute"), *Def.Path); ++Skipped; continue; } Known.Add(Def.Path); ++Added; // Morph d'ajustement : cle = nom de l'objet, comme pour les coiffures du Blueprint. const FString Name = FPaths::GetExtension(Def.Path); if (!Def.FitMorph.IsEmpty()) { AddMorph(MapProp, GI, Name, Def.FitMorph, Def.FitValue); } UE_LOG(LogLifePlay, Log, TEXT("Coiffures : %s ajoutee (index %d, %s, module %s)"), *Name, Index, Def.bMale ? TEXT("M") : TEXT("F"), *Def.Module); } UE_LOG(LogLifePlay, Display, TEXT("Coiffures : base %d entree(s) (%s), %d ajoutee(s) par les modules, %d ignoree(s), table de %d entree(s), %d morph(s)"), BaseCount, BaseFile.IsEmpty() ? TEXT("Blueprint") : *BaseFile, Added, Skipped, Table.Num(), Map.Num()); SyncTextureLists(GI); } void ULPHairstyleSubsystem::SyncTextureLists(UObject* GI) { static const bool bVerify = FParse::Param(FCommandLine::Get(), TEXT("LPVerifyHairstyles")); IAssetRegistry& Registry = IAssetRegistry::GetChecked(); const bool bFirst = BlueprintListTexts.Num() == 0; for (int32 ListIndex = 0; ListIndex < UE_ARRAY_COUNT(TextureLists); ++ListIndex) { const FTextureList& Spec = TextureLists[ListIndex]; const FArrayProperty* Prop = CastField(GI->GetClass()->FindPropertyByName(Spec.Property)); if (!Prop || !CastField(Prop->Inner)) { UE_LOG(LogLifePlay, Error, TEXT("Pilosite : GI.%s introuvable ou de type inattendu"), Spec.Property); if (bFirst) { BlueprintListTexts.Add(FString()); } continue; } TArray& List = *Prop->ContainerPtrToValuePtr>(GI); if (bFirst) { BlueprintListTexts.Add(ExportProperty(Prop, GI)); } // 1. Base : noms du fichier de base pour cette liste, sinon valeur du Blueprint. TArray Base; for (const FTextureDef& Def : BaseTextures) { if (Def.List == ListIndex) { Base.Add(Def.Name); } } if (Base.Num() > 0) { List = Base; if (bVerify) { const bool bSame = ExportProperty(Prop, GI) == BlueprintListTexts[ListIndex]; UE_LOG(LogLifePlay, Display, TEXT("Pilosite, verification de la base : GI.%s %s (%d noms)"), Spec.Property, bSame ? TEXT("IDENTIQUE au Blueprint") : TEXT("DIFFERENTE du Blueprint"), List.Num()); } } else { Prop->ImportText_Direct(*BlueprintListTexts[ListIndex], &List, GI, PPF_None); } const int32 BaseCount = List.Num(); // 2. Noms des modules actifs : texture presente dans le dossier impose, pas de doublon. for (const FTextureDef& Def : Textures) { if (Def.List != ListIndex || !IsModuleEnabled(Def.Module) || List.Contains(Def.Name)) { continue; } const FString Path = FString(Spec.Folder) + Def.Name + TEXT(".") + Def.Name; const FAssetData Asset = Registry.GetAssetByObjectPath(FSoftObjectPath(Path)); if (!Asset.IsValid() || !Asset.IsInstanceOf(UTexture2D::StaticClass())) { UE_LOG(LogLifePlay, Warning, TEXT("Pilosite : %s introuvable ou n'est pas une texture (%s), ignore"), *Path, *Def.File); continue; } List.Add(Def.Name); UE_LOG(LogLifePlay, Log, TEXT("Pilosite : %s ajoute a GI.%s (module %s)"), *Def.Name, Spec.Property, *Def.Module); } UE_LOG(LogLifePlay, Display, TEXT("Pilosite : GI.%s, base %d nom(s), %d ajoute(s) par les modules"), Spec.Property, BaseCount, List.Num() - BaseCount); } }