diff --git a/Source/LifePlay/LPMeshCompat.cpp b/Source/LifePlay/LPMeshCompat.cpp new file mode 100644 index 0000000..96e0d27 --- /dev/null +++ b/Source/LifePlay/LPMeshCompat.cpp @@ -0,0 +1,19 @@ +// Portage UE 5.5 - voir LPMeshCompat.h + +#include "LPMeshCompat.h" + +#include "Components/SkinnedMeshComponent.h" +#include "Engine/SkeletalMesh.h" +#include "LifePlay.h" + +USkeletalMesh* ULPMeshCompat::GetSkeletalMesh(const USkinnedMeshComponent* Component) +{ + if (!Component) + { + // Le noeud d'origine emettait "Accessed None trying to read property SkeletalMesh" ; + // la trace reste disponible en Verbose pour les mesures A/B. + UE_LOG(LogLifePlay, Verbose, TEXT("GetSkeletalMesh : composant None")); + return nullptr; + } + return Cast(Component->GetSkinnedAsset()); +} diff --git a/Source/LifePlay/LPMeshCompat.h b/Source/LifePlay/LPMeshCompat.h new file mode 100644 index 0000000..92ad9f5 --- /dev/null +++ b/Source/LifePlay/LPMeshCompat.h @@ -0,0 +1,32 @@ +// Portage UE 5.5 - lot L7a (R-03, ERR-012) : remplacement des noeuds "Get SkeletalMesh" deprecies. +// +// En UE 5.1+, la propriete USkinnedMeshComponent::SkeletalMesh est depreciee (remplacee par +// SkinnedAsset). Les Blueprints 4.27 la lisent encore : chaque lecture passe par son getter +// GetSkeletalMesh_DEPRECATED(), qui renvoie Cast(GetSkinnedAsset()). +// +// Le premier essai (CX-B05) rebranchait ces lectures sur USkeletalMeshComponent::GetSkeletalMeshAsset(). +// Cette fonction n'existe que sur USkeletalMeshComponent : sur les composants proxy de LifePlay, +// qui sont des USkinnedMeshComponent, le cast echouait et la lecture renvoyait None (ERR-002 aggrave). +// +// ULPMeshCompat::GetSkeletalMesh reproduit exactement le getter du moteur, pour tout +// USkinnedMeshComponent, sans passer par une API depreciee. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "LPMeshCompat.generated.h" + +class USkeletalMesh; +class USkinnedMeshComponent; + +UCLASS() +class LIFEPLAY_API ULPMeshCompat : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** Maillage squelettique du composant : Cast(GetSkinnedAsset()), None si Component est None. */ + UFUNCTION(BlueprintPure, Category = "Components|SkinnedMesh", meta = (DisplayName = "Get Skeletal Mesh (UE4)")) + static USkeletalMesh* GetSkeletalMesh(const USkinnedMeshComponent* Component); +}; diff --git a/Source/LifePlayEditorTools/LPFixSkeletalMeshGetCommandlet.cpp b/Source/LifePlayEditorTools/LPFixSkeletalMeshGetCommandlet.cpp new file mode 100644 index 0000000..b9e5229 --- /dev/null +++ b/Source/LifePlayEditorTools/LPFixSkeletalMeshGetCommandlet.cpp @@ -0,0 +1,253 @@ +// Portage UE 5.5 - voir LPFixSkeletalMeshGetCommandlet.h + +#include "LPFixSkeletalMeshGetCommandlet.h" + +#include "AssetRegistry/AssetData.h" +#include "AssetRegistry/AssetRegistryModule.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 "LPMeshCompat.h" +#include "Misc/PackageName.h" +#include "UObject/Package.h" +#include "UObject/SavePackage.h" + +DEFINE_LOG_CATEGORY_STATIC(LogLPFixSkelMesh, Log, All); + +namespace +{ + const FName SkeletalMeshPropertyName(TEXT("SkeletalMesh")); + const FName NewFunctionName(TEXT("GetSkeletalMesh")); + const FName ComponentPinName(TEXT("Component")); + + /** Extremites reliees a une broche, triees : "Noeud.Broche". */ + TArray Links(const UEdGraphPin* Pin) + { + TArray Out; + if (Pin) + { + for (const UEdGraphPin* L : Pin->LinkedTo) + { + Out.Add(FString::Printf(TEXT("%s.%s"), + L && L->GetOwningNode() ? *L->GetOwningNode()->GetName() : TEXT("?"), L ? *L->PinName.ToString() : TEXT("?"))); + } + } + Out.Sort(); + return Out; + } + + bool IsDeprecatedSkeletalMeshGet(const UK2Node_VariableGet* Node) + { + if (!Node || Node->VariableReference.GetMemberName() != SkeletalMeshPropertyName) + { + return false; + } + const UClass* Owner = Node->VariableReference.GetMemberParentClass(); + return Owner && Owner->IsChildOf(USkinnedMeshComponent::StaticClass()); + } + + bool HasExecPins(const UEdGraphNode* Node) + { + for (const UEdGraphPin* Pin : Node->Pins) + { + if (Pin && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) + { + return true; + } + } + return false; + } +} + +ULPFixSkeletalMeshGetCommandlet::ULPFixSkeletalMeshGetCommandlet() +{ + IsClient = false; + IsServer = false; + IsEditor = true; + LogToConsole = true; +} + +int32 ULPFixSkeletalMeshGetCommandlet::Main(const FString& Params) +{ + const bool bDryRun = Params.Contains(TEXT("DryRun"), ESearchCase::IgnoreCase); + + UFunction* NewFunction = ULPMeshCompat::StaticClass()->FindFunctionByName(NewFunctionName); + if (!NewFunction) + { + UE_LOG(LogLPFixSkelMesh, Error, TEXT("ULPMeshCompat::GetSkeletalMesh introuvable.")); + return 1; + } + + UE_LOG(LogLPFixSkelMesh, Warning, TEXT("=== Get SkeletalMesh (deprecie) -> LPMeshCompat::GetSkeletalMesh%s ==="), + bDryRun ? TEXT(" [DRY RUN]") : TEXT("")); + + FAssetRegistryModule& AssetRegistryModule = + FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + AssetRegistryModule.Get().SearchAllAssets(true); + + TArray BlueprintAssets; + // Sous-classes incluses : couvre les UWidgetBlueprint (CharGen, Inventory, ...). + AssetRegistryModule.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), BlueprintAssets, true); + + const UEdGraphSchema_K2* Schema = GetDefault(); + + int32 NodesFound = 0; + int32 NodesFixed = 0; + int32 Unhandled = 0; + int32 BlueprintsModified = 0; + int32 SaveFailures = 0; + + 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; + int32 FailedHere = 0; + + for (UEdGraph* Graph : Graphs) + { + if (!Graph) + { + continue; + } + // Copie : on ajoute et on detruit des noeuds pendant le parcours. + TArray Snapshot = Graph->Nodes; + for (UEdGraphNode* GraphNode : Snapshot) + { + UK2Node_VariableGet* VarGet = Cast(GraphNode); + if (!IsDeprecatedSkeletalMeshGet(VarGet)) + { + continue; + } + ++NodesFound; + + UEdGraphPin* OldSelf = VarGet->FindPin(UEdGraphSchema_K2::PN_Self, EGPD_Input); + UEdGraphPin* OldValue = VarGet->GetValuePin(); + const TArray SelfBefore = Links(OldSelf); + const TArray ValueBefore = Links(OldValue); + + UE_LOG(LogLPFixSkelMesh, Warning, TEXT(" %s :: %s -> %s | cible [%s] | valeur [%s]"), + *Blueprint->GetName(), *Graph->GetName(), *VarGet->GetName(), + *FString::Join(SelfBefore, TEXT(", ")), *FString::Join(ValueBefore, TEXT(", "))); + + if (!OldValue || SelfBefore.Num() == 0 || HasExecPins(VarGet)) + { + ++Unhandled; + ++FailedHere; + UE_LOG(LogLPFixSkelMesh, Error, TEXT(" cas non pris en charge (%s) : noeud laisse en l'etat."), + !OldValue ? TEXT("pas de broche de valeur") : SelfBefore.Num() == 0 ? TEXT("cible = self") : TEXT("noeud impur")); + continue; + } + if (bDryRun) + { + continue; + } + + UK2Node_CallFunction* NewNode = NewObject(Graph); + NewNode->SetFromFunction(NewFunction); + NewNode->NodePosX = VarGet->NodePosX; + NewNode->NodePosY = VarGet->NodePosY; + NewNode->CreateNewGuid(); + NewNode->PostPlacedNewNode(); + NewNode->AllocateDefaultPins(); + Graph->Modify(); + Graph->AddNode(NewNode, false, false); + + UEdGraphPin* NewComponent = NewNode->FindPin(ComponentPinName, EGPD_Input); + UEdGraphPin* NewValue = NewNode->GetReturnValuePin(); + + bool bOk = NewComponent && NewValue + && Schema->MovePinLinks(*OldSelf, *NewComponent).Response != CONNECT_RESPONSE_DISALLOW + && Schema->MovePinLinks(*OldValue, *NewValue).Response != CONNECT_RESPONSE_DISALLOW; + + // Les noms des noeuds voisins ne changent pas : on doit retrouver exactement les memes extremites. + if (bOk && (Links(NewComponent) != SelfBefore || Links(NewValue) != ValueBefore)) + { + bOk = false; + UE_LOG(LogLPFixSkelMesh, Error, TEXT(" apres : cible [%s] | valeur [%s]"), + *FString::Join(Links(NewComponent), TEXT(", ")), *FString::Join(Links(NewValue), TEXT(", "))); + } + + if (!bOk) + { + // Le Blueprint ne sera pas sauvegarde : le paquet sur disque reste intact. + ++Unhandled; + ++FailedHere; + UE_LOG(LogLPFixSkelMesh, Error, TEXT(" ecart de liaisons : Blueprint non sauvegarde.")); + continue; + } + + VarGet->DestroyNode(); + ++NodesFixed; + ++FixedHere; + } + } + + if (FailedHere > 0) + { + UE_LOG(LogLPFixSkelMesh, Error, TEXT(" -> %s : %d noeud(s) en ecart, Blueprint NON sauvegarde."), + *Blueprint->GetName(), FailedHere); + continue; + } + if (FixedHere == 0 || bDryRun) + { + continue; + } + + ++BlueprintsModified; + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); + FKismetEditorUtilities::CompileBlueprint(Blueprint); + + UPackage* Package = Blueprint->GetOutermost(); + FString Filename; + if (!FPackageName::TryConvertLongPackageNameToFilename( + Package->GetName(), Filename, FPackageName::GetAssetPackageExtension())) + { + ++SaveFailures; + UE_LOG(LogLPFixSkelMesh, 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(LogLPFixSkelMesh, Warning, TEXT(" -> %s sauvegarde (%d noeud(s))."), *Package->GetName(), FixedHere); + } + else + { + ++SaveFailures; + UE_LOG(LogLPFixSkelMesh, Error, TEXT(" -> ECHEC de sauvegarde de %s"), *Package->GetName()); + } + } + + UE_LOG(LogLPFixSkelMesh, Warning, + TEXT("=== Termine : %d noeud(s) trouve(s), %d remplace(s), %d Blueprint(s) modifie(s), %d non traite(s), %d echec(s) de sauvegarde. ==="), + NodesFound, NodesFixed, BlueprintsModified, Unhandled, SaveFailures); + + return (SaveFailures > 0 || Unhandled > 0) ? 1 : 0; +} diff --git a/Source/LifePlayEditorTools/LPFixSkeletalMeshGetCommandlet.h b/Source/LifePlayEditorTools/LPFixSkeletalMeshGetCommandlet.h new file mode 100644 index 0000000..8d0294b --- /dev/null +++ b/Source/LifePlayEditorTools/LPFixSkeletalMeshGetCommandlet.h @@ -0,0 +1,29 @@ +// Portage UE 5.5 - lot L7a (R-03, ERR-012). Voir Source/LifePlay/LPMeshCompat.h. +// +// Remplace chaque noeud K2Node_VariableGet de la propriete depreciee +// USkinnedMeshComponent::SkeletalMesh par un appel a ULPMeshCompat::GetSkeletalMesh(Component), +// au meme emplacement : les liaisons de la cible (self) passent sur Component, celles de la +// valeur sur ReturnValue. +// +// Controle integre : les liaisons relevees avant sont comparees a celles du nouveau noeud. +// Tout ecart, cible non reliee (lecture sur self) ou noeud impur (broches d'execution) : le +// noeud est laisse en l'etat, compte comme non traite, et son Blueprint n'est pas sauvegarde. +// +// Usage : +// UnrealEditor-Cmd.exe -run=LPFixSkeletalMeshGet [-DryRun] + +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPFixSkeletalMeshGetCommandlet.generated.h" + +UCLASS() +class ULPFixSkeletalMeshGetCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPFixSkeletalMeshGetCommandlet(); + virtual int32 Main(const FString& Params) override; +};