// Copyright Epic Games, Inc. All Rights Reserved. #include "AnimationModifier.h" #include "AnimationModifiersAssetUserData.h" #include "AnimationModifierHelpers.h" #include "Algo/Transform.h" #include "Animation/AnimData/IAnimationDataController.h" #include "Animation/AnimSequence.h" #include "Animation/Skeleton.h" #include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/IAssetRegistry.h" #include "AssetViewUtils.h" #include "Containers/Array.h" #include "Containers/UnrealString.h" #include "CoreGlobals.h" #include "Editor/Transactor.h" #include "HAL/PlatformCrt.h" #include "Internationalization/Internationalization.h" #include "Internationalization/Text.h" #include "Misc/MessageDialog.h" #include "Misc/OutputDeviceRedirector.h" #include "ModifierOutputFilter.h" #include "Modules/ModuleManager.h" #include "ScopedTransaction.h" #include "Serialization/Archive.h" #include "Templates/Casts.h" #include "UObject/Class.h" #include "UObject/NameTypes.h" #include "UObject/ObjectKey.h" #include "UObject/Package.h" #include "UObject/ReleaseObjectVersion.h" #include "UObject/Script.h" #include "UObject/UObjectIterator.h" #define LOCTEXT_NAMESPACE "AnimationModifier" int32 UE::Anim::FApplyModifiersScope::ScopesOpened = 0; TMap> UE::Anim::FApplyModifiersScope::PerClassReturnTypeValues; const FName UAnimationModifier::AnimationModifiersTag = TEXT("AnimationModifierList"); TOptional UE::Anim::FApplyModifiersScope::GetReturnType(const UAnimationModifier* InModifier) { TOptional* ReturnTypePtr = PerClassReturnTypeValues.Find(FObjectKey(InModifier->GetClass())); return ReturnTypePtr ? *ReturnTypePtr : TOptional(); } void UE::Anim::FApplyModifiersScope::SetReturnType(const UAnimationModifier* InModifier, EAppReturnType::Type InReturnType) { const FObjectKey Key(InModifier->GetClass()); ensure(!PerClassReturnTypeValues.Contains(Key)); PerClassReturnTypeValues.Add(Key, InReturnType); } UAnimationModifier::UAnimationModifier() { } void UAnimationModifier::ApplyToAnimationSequence(class UAnimSequence* AnimSequence) const { FEditorScriptExecutionGuard ScriptGuard; checkf(AnimSequence, TEXT("Invalid Animation Sequence supplied")); USkeleton* Skeleton = AnimSequence->GetSkeleton(); checkf(Skeleton, TEXT("Invalid Skeleton for supplied Animation Sequence")); // Filter to check for warnings / errors thrown from animation blueprint library (rudimentary approach for now) FCategoryLogOutputFilter OutputLog; OutputLog.SetAutoEmitLineTerminator(true); OutputLog.AddCategoryName("LogAnimationBlueprintLibrary"); OutputLog.AddCategoryName("LogAnimation"); GLog->AddOutputDevice(&OutputLog); bool AppliedModifierOuterWasCreated = false; UObject* AppliedModifierOuter = AnimSequence->GetAssetUserData(); if (!AppliedModifierOuter) { AppliedModifierOuterWasCreated = true; AppliedModifierOuter = FAnimationModifierHelpers::RetrieveOrCreateModifierUserData(AnimSequence); } // Using explicit name may cause duplicate path name UAnimationModifier* AppliedModifier = DuplicateObject(this, AppliedModifierOuter); // Set the revision guid on applied modifier to latest AppliedModifier->RevisionGuid = GetLatestRevisionGuid(); FTransaction AnimationDataTransaction; AnimationDataTransaction.SaveObject(AnimSequence); AnimationDataTransaction.SaveObject(Skeleton); { // Group the OnRevert & OnApply operation into one Bracket to reduce compression request IAnimationDataController::FScopedBracket ScopedBracket(AnimSequence->GetController(), LOCTEXT("ApplyModifierBracket", "Applying Animation Modifier")); /** In case this modifier has been previously applied, revert it using the serialised out version at the time */ if (UAnimationModifier* PreviouslyAppliedModifier = GetAppliedModifier(AnimSequence)) { // Save the applied modifier as well AnimationDataTransaction.SaveObject(PreviouslyAppliedModifier); PreviouslyAppliedModifier->ExecuteOnRevert(AnimSequence); } { /** Reverting and applying, populates the log with possible warnings and or errors to notify the user about */ AppliedModifier->ExecuteOnApply(AnimSequence); } } GLog->RemoveOutputDevice(&OutputLog); // Check if warnings or errors have occurred and show dialog to user to inform them about this const bool bWarnings = OutputLog.ContainsWarnings(); const bool bErrors = OutputLog.ContainsErrors(); bool bShouldRevert = bErrors; // If errors have occured - prompt user with OK and revert TOptional ScopeReturnType = UE::Anim::FApplyModifiersScope::GetReturnType(this); static const FText MessageTitle = LOCTEXT("ModifierDialogTitle", "Modifier has generated errors during test run."); if (bErrors) { if (UE::Anim::FApplyModifiersScope::ScopesOpened == 0 || !ScopeReturnType.IsSet() || ScopeReturnType.GetValue() != EAppReturnType::Type::Ok) { const FText ErrorMessageFormat = FText::FormatOrdered(LOCTEXT("ModifierErrorDescription", "Modifier: {0}\nAsset: {1}\n{2}\nResolve the errors before trying to apply again."), FText::FromString(GetClass()->GetPathName()), FText::FromString(AnimSequence->GetPathName()), FText::FromString(OutputLog)); EAppReturnType::Type ReturnValue = FMessageDialog::Open(EAppMsgType::Ok, ErrorMessageFormat, &MessageTitle); UE::Anim::FApplyModifiersScope::SetReturnType(this, ReturnValue); } } // If _only_ warnings have occured - check if user has previously said YesAll / NoAll and process the result if (bWarnings && !bShouldRevert) { if (UE::Anim::FApplyModifiersScope::ScopesOpened == 0 || !ScopeReturnType.IsSet()) { const FText WarningMessage = FText::FormatOrdered(LOCTEXT("ModifierWarningDescription", "Modifier: {0}\nAsset: {1}\n{2}\nAre you sure you want to apply it?"), FText::FromString(GetClass()->GetPathName()), FText::FromString(AnimSequence->GetPathName()), FText::FromString(OutputLog)); EAppReturnType::Type ReturnValue = FMessageDialog::Open(EAppMsgType::YesNoYesAllNoAll, WarningMessage, &MessageTitle); bShouldRevert = ReturnValue == EAppReturnType::No || ReturnValue == EAppReturnType::NoAll; // check if user response should be stored for further modifier applications if(UE::Anim::FApplyModifiersScope::ScopesOpened > 0) { if (ReturnValue == EAppReturnType::Type::YesAll || ReturnValue == EAppReturnType::Type::NoAll) { UE::Anim::FApplyModifiersScope::SetReturnType(this, ReturnValue); } } } else { // Revert if previous user prompt return NoAll or if any errors occured previously bShouldRevert = ScopeReturnType.GetValue() == EAppReturnType::NoAll || ScopeReturnType.GetValue() == EAppReturnType::Ok; } } // Revert changes if necessary, otherwise post edit and refresh animation data if (bShouldRevert) { AnimationDataTransaction.BeginOperation(); AnimationDataTransaction.Apply(); AnimationDataTransaction.EndOperation(); AnimSequence->RefreshCacheData(); AppliedModifier->MarkAsGarbage(); if (AppliedModifierOuterWasCreated) { // The animation sequence's asset user data array should have been reverted by the transaction AppliedModifierOuter->MarkAsGarbage(); } } else { SetAppliedModifier(AnimSequence, AppliedModifier); if (Skeleton->GetPackage()->IsDirty()) { Skeleton->PostEditChange(); } if (AnimSequence->GetPackage()->IsDirty()) { AnimSequence->PostEditChange(); AnimSequence->RefreshCacheData(); } } } void UAnimationModifier::UpdateCompressedAnimationData() { if (CurrentAnimSequence->DoesNeedRecompress()) { CurrentAnimSequence->CacheDerivedDataForCurrentPlatform(); } } void UAnimationModifier::RevertFromAnimationSequence(class UAnimSequence* AnimSequence) const { FEditorScriptExecutionGuard ScriptGuard; checkf(AnimSequence, TEXT("Invalid Animation Sequence supplied")); USkeleton* Skeleton = AnimSequence->GetSkeleton(); checkf(Skeleton, TEXT("Invalid Skeleton for supplied Animation Sequence")); /** Can only revert if previously applied, which means there should be a previous modifier */ UAnimationModifier* PreviouslyAppliedModifier = FindAndRemoveAppliedModifier(AnimSequence); // Backward compatibility // Is PreviouslyAppliedModifier owned by InAnimationSequence // Modifier on Skeleton read from legacy version may _share_ the PreviouslyAppliedModifier bool AppliedModifierOwnedByAnimation = true; if (!PreviouslyAppliedModifier) { PreviouslyAppliedModifier = GetLegacyPreviouslyAppliedModifierForModifierOnSkeleton(); if (PreviouslyAppliedModifier) { AppliedModifierOwnedByAnimation = false; } } if (PreviouslyAppliedModifier) { // Transact the modifier to prevent instance variables/data to change during reverting { IAnimationDataController::FScopedBracket ScopedBracket(AnimSequence->GetController(), LOCTEXT("ApplyModifierBracket", "Applying Animation Modifier")); PreviouslyAppliedModifier->ExecuteOnRevert(AnimSequence); } if (AppliedModifierOwnedByAnimation) { PreviouslyAppliedModifier->MarkAsGarbage(); } if (Skeleton->GetPackage()->IsDirty()) { Skeleton->PostEditChange(); } if (AnimSequence->GetPackage()->IsDirty()) { AnimSequence->PostEditChange(); AnimSequence->RefreshCacheData(); } } } bool UAnimationModifier::CanRevert(IInterface_AssetUserData* AssetUserDataInterface) const { return GetAppliedModifier(AssetUserDataInterface) != nullptr; } UAnimationModifier* UAnimationModifier::GetAppliedModifier(IInterface_AssetUserData* AssetUserDataInterface) const { if (UAnimationModifiersAssetUserData* AssetData = AssetUserDataInterface->GetAssetUserData()) { TObjectPtr* AppliedModifier = AssetData->AppliedModifiers.Find(this); if (AppliedModifier) { return AppliedModifier->Get(); } } // Backward compatibility // Read the shared applied instance for modifier on skeleton ready from legacy version if (UAnimationModifier* LegacyAppliedModifierOnSkeleton = GetLegacyPreviouslyAppliedModifierForModifierOnSkeleton()) { return LegacyAppliedModifierOnSkeleton; } return nullptr; } UAnimationModifier* UAnimationModifier::FindAndRemoveAppliedModifier(TScriptInterface AssetUserDataInterface) const { if (UAnimationModifiersAssetUserData* AssetData = AssetUserDataInterface->GetAssetUserData()) { TObjectPtr AppliedModifier = nullptr; AssetData->AppliedModifiers.RemoveAndCopyValue(this, AppliedModifier); if (AppliedModifier) { AssetData->Modify(); AssetUserDataInterface.GetObject()->Modify(); } return AppliedModifier.Get(); } return nullptr; } void UAnimationModifier::SetAppliedModifier(TScriptInterface AssetUserDataInterface, UAnimationModifier* AppliedModifier) const { if (UAnimationModifiersAssetUserData* AssetData = FAnimationModifierHelpers::RetrieveOrCreateModifierUserData(AssetUserDataInterface)) { TObjectPtr& Modifier = AssetData->AppliedModifiers.FindOrAdd(this); if (Modifier) { Modifier->MarkAsGarbage(); } Modifier = AppliedModifier; AssetData->Modify(); AssetUserDataInterface.GetObject()->Modify(); } } bool UAnimationModifier::IsLatestRevisionApplied(IInterface_AssetUserData* AssetUserDataInterface) const { if (UAnimationModifier* AppliedModifier = GetAppliedModifier(AssetUserDataInterface)) { return AppliedModifier->RevisionGuid == GetLatestRevisionGuid(); } return false; } UAnimationModifier* UAnimationModifier::GetLegacyPreviouslyAppliedModifierForModifierOnSkeleton() const { // Read PreviouslyAppliedModifier_DEPRECATED from previous version // Check PostLoad() when we moved the deprecated value into this place if (UAnimationModifiersAssetUserData* AssetData = Cast(GetOuter())) { if (USkeleton* Skeleton = Cast(AssetData->GetOuter())) { if (TObjectPtr* PtrToModifier = AssetData->AppliedModifiers.Find(this)) { // The new system uses a _placeholder_ object with _exact_ UAnimationAsset class to store RevisionGuid // Any object with concrete modifier class must come from PreviouslyAppliedModifier_DEPRECATED if (PtrToModifier->GetClass() != UAnimationAsset::StaticClass()) { return PtrToModifier->Get(); } } } } return nullptr; } FGuid UAnimationModifier::GetLatestRevisionGuid() const { return GetDefault(GetClass())->RevisionGuid; } void UAnimationModifier::GetAssetRegistryTagsForAppliedModifiersFromSkeleton(const UObject* Object, TArray& OutTags) { FString ModifiersList; if (UAnimSequence* AnimSequence = const_cast(Cast(Object))) { if (USkeleton* Skeleton = AnimSequence->GetSkeleton()) { if (UAnimationModifiersAssetUserData* SkeletonAssetData = Skeleton->GetAssetUserData()) { for (const UAnimationModifier* Modifier : SkeletonAssetData->GetAnimationModifierInstances()) { if (const UAnimationModifier* AppliedModifier = Modifier->GetAppliedModifier(AnimSequence)) { // "{Name}={Revision};" FString Revision; AppliedModifier->RevisionGuid.AppendString(Revision, EGuidFormats::Short); ModifiersList += FString::Printf(TEXT("%s%c%s%c"), *Modifier->GetName(), AnimationModifiersAssignment, *Revision, AnimationModifiersDelimiter); } } if (!ModifiersList.IsEmpty()) { OutTags.Emplace(AnimationModifiersTag, ModifiersList, UObject::FAssetRegistryTag::TT_Hidden); } } } } } //! @brief Mark this modifier on skeleton as reverted (affect CanRevert, IsLatestRevisionApplied) void UAnimationModifier::RemoveLegacyPreviousAppliedModifierOnSkeleton(USkeleton* Skeleton) { if (UAnimationModifier* AppliedModifier = FindAndRemoveAppliedModifier(Skeleton)) { AppliedModifier->MarkAsGarbage(); } } void UAnimationModifier::PostInitProperties() { Super::PostInitProperties(); UpdateNativeRevisionGuid(); } void UAnimationModifier::Serialize(FArchive& Ar) { Super::Serialize(Ar); Ar.UsingCustomVersion(FReleaseObjectVersion::GUID); /** Back-wards compatibility, assume the current modifier as previously applied */ if (Ar.CustomVer(FReleaseObjectVersion::GUID) < FReleaseObjectVersion::SerializeAnimModifierState) { UObject* AssetData = GetOuter(); UObject* AnimationOrSkeleton = AssetData ? AssetData->GetOuter() : nullptr; if (AnimationOrSkeleton) { SetAppliedModifier(AnimationOrSkeleton, DuplicateObject(this, GetOuter())); } Modify(); } } void UAnimationModifier::PostLoad() { Super::PostLoad(); UClass* Class = GetClass(); UObject* DefaultObject = Class->GetDefaultObject(); // CDO, set GUID if invalid if(DefaultObject == this) { // Ensure we always have a valid guid if (!RevisionGuid.IsValid()) { UpdateRevisionGuid(GetClass()); MarkPackageDirty(); } } if (PreviouslyAppliedModifier_DEPRECATED) { // The applied guid is now stored at the revision guid of applied modifier PreviouslyAppliedModifier_DEPRECATED->RevisionGuid = this->AppliedGuid_DEPRECATED; if (UAnimationModifiersAssetUserData* AssetData = Cast(GetOuter())) { if (UAnimSequence* AnimSequence = Cast(AssetData->GetOuter())) { UE_LOG(LogAnimation, Log, TEXT("Upgrading Applied Animation Modifier on Sequence %s."), *AssetData->GetOuter()->GetName()); UAnimationModifier* AppliedModifier = PreviouslyAppliedModifier_DEPRECATED; PreviouslyAppliedModifier_DEPRECATED = nullptr; SetAppliedModifier(AnimSequence, AppliedModifier); } else if (USkeleton* Skeleton = Cast(AssetData->GetOuter())) { UE_LOG(LogAnimation, Warning, TEXT("Upgrading Applied Animation Modifier %s on Skeleton %s, please reapply the modifier."), *GetName(), *AssetData->GetOuter()->GetName()); UAnimationModifier* AppliedModifier = PreviouslyAppliedModifier_DEPRECATED; PreviouslyAppliedModifier_DEPRECATED = nullptr; SetAppliedModifier(Skeleton, AppliedModifier); } else { UE_LOG(LogAnimation, Error, TEXT("Upgrading Applied Animation Modifier on Unknown type of asset %s, Ignored."), *AssetData->GetOuter()->GetName()); PreviouslyAppliedModifier_DEPRECATED->MarkAsGarbage(); PreviouslyAppliedModifier_DEPRECATED = nullptr; } } else { UE_LOG(LogAnimation, Error, TEXT("Upgrading Applied Animation Modifier on Unknown asset, Ignored.")); PreviouslyAppliedModifier_DEPRECATED->MarkAsGarbage(); PreviouslyAppliedModifier_DEPRECATED = nullptr; } this->Modify(); } } const USkeleton* UAnimationModifier::GetSkeleton() { return CurrentSkeleton; } void UAnimationModifier::UpdateRevisionGuid(UClass* ModifierClass) { if (ModifierClass) { RevisionGuid = FGuid::NewGuid(); } } void UAnimationModifier::UpdateNativeRevisionGuid() { UClass* Class = GetClass(); // Check if this is the class default object if (this == GetDefault(Class)) { // If so check whether or not the config stored revision matches the natively defined one if (StoredNativeRevision != GetNativeClassRevision()) { // If not update the blueprint revision GUID UpdateRevisionGuid(Class); StoredNativeRevision = GetNativeClassRevision(); MarkPackageDirty(); // Save the new native revision to config files SaveConfig(); TryUpdateDefaultConfigFile(); } } } void UAnimationModifier::ExecuteOnRevert(UAnimSequence* InAnimSequence) { CurrentAnimSequence = InAnimSequence; CurrentSkeleton = InAnimSequence->GetSkeleton(); OnRevert(InAnimSequence); CurrentAnimSequence = nullptr; CurrentSkeleton = nullptr; } void UAnimationModifier::ExecuteOnApply(UAnimSequence* InAnimSequence) { CurrentAnimSequence = InAnimSequence; CurrentSkeleton = InAnimSequence->GetSkeleton(); OnApply(InAnimSequence); CurrentAnimSequence = nullptr; CurrentSkeleton = nullptr; } void UAnimationModifier::ApplyToAll(TSubclassOf ModifierSubClass, bool bForceApply /*= true*/) { if (UClass* ModifierClass = ModifierSubClass.Get()) { // Make sure all packages (in this case UAnimSequences) are loaded to ensure the TObjectIterator has any instances to iterate over LoadModifierReferencers(ModifierSubClass); const FScopedTransaction Transaction(LOCTEXT("UndoAction_ApplyModifiers", "Applying Animation Modifier to Animation Sequence(s)")); for (UAnimationModifiersAssetUserData* AssetUserData : TObjectRange{}) { UAnimSequence* AnimSequence = Cast(AssetUserData->GetOuter()); USkeleton* Skeleton = Cast(AssetUserData->GetOuter()); ensure(AnimSequence || Skeleton); for (UAnimationModifier* Modifier : AssetUserData->AnimationModifierInstances) { if (Modifier->GetClass() == ModifierClass && IsValidChecked(Modifier)) { if (AnimSequence) { if (bForceApply || !Modifier->IsLatestRevisionApplied(AnimSequence)) { Modifier->ApplyToAnimationSequence(AnimSequence); } } else if (Skeleton) { // TODO : Modifier on Skeleton // We can apply the modifier to all animation sequences referencing this skeleton // Save this behavior change for another CL UE_LOG(LogAnimation, Warning, TEXT("Animation Modifier %s on Skeleton %s was skipped, please manually apply it from Skeleton"), *ModifierClass->GetName(), *Skeleton->GetName()); } } } } } } void UAnimationModifier::LoadModifierReferencers(TSubclassOf ModifierSubClass) { if (UClass* ModifierClass = ModifierSubClass.Get()) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); TArray PackageDependencies; AssetRegistryModule.GetRegistry().GetReferencers(ModifierClass->GetPackage()->GetFName(), PackageDependencies); TArray PackageNames; Algo::Transform(PackageDependencies, PackageNames, [](FName Name) { return Name.ToString(); }); TArray Packages = AssetViewUtils::LoadPackages(PackageNames); } } int32 UAnimationModifier::GetNativeClassRevision() const { // Overriden in derrived classes to perform native revisioning return 0; } const UAnimSequence* UAnimationModifier::GetAnimationSequence() { return CurrentAnimSequence; } #undef LOCTEXT_NAMESPACE