// Copyright Epic Games, Inc. All Rights Reserved. #include "AssetSystemContentBrowserInfoProvider.h" #include "ActorFolder.h" #include "ActorFolderDesc.h" #include "Algo/Sort.h" #include "AssetDefinition.h" #include "AssetDefinitionAssetInfo.h" #include "AssetDefinitionRegistry.h" #include "AssetRegistry/AssetRegistryModule.h" #include "AssetToolsModule.h" #include "AssetViewTypes.h" #include "AutoReimport/AssetSourceFilenameCache.h" #include "CollectionManagerModule.h" #include "Containers/VersePath.h" #include "ContentBrowserDataSource.h" #include "ContentBrowserUtils.h" #include "IAssetTools.h" #include "ICollectionContainer.h" #include "ICollectionManager.h" #include "Misc/EngineBuildSettings.h" #define LOCTEXT_NAMESPACE "AssetSystemContentBrowserInfoProvider" FAssetSystemContentBrowserInfoProvider::FAssetSystemContentBrowserInfoProvider(const TSharedPtr& InAssetItem) : AssetItem(InAssetItem) { if (AssetItem.IsValid()) { OnItemDataChangedCacheDisplayTagsDelegateHandle = AssetItem->OnItemDataChanged().AddRaw(this, &FAssetSystemContentBrowserInfoProvider::CacheDisplayTags); OnItemDataChangedCacheDirtyExternalPackageDelegateHandle = AssetItem->OnItemDataChanged().AddRaw(this, &FAssetSystemContentBrowserInfoProvider::CacheDirtyExternalPackageInfo); } FAssetData AssetData; AssetItem->GetItem().Legacy_TryGetAssetData(AssetData); if (AssetData.IsValid()) { if (const UAssetDefinition* AssetDefinition = UAssetDefinitionRegistry::Get()->GetAssetDefinitionForAsset(AssetData)) { bShouldSaveExternalPackages = AssetDefinition->ShouldSaveExternalPackages(); } } CacheDisplayTags(); } FAssetSystemContentBrowserInfoProvider::~FAssetSystemContentBrowserInfoProvider() { if (AssetItem.IsValid()) { AssetItem->OnItemDataChanged().Remove(OnItemDataChangedCacheDisplayTagsDelegateHandle); AssetItem->OnItemDataChanged().Remove(OnItemDataChangedCacheDirtyExternalPackageDelegateHandle); OnItemDataChangedCacheDisplayTagsDelegateHandle.Reset(); OnItemDataChangedCacheDirtyExternalPackageDelegateHandle.Reset(); } } void FAssetSystemContentBrowserInfoProvider::PopulateAssetInfo(TArray& OutAssetDisplayInfo) const { if (AssetItem->IsFile()) { // The tooltip contains the name, class, path, asset registry tags and source control status FText PublicStateText; const FSlateBrush* PublicStateIcon = nullptr; // Create a box to hold every line of info in the body of the tooltip TSharedRef InfoBox = SNew(SVerticalBox); FAssetData ItemAssetData; AssetItem->GetItem().Legacy_TryGetAssetData(ItemAssetData); // TODO: Always use the virtual path? FAssetDisplayInfo PathInfo = FAssetDisplayInfo(); PathInfo.StatusTitle = LOCTEXT("TileViewTooltipPath", "Path"); if (ItemAssetData.IsValid()) { PathInfo.StatusDescription = FText::FromName(ItemAssetData.PackagePath); } else { PathInfo.StatusDescription = FText::FromName(AssetItem->GetItem().GetVirtualPath()); } OutAssetDisplayInfo.Add(PathInfo); if (ItemAssetData.IsValid() && FAssetToolsModule::GetModule().Get().ShowingContentVersePath()) { UE::Core::FVersePath VersePath = ItemAssetData.GetVersePath(); if (VersePath.IsValid()) { FAssetDisplayInfo& VersePathInfo = OutAssetDisplayInfo.AddDefaulted_GetRef(); VersePathInfo.StatusTitle = LOCTEXT("TileViewTooltipVersePath", "Verse Path"); VersePathInfo.StatusDescription = FText::FromString(VersePath.ToString()); } } if (ItemAssetData.IsValid() && ItemAssetData.PackageName != NAME_None) { const FString PackagePathWithinRoot = ContentBrowserUtils::GetPackagePathWithinRoot(ItemAssetData.PackageName.ToString()); int32 PackageNameLength = PackagePathWithinRoot.Len(); int32 MaxAssetPathLen = ContentBrowserUtils::GetMaxAssetPathLen(); // Asset Path Length Info FAssetDisplayInfo AssetPathLengthInfo = FAssetDisplayInfo(); AssetPathLengthInfo.StatusTitle = LOCTEXT("TileViewTooltipAssetPathLengthKey", "Asset Filepath Length"); AssetPathLengthInfo.StatusDescription = FText::Format(LOCTEXT("TileViewTooltipAssetPathLengthValue", "{0} / {1}"), FText::AsNumber(PackageNameLength), FText::AsNumber(MaxAssetPathLen)); OutAssetDisplayInfo.Add(AssetPathLengthInfo); int32 PackageNameLengthForCooking = ContentBrowserUtils::GetPackageLengthForCooking(ItemAssetData.PackageName.ToString(), FEngineBuildSettings::IsInternalBuild()); // Cook Path Length Info int32 MaxCookPathLen = ContentBrowserUtils::GetMaxCookPathLen(); FAssetDisplayInfo CookPathLenInfo = FAssetDisplayInfo(); CookPathLenInfo.StatusTitle = LOCTEXT("TileViewTooltipPathLengthForCookingKey", "Cooking Filepath Length"); CookPathLenInfo.StatusDescription = FText::Format(LOCTEXT("TileViewTooltipPathLengthForCookingValue", "{0} / {1}"), FText::AsNumber(PackageNameLengthForCooking), FText::AsNumber(MaxCookPathLen)); OutAssetDisplayInfo.Add(CookPathLenInfo); if (ItemAssetData.GetAssetAccessSpecifier() == EAssetAccessSpecifier::Public) { PublicStateText = LOCTEXT("PublicAssetState", "Public"); } else if (ItemAssetData.GetAssetAccessSpecifier() == EAssetAccessSpecifier::EpicInternal) { PublicStateText = LOCTEXT("EpicInternalAssetState", "Epic Internal"); } else { PublicStateText = LOCTEXT("PrivateAssetState", "Private"); } } if (!AssetItem->GetItem().CanEdit()) { if(AssetItem->GetItem().CanView()) { PublicStateText = LOCTEXT("ViewReadOnlyAssetState", "View / Read Only"); PublicStateIcon = FAppStyle::GetBrush("AssetEditor.ReadOnlyOpenable"); } else { PublicStateText = LOCTEXT("ReadOnlyAssetState", "Read Only"); PublicStateIcon = FAppStyle::GetBrush("Icons.Lock"); } } if(!AssetItem->GetItem().IsSupported()) { PublicStateText = LOCTEXT("UnsupportedAssetState", "Unsupported"); } // Add tags for (const FTagContentBrowserDisplayItem& DisplayTagItem : CachedDisplayTags) { FAssetDisplayInfo DisplayTagInfo = FAssetDisplayInfo(); DisplayTagInfo.StatusTitle = DisplayTagItem.DisplayKey; DisplayTagInfo.StatusDescription = DisplayTagItem.DisplayValue; OutAssetDisplayInfo.Add(DisplayTagInfo); } // Add asset source files if (ItemAssetData.IsValid()) { TOptional ImportInfo = FAssetSourceFilenameCache::ExtractAssetImportInfo(ItemAssetData); if (ImportInfo.IsSet()) { for (const FAssetImportInfo::FSourceFile& File : ImportInfo->SourceFiles) { FText SourceLabel = LOCTEXT("TileViewTooltipSourceFile", "Source File"); if (File.DisplayLabelName.Len() > 0) { SourceLabel = FText::FromString(FText(LOCTEXT("TileViewTooltipSourceFile", "Source File")).ToString() + TEXT(" (") + File.DisplayLabelName + TEXT(")")); } FAssetDisplayInfo SourceFileInfo = FAssetDisplayInfo(); SourceFileInfo.StatusTitle = SourceLabel; SourceFileInfo.StatusDescription = FText::FromString(File.RelativeFilename); OutAssetDisplayInfo.Add(SourceFileInfo); } } } static const IConsoleVariable* EnablePublicAssetFeatureCVar = IConsoleManager::Get().FindConsoleVariable(TEXT("AssetTools.EnablePublicAssetFeature")); const bool bIsPublicAssetUIEnabled = EnablePublicAssetFeatureCVar && EnablePublicAssetFeatureCVar->GetBool(); // Restriction Info FAssetDisplayInfo RestrictionInfo = FAssetDisplayInfo(); RestrictionInfo.IsVisible = PublicStateIcon && bIsPublicAssetUIEnabled && !PublicStateText.IsEmpty() ? EVisibility::Visible : EVisibility::Collapsed; RestrictionInfo.StatusIcon = PublicStateIcon; RestrictionInfo.StatusTitle = LOCTEXT("Restriction", "Restriction"); RestrictionInfo.StatusDescription = PublicStateText; OutAssetDisplayInfo.Add(RestrictionInfo); // Unsupported Info FAssetDisplayInfo UnsupportedInfo = FAssetDisplayInfo(); UnsupportedInfo.IsVisible = AssetItem->GetItem().IsSupported() ? EVisibility::Collapsed : EVisibility::Visible;; UnsupportedInfo.StatusTitle = LOCTEXT("UnsupportedAssetTitleText", "Item is not supported"); UnsupportedInfo.StatusDescription = LOCTEXT("UnsupportedAssetDescriptionText", "This type of asset is not allowed in this project. Delete unsupported assets to avoid errors."); OutAssetDisplayInfo.Add(UnsupportedInfo); // External Package Info FAssetDisplayInfo ExternalPackageInfo = FAssetDisplayInfo(); ExternalPackageInfo.IsVisible = bShouldSaveExternalPackages && !GetExternalPackagesText().IsEmpty() ? EVisibility::Visible : EVisibility::Collapsed;; ExternalPackageInfo.StatusTitle = LOCTEXT("DirtyExternalPackages", "Modified external packages"); ExternalPackageInfo.StatusDescription = GetExternalPackagesText(); OutAssetDisplayInfo.Add(ExternalPackageInfo); // User Description FAssetDisplayInfo UserDescriptionInfo = FAssetDisplayInfo(); UserDescriptionInfo.IsVisible = GetAssetUserDescription().IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible;; UserDescriptionInfo.StatusTitle = LOCTEXT("UserDescriptionTitle", "User Description"); UserDescriptionInfo.StatusDescription = GetAssetUserDescription(); OutAssetDisplayInfo.Add(UserDescriptionInfo); // Collection Pips if (ItemAssetData.IsValid()) { ICollectionManager& CollectionManager = FCollectionManagerModule::GetModule().Get(); TArray> CollectionContainers; CollectionManager.GetVisibleCollectionContainers(CollectionContainers); TArray CollectionsContainingObject; for (const TSharedPtr& CollectionContainer : CollectionContainers) { CollectionsContainingObject.Reset(); CollectionContainer->GetCollectionsContainingObject(ItemAssetData.ToSoftObjectPath(), CollectionsContainingObject); Algo::Sort(CollectionsContainingObject, [](const FCollectionNameType& A, const FCollectionNameType& B) { int32 result = A.Name.Compare(B.Name); return result < 0 || (result == 0 && A.Type < B.Type); }); bool bAddedCollectionHeader = false; for (const FCollectionNameType& CollectionContainingObject : CollectionsContainingObject) { FCollectionStatusInfo CollectionStatusInfo; if (CollectionContainer->GetCollectionStatusInfo(CollectionContainingObject.Name, CollectionContainingObject.Type, CollectionStatusInfo)) { if (!bAddedCollectionHeader) { // StatusTitle currently used to add a separator for status, need to be changed in future version to allow more configurability bAddedCollectionHeader = true; FAssetDisplayInfo CollectionHeader = FAssetDisplayInfo(); CollectionHeader.StatusTitle = LOCTEXT("CollectionHeaderTitle", "Collection(s)"); CollectionHeader.StatusDescription = FText::GetEmpty(); OutAssetDisplayInfo.Add(CollectionHeader); } FAssetDisplayInfo CollectionInfo = FAssetDisplayInfo(); CollectionInfo.StatusTitle = FText::FromName(CollectionContainingObject.Name); CollectionInfo.StatusDescription = FText::AsNumber(CollectionStatusInfo.NumObjects); OutAssetDisplayInfo.Add(CollectionInfo); } } } } } } void FAssetSystemContentBrowserInfoProvider::CacheDisplayTags() { CachedDisplayTags.Reset(); const FContentBrowserItemDataAttributeValues AssetItemAttributes = AssetItem->GetItem().GetItemAttributes(/*bIncludeMetaData*/true); FAssetData ItemAssetData; AssetItem->GetItem().Legacy_TryGetAssetData(ItemAssetData); // Add all visible attributes for (const auto& AssetItemAttributePair : AssetItemAttributes) { const FName AttributeName = AssetItemAttributePair.Key; const FContentBrowserItemDataAttributeValue& AttributeValue = AssetItemAttributePair.Value; const FContentBrowserItemDataAttributeMetaData& AttributeMetaData = AttributeValue.GetMetaData(); if (AttributeMetaData.AttributeType == UObject::FAssetRegistryTag::TT_Hidden) { continue; } // Build the display value for this attribute FText DisplayValue; if (AttributeValue.GetValueType() == EContentBrowserItemDataAttributeValueType::Text) { DisplayValue = AttributeValue.GetValueText(); } else { const FString AttributeValueStr = AttributeValue.GetValue(); auto ReformatNumberStringForDisplay = [](const FString& InNumberString) -> FText { // Respect the number of decimal places in the source string when converting for display int32 NumDecimalPlaces = 0; { int32 DotIndex = INDEX_NONE; if (InNumberString.FindChar(TEXT('.'), DotIndex)) { NumDecimalPlaces = InNumberString.Len() - DotIndex - 1; } } if (NumDecimalPlaces > 0) { // Convert the number as a double double Num = 0.0; LexFromString(Num, *InNumberString); const FNumberFormattingOptions NumFormatOpts = FNumberFormattingOptions() .SetMinimumFractionalDigits(NumDecimalPlaces) .SetMaximumFractionalDigits(NumDecimalPlaces); return FText::AsNumber(Num, &NumFormatOpts); } const bool bIsSigned = InNumberString.Len() > 0 && (InNumberString[0] == TEXT('-') || InNumberString[0] == TEXT('+')); if (bIsSigned) { // Convert the number as a signed int int64 Num = 0; LexFromString(Num, *InNumberString); return FText::AsNumber(Num); } // Convert the number as an unsigned int uint64 Num = 0; LexFromString(Num, *InNumberString); return FText::AsNumber(Num); }; bool bHasSetDisplayValue = false; // Numerical tags need to format the specified number based on the display flags if (!bHasSetDisplayValue && AttributeMetaData.AttributeType == UObject::FAssetRegistryTag::TT_Numerical && AttributeValueStr.IsNumeric()) { bHasSetDisplayValue = true; const bool bAsMemory = !!(AttributeMetaData.DisplayFlags & UObject::FAssetRegistryTag::TD_Memory); if (bAsMemory) { // Memory should be a 64-bit unsigned number of bytes uint64 NumBytes = 0; LexFromString(NumBytes, *AttributeValueStr); DisplayValue = FText::AsMemory(NumBytes); } else { DisplayValue = ReformatNumberStringForDisplay(AttributeValueStr); } } // Dimensional tags need to be split into their component numbers, with each component number re-formatted if (!bHasSetDisplayValue && AttributeMetaData.AttributeType == UObject::FAssetRegistryTag::TT_Dimensional) { // Formats: // 123 (1D) // 123x234 (2D) // 123x234*345 (2D array) // 123x234x345 (3D) int32 FirstXPos; if (AttributeValueStr.FindChar(TEXT('x'), FirstXPos)) { FString FirstPart = AttributeValueStr.Left(FirstXPos); FString Remainder = AttributeValueStr.Mid(FirstXPos + 1); int32 RemainderSeparatorPos; if (Remainder.FindChar(TEXT('*'), RemainderSeparatorPos)) { // AxB*C form (2D array) FString SecondPart = Remainder.Left(RemainderSeparatorPos); FString ThirdPart = Remainder.Mid(RemainderSeparatorPos + 1); bHasSetDisplayValue = true; DisplayValue = FText::Format(LOCTEXT("DisplayTag2xArrayFmt", "{0} \u00D7 {1} ({2} elements)"), ReformatNumberStringForDisplay(FirstPart), ReformatNumberStringForDisplay(SecondPart), ReformatNumberStringForDisplay(ThirdPart)); } else if (Remainder.FindChar(TEXT('x'), RemainderSeparatorPos)) { // AxBxC form (3D) FString SecondPart = Remainder.Left(RemainderSeparatorPos); FString ThirdPart = Remainder.Mid(RemainderSeparatorPos + 1); bHasSetDisplayValue = true; DisplayValue = FText::Format(LOCTEXT("DisplayTag3xFmt", "{0} \u00D7 {1} \u00D7 {2}"), ReformatNumberStringForDisplay(FirstPart), ReformatNumberStringForDisplay(SecondPart), ReformatNumberStringForDisplay(ThirdPart)); } else { // 2D form by default bHasSetDisplayValue = true; DisplayValue = FText::Format(LOCTEXT("DisplayTag2xFmt", "{0} \u00D7 {1}"), ReformatNumberStringForDisplay(FirstPart), ReformatNumberStringForDisplay(Remainder)); } } else { // No separators, assume 1D bHasSetDisplayValue = true; DisplayValue = ReformatNumberStringForDisplay(AttributeValueStr); } } // Chronological tags need to format the specified timestamp based on the display flags if (!bHasSetDisplayValue && AttributeMetaData.AttributeType == UObject::FAssetRegistryTag::TT_Chronological) { bHasSetDisplayValue = true; FDateTime Timestamp; if (FDateTime::Parse(AttributeValueStr, Timestamp)) { const bool bDisplayDate = !!(AttributeMetaData.DisplayFlags & UObject::FAssetRegistryTag::TD_Date); const bool bDisplayTime = !!(AttributeMetaData.DisplayFlags & UObject::FAssetRegistryTag::TD_Time); const FString TimeZone = (AttributeMetaData.DisplayFlags & UObject::FAssetRegistryTag::TD_InvariantTz) ? FText::GetInvariantTimeZone() : FString(); if (bDisplayDate && bDisplayTime) { DisplayValue = FText::AsDateTime(Timestamp, EDateTimeStyle::Short, EDateTimeStyle::Short, TimeZone); } else if (bDisplayDate) { DisplayValue = FText::AsDate(Timestamp, EDateTimeStyle::Short, TimeZone); } else if (bDisplayTime) { DisplayValue = FText::AsTime(Timestamp, EDateTimeStyle::Short, TimeZone); } } } // The tag value might be localized text, so we need to parse it for display if (!bHasSetDisplayValue && FTextStringHelper::IsComplexText(*AttributeValueStr)) { bHasSetDisplayValue = FTextStringHelper::ReadFromBuffer(*AttributeValueStr, DisplayValue) != nullptr; } // Do our best to build something valid from the string value if (!bHasSetDisplayValue) { bHasSetDisplayValue = true; // Since all we have at this point is a string, we can't be very smart here. // We need to strip some noise off class paths in some cases, but can't load the asset to inspect its UPROPERTYs manually due to performance concerns. FString ValueString = FPackageName::ExportTextPathToObjectPath(AttributeValueStr); const TCHAR StringToRemove[] = TEXT("/Script/"); if (ValueString.StartsWith(StringToRemove)) { // Remove the class path for native classes, and also remove Engine. for engine classes const int32 SizeOfPrefix = UE_ARRAY_COUNT(StringToRemove) - 1; ValueString.MidInline(SizeOfPrefix, ValueString.Len() - SizeOfPrefix, EAllowShrinking::No); ValueString.ReplaceInline(TEXT("Engine."), TEXT("")); } if (ItemAssetData.IsValid()) { if (const UClass* AssetClass = ItemAssetData.GetClass()) { if (const FProperty* TagField = FindFProperty(AssetClass, AttributeName)) { const FProperty* TagProp = nullptr; const UEnum* TagEnum = nullptr; if (const FByteProperty* ByteProp = CastField(TagField)) { TagProp = ByteProp; TagEnum = ByteProp->Enum; } else if (const FEnumProperty* EnumProp = CastField(TagField)) { TagProp = EnumProp; TagEnum = EnumProp->GetEnum(); } // Strip off enum prefixes if they exist if (TagProp) { if (TagEnum) { const FString EnumPrefix = TagEnum->GenerateEnumPrefix(); if (EnumPrefix.Len() && ValueString.StartsWith(EnumPrefix)) { ValueString.RightChopInline(EnumPrefix.Len() + 1, EAllowShrinking::No); // +1 to skip over the underscore } } ValueString = FName::NameToDisplayString(ValueString, false); } } } } DisplayValue = FText::AsCultureInvariant(MoveTemp(ValueString)); } // Add suffix to the value, if one is defined for this tag if (!AttributeMetaData.Suffix.IsEmpty()) { DisplayValue = FText::Format(LOCTEXT("DisplayTagSuffixFmt", "{0} {1}"), DisplayValue, AttributeMetaData.Suffix); } } if (!DisplayValue.IsEmpty()) { CachedDisplayTags.Add(FTagContentBrowserDisplayItem(AttributeName, AttributeMetaData.DisplayName, DisplayValue, AttributeMetaData.bIsImportant)); } } } void FAssetSystemContentBrowserInfoProvider::CacheDirtyExternalPackageInfo() { if (bShouldSaveExternalPackages) { CachedDirtyExternalPackagesList.Empty(); FAssetData AssetData; AssetItem->GetItem().Legacy_TryGetAssetData(AssetData); if (AssetData.IsAssetLoaded()) { if(const UObject* Asset = AssetData.GetAsset()) { if (const UPackage* Package = Asset->GetPackage()) { TArray ExternalPackages = Package->GetExternalPackages(); IAssetRegistry& AssetRegistry = FModuleManager::GetModuleChecked(TEXT("AssetRegistry")).Get(); // Mirrored/copied from SSourceControlCommon.cpp auto RetrieveAssetName = [](const FAssetData& InAssetData) -> FString { static const FName NAME_ActorLabel(TEXT("ActorLabel")); if (InAssetData.FindTag(NAME_ActorLabel)) { FString ResultAssetName; InAssetData.GetTagValue(NAME_ActorLabel, ResultAssetName); return ResultAssetName; } if (InAssetData.FindTag(FPrimaryAssetId::PrimaryAssetDisplayNameTag)) { FString ResultAssetName; InAssetData.GetTagValue(FPrimaryAssetId::PrimaryAssetDisplayNameTag, ResultAssetName); return ResultAssetName; } if (InAssetData.AssetClassPath == UActorFolder::StaticClass()->GetClassPathName()) { FString ActorFolderPath = UActorFolder::GetAssetRegistryInfoFromPackage(InAssetData.PackageName).GetDisplayName(); if (!ActorFolderPath.IsEmpty()) { return ActorFolderPath; } } return InAssetData.AssetName.ToString(); }; for (const UPackage* ExternalPackage : ExternalPackages) { if (ExternalPackage->IsDirty()) { TArray DirtyAssetDataEntries; AssetRegistry.GetAssetsByPackageName(*ExternalPackage->GetName(), DirtyAssetDataEntries); if (CachedDirtyExternalPackagesList.Len()) { CachedDirtyExternalPackagesList.Append("\n"); } CachedDirtyExternalPackagesList.Append(ExternalPackage->GetPathName()); for (const FAssetData& DirtyAssetData : DirtyAssetDataEntries) { const FString AssetName = RetrieveAssetName(DirtyAssetData); const FString AssetClass = DirtyAssetData.AssetClassPath.GetAssetName().ToString(); CachedDirtyExternalPackagesList.Append("\n\t"); CachedDirtyExternalPackagesList.Append(FString::Printf(TEXT("%s (%s)"), *AssetName, *AssetClass)); } } } } } } } } FText FAssetSystemContentBrowserInfoProvider::GetExternalPackagesText() const { if (CachedDirtyExternalPackagesList.Len()) { return FText::FromString(CachedDirtyExternalPackagesList); } return FText::GetEmpty(); } FText FAssetSystemContentBrowserInfoProvider::GetAssetUserDescription() const { if (AssetItem && AssetItem->IsFile()) { FContentBrowserItemDataAttributeValue DescriptionAttributeValue = AssetItem->GetItem().GetItemAttribute(ContentBrowserItemAttributes::ItemDescription); if (DescriptionAttributeValue.IsValid()) { return DescriptionAttributeValue.GetValue(); } } return FText::GetEmpty(); } #undef LOCTEXT_NAMESPACE