// Portage UE 5.5 - voir LPFixDeprecatedNodesCommandlet.h #include "LPFixDeprecatedNodesCommandlet.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Components/SkeletalMeshComponent.h" #include "Components/SkinnedMeshComponent.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphPin.h" #include "EdGraphSchema_K2.h" #include "Engine/Blueprint.h" #include "K2Node_CallFunction.h" #include "K2Node_VariableGet.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/KismetEditorUtilities.h" #include "LPNavigationHelpers.h" #include "Misc/PackageName.h" #include "NavigationSystem.h" #include "UObject/Package.h" #include "UObject/SavePackage.h" #include "UObject/UObjectGlobals.h" DEFINE_LOG_CATEGORY_STATIC(LogLPFixDeprecated, Log, All); namespace { // --- R-03 --- const FName SkeletalMeshPropertyName(TEXT("SkeletalMesh")); const FName DeprecatedGetterName(TEXT("GetSkeletalMesh_DEPRECATED")); const FName ReplacementGetterName(TEXT("GetSkeletalMeshAsset")); // --- R-04 --- const FName DeprecatedNavFunctionName(TEXT("K2_GetRandomPointInNavigableRadius")); const FName ReplacementNavFunctionName(TEXT("GetRandomLocationInNavigableRadius")); /** Decrit une broche et ses liaisons, pour le rapport. Copie du motif de LPFixEditableTextBoxFont. */ FString DescribeLinks(const UEdGraphPin* Pin) { if (!Pin) { return TEXT("(broche absente)"); } if (Pin->LinkedTo.Num() == 0) { return FString::Printf(TEXT("non connectee (defaut: '%s')"), *Pin->DefaultValue); } TArray Targets; for (const UEdGraphPin* Linked : Pin->LinkedTo) { Targets.Add(FString::Printf(TEXT("%s.%s"), Linked->GetOwningNode() ? *Linked->GetOwningNode()->GetName() : TEXT("?"), *Linked->PinName.ToString())); } return FString::Printf(TEXT("%d liaison(s) -> %s"), Pin->LinkedTo.Num(), *FString::Join(Targets, TEXT(", "))); } /** Vrai si la classe proprietaire du membre est USkinnedMeshComponent ou une de ses filles. */ bool IsSkinnedMeshOwner(const UClass* OwnerClass) { return OwnerClass && OwnerClass->IsChildOf(USkinnedMeshComponent::StaticClass()); } } ULPFixDeprecatedNodesCommandlet::ULPFixDeprecatedNodesCommandlet() { IsClient = false; IsServer = false; IsEditor = true; LogToConsole = true; } int32 ULPFixDeprecatedNodesCommandlet::Main(const FString& Params) { const bool bDryRun = Params.Contains(TEXT("DryRun"), ESearchCase::IgnoreCase); UFunction* ReplacementGetter = USkeletalMeshComponent::StaticClass()->FindFunctionByName(ReplacementGetterName); UFunction* ReplacementNavFunction = ULPNavigationHelpers::StaticClass()->FindFunctionByName(ReplacementNavFunctionName); if (!ReplacementGetter || !ReplacementNavFunction) { UE_LOG(LogLPFixDeprecated, Error, TEXT("Fonctions de remplacement introuvables (GetSkeletalMeshAsset=%s, GetRandomLocationInNavigableRadius=%s)."), ReplacementGetter ? TEXT("ok") : TEXT("NULL"), ReplacementNavFunction ? TEXT("ok") : TEXT("NULL")); return 1; } UE_LOG(LogLPFixDeprecated, Warning, TEXT("=== Noeuds deprecies : SkeletalMesh (R-03) + GetRandomPointInNavigableRadius (R-04)%s ==="), bDryRun ? TEXT(" [DRY RUN]") : TEXT("")); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); AssetRegistryModule.Get().SearchAllAssets(true); TArray BlueprintAssets; // bSearchSubClasses = true : couvre aussi les UWidgetBlueprint (CharGen, Inventory, ...). AssetRegistryModule.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), BlueprintAssets, true); int32 MeshNodesFound = 0; int32 NavNodesFound = 0; int32 MeshNodesFixed = 0; int32 NavNodesFixed = 0; int32 BlueprintsModified = 0; int32 SaveFailures = 0; int32 Unhandled = 0; const UEdGraphSchema_K2* Schema = GetDefault(); for (const FAssetData& AssetData : BlueprintAssets) { if (!AssetData.PackageName.ToString().StartsWith(TEXT("/Game/"))) { continue; } UBlueprint* Blueprint = Cast(AssetData.GetAsset()); if (!Blueprint) { continue; } TArray Graphs; Blueprint->GetAllGraphs(Graphs); int32 FixedHere = 0; for (UEdGraph* Graph : Graphs) { if (!Graph) { continue; } // Snapshot : on cree et on detruit des noeuds pendant l'iteration. TArray Snapshot = Graph->Nodes; for (UEdGraphNode* GraphNode : Snapshot) { // --------------------------------------------------------------- // R-04 : reference de fonction a reaiguiller, rien d'autre a faire. // Signatures et noms de broches identiques -> ReconstructNode conserve // toutes les liaisons. // --------------------------------------------------------------- if (UK2Node_CallFunction* CallNode = Cast(GraphNode)) { const FName MemberName = CallNode->FunctionReference.GetMemberName(); const UClass* MemberOwner = CallNode->FunctionReference.GetMemberParentClass(); if (MemberName == DeprecatedNavFunctionName) { ++NavNodesFound; UE_LOG(LogLPFixDeprecated, Warning, TEXT(" [R-04] %s :: %s -> %s"), *Blueprint->GetName(), *Graph->GetName(), *GraphNode->GetName()); UE_LOG(LogLPFixDeprecated, Warning, TEXT(" RandomLocation : %s"), *DescribeLinks(GraphNode->FindPin(TEXT("RandomLocation")))); if (!bDryRun) { GraphNode->Modify(); CallNode->SetFromFunction(ReplacementNavFunction); CallNode->ReconstructNode(); ++NavNodesFixed; ++FixedHere; } continue; } // R-03, variante « appel explicite du getter deprecie ». if (MemberName == DeprecatedGetterName && IsSkinnedMeshOwner(MemberOwner)) { ++MeshNodesFound; UE_LOG(LogLPFixDeprecated, Warning, TEXT(" [R-03/appel] %s :: %s -> %s"), *Blueprint->GetName(), *Graph->GetName(), *GraphNode->GetName()); if (!bDryRun) { GraphNode->Modify(); CallNode->SetFromFunction(ReplacementGetter); CallNode->ReconstructNode(); ++MeshNodesFixed; ++FixedHere; } continue; } } // --------------------------------------------------------------- // R-03 : acces direct a la propriete depreciee. Il faut remplacer le // noeud, pas seulement sa reference : un K2Node_VariableGet ne peut pas // devenir un K2Node_CallFunction. // --------------------------------------------------------------- UK2Node_VariableGet* VarGet = Cast(GraphNode); if (!VarGet || VarGet->VariableReference.GetMemberName() != SkeletalMeshPropertyName) { continue; } if (!IsSkinnedMeshOwner(VarGet->VariableReference.GetMemberParentClass())) { continue; } UEdGraphPin* OldSelfPin = VarGet->FindPin(UEdGraphSchema_K2::PN_Self, EGPD_Input); UEdGraphPin* OldValuePin = VarGet->GetValuePin(); ++MeshNodesFound; UE_LOG(LogLPFixDeprecated, Warning, TEXT(" [R-03/propriete] %s :: %s -> %s"), *Blueprint->GetName(), *Graph->GetName(), *GraphNode->GetName()); UE_LOG(LogLPFixDeprecated, Warning, TEXT(" Target : %s"), *DescribeLinks(OldSelfPin)); UE_LOG(LogLPFixDeprecated, Warning, TEXT(" Valeur : %s"), *DescribeLinks(OldValuePin)); if (bDryRun) { continue; } if (!OldValuePin) { ++Unhandled; UE_LOG(LogLPFixDeprecated, Error, TEXT(" broche de valeur introuvable, noeud laisse en l'etat.")); continue; } // Construction du noeud de remplacement au meme emplacement. UK2Node_CallFunction* NewNode = NewObject(Graph); NewNode->SetFromFunction(ReplacementGetter); NewNode->NodePosX = VarGet->NodePosX; NewNode->NodePosY = VarGet->NodePosY; NewNode->CreateNewGuid(); NewNode->PostPlacedNewNode(); NewNode->AllocateDefaultPins(); Graph->Modify(); Graph->AddNode(NewNode, false, false); UEdGraphPin* NewSelfPin = NewNode->FindPin(UEdGraphSchema_K2::PN_Self, EGPD_Input); UEdGraphPin* NewValuePin = NewNode->GetReturnValuePin(); bool bMoved = true; // GetSkeletalMeshAsset() n'existe que sur USkeletalMeshComponent alors que la // propriete depreciee vit sur USkinnedMeshComponent. Si la cible du noeud est // typee sur la classe de base, le deplacement echoue : on le signale au lieu de // casser le graphe. if (OldSelfPin && NewSelfPin) { const FPinConnectionResponse Response = Schema->MovePinLinks(*OldSelfPin, *NewSelfPin); if (Response.Response == CONNECT_RESPONSE_DISALLOW) { bMoved = false; UE_LOG(LogLPFixDeprecated, Error, TEXT(" Target incompatible (%s) : reprise manuelle necessaire."), *Response.Message.ToString()); } } if (bMoved && NewValuePin) { const FPinConnectionResponse Response = Schema->MovePinLinks(*OldValuePin, *NewValuePin); if (Response.Response == CONNECT_RESPONSE_DISALLOW) { bMoved = false; UE_LOG(LogLPFixDeprecated, Error, TEXT(" Valeur incompatible (%s) : reprise manuelle necessaire."), *Response.Message.ToString()); } } if (!bMoved) { // On remet tout en place : le noeud d'origine garde ses liaisons. ++Unhandled; NewNode->DestroyNode(); continue; } VarGet->DestroyNode(); ++MeshNodesFixed; ++FixedHere; } } if (FixedHere > 0 && !bDryRun) { ++BlueprintsModified; FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); FKismetEditorUtilities::CompileBlueprint(Blueprint); UPackage* Package = Blueprint->GetOutermost(); FString Filename; if (!FPackageName::TryConvertLongPackageNameToFilename( Package->GetName(), Filename, FPackageName::GetAssetPackageExtension())) { ++SaveFailures; UE_LOG(LogLPFixDeprecated, Error, TEXT(" chemin introuvable pour %s"), *Package->GetName()); continue; } ResetLoaders(Package); FSavePackageArgs SaveArgs; SaveArgs.TopLevelFlags = RF_Public | RF_Standalone; SaveArgs.SaveFlags = SAVE_None; SaveArgs.bWarnOfLongFilename = true; // GWarn et non GError : en commandlet, GError rend toute erreur de save fatale. SaveArgs.Error = GWarn; if (UPackage::SavePackage(Package, nullptr, *Filename, SaveArgs)) { UE_LOG(LogLPFixDeprecated, Warning, TEXT(" -> %s sauvegarde (%d noeud(s))."), *Package->GetName(), FixedHere); } else { ++SaveFailures; UE_LOG(LogLPFixDeprecated, Error, TEXT(" -> ECHEC de sauvegarde de %s"), *Package->GetName()); } } } UE_LOG(LogLPFixDeprecated, Warning, TEXT("=== Termine : R-03 %d trouve(s) / %d corrige(s), R-04 %d trouve(s) / %d corrige(s), %d Blueprint(s) modifie(s), %d cas non traite(s), %d echec(s) de sauvegarde. ==="), MeshNodesFound, MeshNodesFixed, NavNodesFound, NavNodesFixed, BlueprintsModified, Unhandled, SaveFailures); return (SaveFailures > 0 || Unhandled > 0) ? 1 : 0; }