diff --git a/Content/BPs/GI.uasset b/Content/BPs/GI.uasset index ce5fe60..9b2266d 100644 Binary files a/Content/BPs/GI.uasset and b/Content/BPs/GI.uasset differ diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index 3acab2c..35e6bb3 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -922,3 +922,4 @@ Validation L7c/L7d (2026-10-02, parc, même cadrage) : la nuit reste claire (sol | CHG-075 | 2026-10-02 | DATA1 | `Plugins/ModdingExtra/.../ModdingExtraBPLibrary.cpp` : statistiques de `LoadTxt` par type de fichier (bilan à la fermeture, commande `ModdingExtra.LoadTxtStats`) ; `11_Audit_Optimisation.md` §17 (stratégie de couche de données) | étape 1 de la couche de données : mesurer avant de construire ; `PopulateRaws` = 12,6 s par chargement de partie (trace O2) ; build `LifePlay_Build_5.5_DATA1` | `git checkout DATA1-avant -- Plugins/ModdingExtra` | | CHG-076 | 2026-10-02 | DATA23 | nouveaux `Plugins/ModdingExtra/.../LPDataCache.h/.cpp` ; `ModdingExtra.cpp` (démarrage en arrière-plan), `ModdingExtraBPLibrary.cpp` (`LoadTxt` depuis la mémoire, `SaveTxt`, vérification, statistiques cache/disque), `ModdingExtra.Build.cs` (`Json`) | couche de données étapes 2 et 3 : `Modules` chargé une fois en arrière-plan, cache compilé JSON par type avec champs et calque ; 2 031 fichiers en 0,14 s depuis le cache ; build `LifePlay_Build_5.5_DATA23` ; validation en jeu en attente | `git checkout DATA23-avant -- Plugins/ModdingExtra` ; ou option `-LPNoDataCache` | | CHG-077 | 2026-10-02 | O1b / DATA23 | `Config/DefaultGame.ini` : `+DirectoriesToNeverCook=(Path="/HairStrands")` retiré ; validation de la couche de données (CHG-076) : 2 658 textes servis par le cache, 0 écart, lecture 0,074 s pour la session | ERR-039 : erreurs `GroomComponent` au démarrage depuis O1 (module conservé par dépendance, contenu non cuit) ; vérifié : 0 erreur après reconstruction | remettre la ligne dans `DefaultGame.ini` | +| CHG-078 | 2026-10-02 | DATA4b | nouveaux `Source/LifePlay/LPDataCompat.h/.cpp` (`WhatWhereToArray` en C++, vérification `-LPVerifyWhatWhere` contre la fonction Blueprint d'origine) et `Source/LifePlayEditorTools/LPFixWhatWhereCommandlet.h/.cpp` ; `Content/BPs/GI.uasset` : 2 appels de l'EventGraph rebranchés (diff : 22 lignes, uniquement les 2 nœuds et les liaisons de leurs voisins) | `WhatWhereToArray` = 94 % de `PopulateScenes` (3,4 s par passage) ; premier essai refusé par le contrôle intégré (broche `Target` non exclue), rien sauvegardé, commandlet corrigée ; build `LifePlay_Build_5.5_DATA4b` ; validation en jeu en attente | `Tools\Restore.ps1 -Lot DATA4b` | diff --git a/Source/LifePlay/LPDataCompat.cpp b/Source/LifePlay/LPDataCompat.cpp new file mode 100644 index 0000000..b7a10c5 --- /dev/null +++ b/Source/LifePlay/LPDataCompat.cpp @@ -0,0 +1,187 @@ +// Portage UE 5.5 - voir LPDataCompat.h + +#include "LPDataCompat.h" + +#include "Kismet/KismetStringLibrary.h" +#include "LifePlay.h" +#include "LPStringCompat.h" +#include "Misc/CommandLine.h" +#include "UObject/UnrealType.h" + +namespace +{ + /** Champ chaine d'une structure Blueprint par son nom d'auteur (les noms internes portent un suffixe GUID). */ + const FStrProperty* FindStructString(const UStruct* Struct, const TCHAR* AuthoredName) + { + for (TFieldIterator It(Struct); It; ++It) + { + if (It->GetAuthoredName() == AuthoredName) + { + return *It; + } + } + return nullptr; + } + + /** Valeurs du champ AuthoredName de chaque element du tableau de structures ArrayName de GI, dans l'ordre. */ + TArray ReadStructArrayField(const UObject* GI, const TCHAR* ArrayName, const TCHAR* AuthoredName) + { + TArray Values; + const FArrayProperty* ArrayProp = GI ? CastField(GI->GetClass()->FindPropertyByName(ArrayName)) : nullptr; + const FStructProperty* Inner = ArrayProp ? CastField(ArrayProp->Inner) : nullptr; + const FStrProperty* Field = Inner ? FindStructString(Inner->Struct, AuthoredName) : nullptr; + if (!Field) + { + UE_LOG(LogLifePlay, Error, TEXT("WhatWhereToArray : champ %s.%s introuvable sur %s"), ArrayName, AuthoredName, *GetNameSafe(GI)); + return Values; + } + FScriptArrayHelper Helper(ArrayProp, ArrayProp->ContainerPtrToValuePtr(GI)); + Values.Reserve(Helper.Num()); + for (int32 i = 0; i < Helper.Num(); ++i) + { + Values.Add(Field->GetPropertyValue_InContainer(Helper.GetRawPtr(i))); + } + return Values; + } + + /** GetArrayItem du Blueprint : valeur par defaut hors bornes. */ + FString ItemOrEmpty(const TArray& Array, int32 Index) + { + return Array.IsValidIndex(Index) ? Array[Index] : FString(); + } + + /** SwitchString du Blueprint (insensible a la casse) : vrai si Value egale un des cas. */ + bool MatchesAnyCase(const FString& Value, std::initializer_list Cases) + { + for (const TCHAR* Case : Cases) + { + if (UKismetStringLibrary::EqualEqual_StriStri(Value, Case)) + { + return true; + } + } + return false; + } +} + +TArray ULPDataCompat::ComputeWhatWhere(const UObject* GI, const FString& Line) +{ + // Partie gauche / droite de la ligne, avec gestion des ':' supplementaires (rejoints par ':'). + const TArray Parts = ULPStringCompat::ParseIntoArray(Line, TEXT(":"), true); + const bool bWhat = UKismetStringLibrary::EqualEqual_StriStri(ItemOrEmpty(Parts, 0), TEXT("WHAT")); + FString SecondPart; + if (Parts.Num() > 2) + { + TArray Dup = Parts; + Dup.RemoveAt(0); + SecondPart = UKismetStringLibrary::JoinStringArray(Dup, TEXT(":")); + } + else + { + SecondPart = ItemOrEmpty(Parts, 1); + } + + // ClearWhiteSpacesFromArray : Trim (debut) puis TrimTrailing, chaque element conserve. + TArray Cleaned; + for (const FString& Item : ULPStringCompat::ParseIntoArray(SecondPart, TEXT(","), true)) + { + Cleaned.Add(UKismetStringLibrary::TrimTrailing(UKismetStringLibrary::Trim(Item))); + } + + if (!UKismetStringLibrary::EqualEqual_StriStri(ItemOrEmpty(Cleaned, 0), TEXT("all"))) + { + return Cleaned; + } + + TArray FinalArray; + if (bWhat) + { + for (const FString& ActionUID : ReadStructArrayField(GI, TEXT("ActionDetail"), TEXT("AmenityUID"))) + { + if (MatchesAnyCase(ActionUID, { TEXT("sleep"), TEXT("sleep_hotel"), TEXT("nap"), TEXT("nap_hotel"), TEXT("masturbate"), + TEXT("bathe"), TEXT("go_to_the_bathroom"), TEXT("check_on_my_family") })) + { + continue; + } + if (ULPStringCompat::Contains(ActionUID, TEXT("_HARDCODE"), false, true)) + { + continue; + } + FinalArray.Add(ActionUID); + } + FinalArray.Remove(TEXT("sleep")); + FinalArray.Remove(TEXT("Sleep")); + } + else + { + for (const FString& Amenity : ReadStructArrayField(GI, TEXT("AmenityAction"), TEXT("Amenity"))) + { + if (!MatchesAnyCase(Amenity, { TEXT("sexwork"), TEXT("crime"), TEXT("modeling"), TEXT("prison") })) + { + FinalArray.Add(Amenity); + } + } + } + + // Elements "-xxx" : retrait de "xxx" (partie droite du premier '-', recherche insensible a la casse). + for (const FString& Item : Cleaned) + { + FString Left, Right; + if (ULPStringCompat::Split(Item, TEXT("-"), Left, Right, ESearchCase::IgnoreCase, ESearchDir::FromStart)) + { + FinalArray.Remove(Right); + } + } + return FinalArray; +} + +void ULPDataCompat::WhatWhereToArray(UObject* Target, const FString& Line, TArray& Array) +{ + Array = ComputeWhatWhere(Target, Line); + + static const bool bVerify = FParse::Param(FCommandLine::Get(), TEXT("LPVerifyWhatWhere")); + if (!bVerify || !Target) + { + return; + } + // Verification : execution de la fonction Blueprint d'origine de GI et comparaison exacte (ordre et casse). + UFunction* Original = Target->FindFunction(TEXT("WhatWhereToArray")); + const FStrProperty* LineProp = Original ? CastField(Original->FindPropertyByName(TEXT("Line"))) : nullptr; + const FArrayProperty* ArrayProp = Original ? CastField(Original->FindPropertyByName(TEXT("Array"))) : nullptr; + if (!LineProp || !ArrayProp) + { + UE_LOG(LogLifePlay, Error, TEXT("WhatWhereToArray : fonction Blueprint d'origine introuvable, verification impossible")); + return; + } + uint8* Params = static_cast(FMemory_Alloca(Original->ParmsSize)); + FMemory::Memzero(Params, Original->ParmsSize); + for (TFieldIterator It(Original); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) + { + It->InitializeValue_InContainer(Params); + } + LineProp->SetPropertyValue_InContainer(Params, Line); + Target->ProcessEvent(Original, Params); + const TArray& Expected = *ArrayProp->ContainerPtrToValuePtr>(Params); + + static int32 Calls = 0, Mismatches = 0; + ++Calls; + bool bSame = Expected.Num() == Array.Num(); + for (int32 i = 0; bSame && i < Array.Num(); ++i) + { + bSame = Expected[i].Equals(Array[i], ESearchCase::CaseSensitive); + } + if (!bSame) + { + ++Mismatches; + UE_LOG(LogLifePlay, Error, TEXT("WhatWhereToArray ECART #%d | '%s' : Blueprint %d element(s) [%s], C++ %d element(s) [%s]"), + Mismatches, *Line, Expected.Num(), *FString::Join(Expected, TEXT(",")).Left(300), Array.Num(), *FString::Join(Array, TEXT(",")).Left(300)); + } + if (Calls % 500 == 0 || !bSame) + { + UE_LOG(LogLifePlay, Display, TEXT("WhatWhereToArray verification : %d appel(s), %d ecart(s)"), Calls, Mismatches); + } + for (TFieldIterator It(Original); It && It->HasAnyPropertyFlags(CPF_Parm); ++It) + { + It->DestroyValue_InContainer(Params); + } +} diff --git a/Source/LifePlay/LPDataCompat.h b/Source/LifePlay/LPDataCompat.h new file mode 100644 index 0000000..ccc33cb --- /dev/null +++ b/Source/LifePlay/LPDataCompat.h @@ -0,0 +1,30 @@ +// Portage UE 5.5 - couche de donnees, etape 4b (Docs/Migration/11_Audit_Optimisation.md §17.4). +// +// GI::WhatWhereToArray developpe les lignes "WHAT: ..." et "WHERE: ..." des en-tetes de scenes en listes d'actions ou de +// lieux. Appelee 1 558 fois par passage de PopulateRaws, elle represente 94 % de PopulateScenes (3,4 s) : pour chaque +// "all", le Blueprint parcourt GI.ActionDetail ou GI.AmenityAction en copiant chaque structure. +// +// Cette fonction reproduit le graphe Blueprint a l'identique (memes fonctions de chaine LPStringCompat / KismetString, +// memes exclusions, memes doublons, meme ordre). Les deux appels de l'EventGraph de GI y sont rebranches par la +// commandlet LPFixWhatWhere. Option -LPVerifyWhatWhere : chaque appel execute aussi la fonction Blueprint d'origine +// (toujours presente dans GI) et journalise tout ecart. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "LPDataCompat.generated.h" + +UCLASS() +class LIFEPLAY_API ULPDataCompat : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + /** Equivalent C++ de GI::WhatWhereToArray. Target = instance de jeu (GI), implicite depuis GI. */ + UFUNCTION(BlueprintCallable, Category = "LifePlay|Compat", meta = (DefaultToSelf = "Target", HidePin = "Target")) + static void WhatWhereToArray(UObject* Target, const FString& Line, TArray& Array); + + /** Implementation pure (sans verification), exposee pour les tests. */ + static TArray ComputeWhatWhere(const UObject* GI, const FString& Line); +}; diff --git a/Source/LifePlayEditorTools/LPFixWhatWhereCommandlet.cpp b/Source/LifePlayEditorTools/LPFixWhatWhereCommandlet.cpp new file mode 100644 index 0000000..be25210 --- /dev/null +++ b/Source/LifePlayEditorTools/LPFixWhatWhereCommandlet.cpp @@ -0,0 +1,315 @@ +// Portage UE 5.5 - voir LPFixWhatWhereCommandlet.h + +#include "LPFixWhatWhereCommandlet.h" + +#include "AssetRegistry/AssetData.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "Engine/Blueprint.h" +#include "K2Node_CallFunction.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "LPDataCompat.h" +#include "Misc/PackageName.h" +#include "UObject/Package.h" +#include "UObject/SavePackage.h" + +DEFINE_LOG_CATEGORY_STATIC(LogLPFixWhatWhere, Log, All); + +namespace +{ + /** 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; + } + + /** Etat comparable d'une broche : liaisons et valeur par defaut. */ + FString PinState(const UEdGraphPin* Pin) + { + return FString::Printf(TEXT("[%s] defaut='%s' objet=%s texte='%s'"), + *FString::Join(Links(Pin), TEXT(", ")), *Pin->DefaultValue, + Pin->DefaultObject ? *Pin->DefaultObject->GetPathName() : TEXT("None"), *Pin->DefaultTextValue.ToString()); + } + + /** ULPDataCompat::WhatWhereToArray si le noeud appelle la fonction WhatWhereToArray de GI (appel sur self), sinon nullptr. */ + UFunction* ReplacementFor(const UK2Node_CallFunction* Node) + { + if (!Node || Node->FunctionReference.GetMemberName() != FName(TEXT("WhatWhereToArray"))) + { + return nullptr; + } + const UClass* Owner = Node->FunctionReference.GetMemberParentClass(Node->GetBlueprintClassFromNode()); + if (!Owner || (!Owner->GetName().StartsWith(TEXT("GI_C")) && !Owner->GetName().StartsWith(TEXT("SKEL_GI_C")))) + { + return nullptr; + } + return ULPDataCompat::StaticClass()->FindFunctionByName(TEXT("WhatWhereToArray")); + } + + /** Broches ignorees dans les comparaisons : cible implicite (self de l'appel d'origine, Target cache du remplacant). */ + bool IsTargetPin(const UEdGraphPin* Pin) + { + return Pin && (Pin->PinName == UEdGraphSchema_K2::PN_Self || Pin->PinName == FName(TEXT("Target"))); + } + + bool HasExecPins(const UEdGraphNode* Node) + { + for (const UEdGraphPin* Pin : Node->Pins) + { + if (Pin && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) + { + return true; + } + } + return false; + } +} + +ULPFixWhatWhereCommandlet::ULPFixWhatWhereCommandlet() +{ + IsClient = false; + IsServer = false; + IsEditor = true; + LogToConsole = true; +} + +int32 ULPFixWhatWhereCommandlet::Main(const FString& Params) +{ + const bool bDryRun = Params.Contains(TEXT("DryRun"), ESearchCase::IgnoreCase); + + if (!ULPDataCompat::StaticClass()->FindFunctionByName(TEXT("WhatWhereToArray"))) + { + UE_LOG(LogLPFixWhatWhere, Error, TEXT("ULPDataCompat::WhatWhereToArray introuvable.")); + return 1; + } + TMap PerFunction; + + UE_LOG(LogLPFixWhatWhere, Warning, TEXT("=== GI::WhatWhereToArray -> LPDataCompat::WhatWhereToArray%s ==="), + bDryRun ? TEXT(" [DRY RUN]") : TEXT("")); + + FAssetRegistryModule& AssetRegistryModule = + FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + AssetRegistryModule.Get().SearchAllAssets(true); + + TArray BlueprintAssets; + // Sous-classes incluses : couvre les UWidgetBlueprint. + 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 != FName(TEXT("/Game/BPs/GI"))) + { + 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_CallFunction* OldNode = Cast(GraphNode); + UFunction* NewFunction = ReplacementFor(OldNode); + if (!NewFunction) + { + continue; + } + ++NodesFound; + ++PerFunction.FindOrAdd(NewFunction->GetFName()); + + UE_LOG(LogLPFixWhatWhere, Warning, TEXT(" %s :: %s -> %s %s (%d, %d)"), + *Blueprint->GetName(), *Graph->GetName(), *OldNode->GetName(), *NewFunction->GetName(), OldNode->NodePosX, OldNode->NodePosY); + + // Etat avant, par nom de broche (self exclu : cible implicite de la bibliotheque). + TMap Before; + for (const UEdGraphPin* Pin : OldNode->Pins) + { + if (Pin && !IsTargetPin(Pin)) + { + Before.Add(Pin->PinName, PinState(Pin)); + } + } + const UEdGraphPin* OldSelf = OldNode->FindPin(UEdGraphSchema_K2::PN_Self, EGPD_Input); + + // Appel impur (broches d'execution) attendu des deux cotes ; seule une cible self reliee est refusee. + if (!HasExecPins(OldNode) || (OldSelf && OldSelf->LinkedTo.Num() > 0)) + { + ++Unhandled; + ++FailedHere; + UE_LOG(LogLPFixWhatWhere, Error, TEXT(" cas non pris en charge (%s) : noeud laisse en l'etat."), + !HasExecPins(OldNode) ? TEXT("noeud pur inattendu") : TEXT("cible self reliee")); + continue; + } + if (bDryRun) + { + continue; + } + + UK2Node_CallFunction* NewNode = NewObject(Graph); + NewNode->SetFromFunction(NewFunction); + NewNode->NodePosX = OldNode->NodePosX; + NewNode->NodePosY = OldNode->NodePosY; + NewNode->CreateNewGuid(); + NewNode->PostPlacedNewNode(); + NewNode->AllocateDefaultPins(); + Graph->Modify(); + Graph->AddNode(NewNode, false, false); + + bool bOk = true; + for (UEdGraphPin* OldPin : OldNode->Pins) + { + if (!OldPin || IsTargetPin(OldPin)) + { + continue; + } + UEdGraphPin* NewPin = NewNode->FindPin(OldPin->PinName, OldPin->Direction); + if (!NewPin) + { + bOk = false; + UE_LOG(LogLPFixWhatWhere, Error, TEXT(" broche %s sans equivalent."), *OldPin->PinName.ToString()); + break; + } + NewPin->DefaultValue = OldPin->DefaultValue; + NewPin->DefaultObject = OldPin->DefaultObject; + NewPin->DefaultTextValue = OldPin->DefaultTextValue; + if (OldPin->LinkedTo.Num() > 0 + && Schema->MovePinLinks(*OldPin, *NewPin).Response == CONNECT_RESPONSE_DISALLOW) + { + bOk = false; + UE_LOG(LogLPFixWhatWhere, Error, TEXT(" liaison refusee sur %s."), *OldPin->PinName.ToString()); + break; + } + } + + // Meme ensemble de broches, memes liaisons (les noms des voisins ne changent pas), memes valeurs. + if (bOk) + { + int32 Compared = 0; + for (const UEdGraphPin* NewPin : NewNode->Pins) + { + if (!NewPin || IsTargetPin(NewPin)) + { + continue; + } + const FString* Expected = Before.Find(NewPin->PinName); + if (!Expected || *Expected != PinState(NewPin)) + { + bOk = false; + UE_LOG(LogLPFixWhatWhere, Error, TEXT(" apres : %s %s (attendu %s)"), + *NewPin->PinName.ToString(), *PinState(NewPin), Expected ? **Expected : TEXT("")); + } + ++Compared; + } + if (Compared != Before.Num()) + { + bOk = false; + UE_LOG(LogLPFixWhatWhere, Error, TEXT(" %d broche(s) apres, %d avant."), Compared, Before.Num()); + } + } + + if (!bOk) + { + // Le Blueprint ne sera pas sauvegarde : le paquet sur disque reste intact. + ++Unhandled; + ++FailedHere; + UE_LOG(LogLPFixWhatWhere, Error, TEXT(" ecart : Blueprint non sauvegarde.")); + continue; + } + + OldNode->DestroyNode(); + ++NodesFixed; + ++FixedHere; + } + } + + if (FailedHere > 0) + { + UE_LOG(LogLPFixWhatWhere, 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(LogLPFixWhatWhere, 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(LogLPFixWhatWhere, Warning, TEXT(" -> %s sauvegarde (%d noeud(s))."), *Package->GetName(), FixedHere); + } + else + { + ++SaveFailures; + UE_LOG(LogLPFixWhatWhere, Error, TEXT(" -> ECHEC de sauvegarde de %s"), *Package->GetName()); + } + } + + for (const TPair& It : PerFunction) + { + UE_LOG(LogLPFixWhatWhere, Warning, TEXT(" %s : %d noeud(s)"), *It.Key.ToString(), It.Value); + } + UE_LOG(LogLPFixWhatWhere, 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/LPFixWhatWhereCommandlet.h b/Source/LifePlayEditorTools/LPFixWhatWhereCommandlet.h new file mode 100644 index 0000000..f119bc1 --- /dev/null +++ b/Source/LifePlayEditorTools/LPFixWhatWhereCommandlet.h @@ -0,0 +1,24 @@ +// Portage UE 5.5 - couche de donnees, etape 4b. Voir Source/LifePlay/LPDataCompat.h. +// +// Rebranche les appels de GI a sa fonction Blueprint WhatWhereToArray (2 appels dans l'EventGraph, PopulateScenes) sur +// ULPDataCompat::WhatWhereToArray (memes broches Line et Array, broches d'execution conservees, cible implicite GI). +// Controle integre (repris de LPFixStringSearch) : l'etat de chaque broche est compare avant et apres ; tout ecart : +// noeud laisse en l'etat, Blueprint non sauvegarde. La fonction Blueprint d'origine reste dans GI (verification). +// +// Usage : +// UnrealEditor-Cmd.exe -run=LPFixWhatWhere [-DryRun] +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPFixWhatWhereCommandlet.generated.h" + +UCLASS() +class ULPFixWhatWhereCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPFixWhatWhereCommandlet(); + virtual int32 Main(const FString& Params) override; +};