diff --git a/Content/BPs/AI.uasset b/Content/BPs/AI.uasset index c10b568..6dd9dae 100644 Binary files a/Content/BPs/AI.uasset and b/Content/BPs/AI.uasset differ diff --git a/Content/BPs/Person.uasset b/Content/BPs/Person.uasset index cba0722..7cbfc6d 100644 Binary files a/Content/BPs/Person.uasset and b/Content/BPs/Person.uasset differ diff --git a/Content/BPs/Room.uasset b/Content/BPs/Room.uasset index 5391254..397ab97 100644 Binary files a/Content/BPs/Room.uasset and b/Content/BPs/Room.uasset differ diff --git a/Docs/Migration/04_Cahier_Des_Charges.md b/Docs/Migration/04_Cahier_Des_Charges.md index 76f2c59..f35013b 100644 --- a/Docs/Migration/04_Cahier_Des_Charges.md +++ b/Docs/Migration/04_Cahier_Des_Charges.md @@ -142,7 +142,7 @@ Ordre indicatif, à réviser selon le registre : | Lot | Famille | Périmètre | |---|---|---| | L7a | R-03 refait correctement | ✅ CX-B10 (56 nœuds, 8 Blueprints), validé en jeu le 2026-09-30 | -| L7b | Erreurs d'exécution fréquentes | `ParseIntoArray`, `getBeautyValue`, `standIdle`, `ContactMenu` | +| L7b | R-04 : derniers nœuds dépréciés | ✅ CX-B11 (6 nœuds `GetRandomPointInNavigableRadius`, 3 Blueprints) : compilation 0 avertissement ; validation en jeu en attente | | L7c | Configuration | clés de nativisation, rendu 5.5 explicite (GI, ombres, RHI) | | L7d | Visuel | AtmosphericFog → SkyAtmosphere ; reconstruction de l'éclairage | | L7e | Effets | 4 systèmes Cascade → Niagara (si nécessaire) ; NvCloth → Chaos | @@ -151,6 +151,7 @@ Ordre indicatif, à réviser selon le registre : | L7h | Vérification StreetMap | `LPCheckStreetMap` sur 4.27 et 5.5, comparaison des CSV (ERR-018) | | L7i | Variables DSL `Person` vides | ERR-030/ERR-003 : `GI::Dress` par rafales (0 à 284 par personnage selon les lieux), `ReturnStatIfNeeded` ; traceur par fonction | | L7j | Écran secondaire (amélioration) | ERR-031, préexistant en 4.27 : caméra `SecondScreen` sans racine ; seulement si souhaité | +| L7k | Erreurs d'exécution fréquentes (ex-L7b) | `getBeautyValue`, `standIdle` (ERR-006), `ContactMenu` (ERR-007) ; `ParseIntoArray` déjà traité (CX-B08) | ## 4. Décisions diff --git a/Docs/Migration/05_Correctifs_Isoles.md b/Docs/Migration/05_Correctifs_Isoles.md index af2c64c..affa699 100644 --- a/Docs/Migration/05_Correctifs_Isoles.md +++ b/Docs/Migration/05_Correctifs_Isoles.md @@ -38,10 +38,11 @@ exporté dans `Docs/Migration/correctifs/` pour relecture. | CX-B03 | `LPFixZeroScaleActors -Fix -Scale=0.1` | `Scenes/CentralLondon` : `Wall.StaticMeshComponent0` | échelle X 0 → 0,1 | corps physique invalide ; `ensure InverseFast` (plantage éditeur du 2026-09-07) | 3 | **réimporté** (CHG-023) | | CX-B04 | `LPFixOrphanBoundEvents` | `UI/TopRight` (1), `UI/CharGen` (2) | 3 événements `On Clicked` orphelins supprimés | avertissements ; clics déjà morts | 4 | **réimporté** (CHG-024) | | CX-B05 | `LPFixDeprecatedNodes` (R-03) | 56 nœuds `SkeletalMesh`, 10 Blueprints | `GetSkeletalMeshAsset()` | **rejeté** : aggrave ERR-002 (16,6 → 35 `Accessed None`/perso) car les proxies sont des `USkinnedMeshComponent` | — | à refaire autrement (L7a) | -| CX-B06 | `LPFixDeprecatedNodes` (R-04) | 6 nœuds, `AI`, `Person`, `Room` | `LPNavigationHelpers` | avertissements de dépréciation seulement | 5 | reporté (L7) | +| CX-B06 | `LPFixDeprecatedNodes` (R-04) | 6 nœuds, `AI`, `Person`, `Room` | `LPNavigationHelpers` | avertissements de dépréciation seulement | 5 | **remplacé par CX-B11** (commandlet sans contrôle, embarquait aussi la variante R-03 rejetée) | | CX-B08 | `LPFixParseIntoArray` (nouveau) | 114 nœuds `ParseIntoArray`, 17 Blueprints | rebranchés sur `LPStringCompat::ParseIntoArray` (algorithme UE 4.27) | UE5 renvoie `[""]` pour une chaîne vide : cause d'ERR-027 (personnages de quête invisibles/dévêtus) | 1 | **réimporté** (CHG-029), validé en jeu | | CX-B09 | `LPFixButtonPadding` (nouveau) | 430 `UButton`, 46 Widget Blueprints | marges des boutons remises au défaut UE 4.27 (Normal 2,2,2,2 ; Pressed 2,3,2,1), composante par composante | le style par défaut de `UButton` est passé à 12 px horizontaux en UE5 (`FUMGCoreStyle`) ; les assets 4.27 ne stockent pas la marge par défaut : cause d'ERR-034 (barre de temps décalée) | 1 | **réimporté** (CHG-033), validé en jeu | | CX-B10 | `LPFixSkeletalMeshGet` (nouveau) | 56 nœuds `Get SkeletalMesh`, 8 Blueprints (`Person` 28, `GI` 17, `CharGen` 3, `Inventory` 3, `Hairstyle` 2, `CharGenSlider`, `Cursor`, `Inventory_Cloth` 1) | remplacés par `LPMeshCompat::GetSkeletalMesh(Component)` = `Cast(GetSkinnedAsset())`, la sémantique exacte du getter déprécié, valable pour tout `USkinnedMeshComponent` | R-03 refait (L7a) ; remplace CX-B05 rejeté | 1 | **réimporté** (CHG-036), validé en jeu | +| CX-B11 | `LPFixNavRandomPoint` (nouveau) | 6 nœuds `GetRandomPointInNavigableRadius` (`Person` 3, `Room` 2, `AI` 1) | remplacés par `LPNavigationHelpers::GetRandomLocationInNavigableRadius` : nœud pur, mêmes broches, même corps que la fonction dépréciée (appel de `K2_GetRandomLocationInNavigableRadius`, identique en 4.27 et 5.5) ; liaisons et valeurs par défaut reportées broche par broche | R-04 (L7b) : 6 derniers avertissements de compilation ; le remplaçant moteur est impur (broches d'exécution), d'où l'enrobage pur | 1 | **réimporté** (CHG-037) ; validation en jeu en attente | | CX-B07 | `LPResavePackages` (nouveau) | 239 paquets UE4 restants (`UI` 147, `Structs` 57, `BPs` 22, `Render` 11, `Characters` 1, `Scenes` 1) | format UE5, contenu inchangé | EX-03 ; T-STAT-01 | 6 | **réimporté** (CHG-035), diff avant/après neutre ; validation en jeu en attente | ## 3. Protocole de réimport sous contrôle renforcé diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index 17e6cba..2172918 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -545,4 +545,26 @@ sortie normale, sauvegardes `1.sav`/`2.sav` ; tests utilisateur effectués sans (situationnel, ERR-030). Messages moteur `GetSocketInfoByName … No SkeletalMesh for Component(CharacterMesh0)` : même signature et mêmes 5 sockets qu'en V1rc, volume lié aux scènes jouées (ERR-011, existe en 4.27) : non imputable à L7a. Fonctions de variables DSL (`getActorVar`, `clearVariable`, `CallFunction`) non touchées par L7a, en hausse par personnage -sur cette partie : famille ERR-003, à suivre en L7i. +sur cette partie : famille ERR-003, à suivre en L7i. + +### L7b — R-04 : derniers nœuds dépréciés (2026-09-30) + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +|---|---|---|---|---|---| +| CHG-037 | 2026-09-30 | L7b (CX-B11) | `Source/LifePlayEditorTools/LPFixNavRandomPointCommandlet.h/.cpp` (nouveau) ; `Tools/DiffRemplacementNoeuds.py` (profils par lot : `L7a`, `L7b`) ; `Content/BPs/AI`, `Person`, `Room` (`etat/L7b_assets.txt`) | ERR-035 / R-04 : 6 nœuds `GetRandomPointInNavigableRadius` dépréciés | `Tools\Restore.ps1 -Lot L7b` ; `git checkout L7b-avant -- Source Tools` | + +Choix : le remplaçant proposé par le moteur (`K2_GetRandomLocationInNavigableRadius`) est `BlueprintCallable`, alors que +l'ancien nœud est pur ; le brancher imposerait de recâbler le flux d'exécution. On réutilise `LPNavigationHelpers` +(enrobage pur déjà présent, CX-C09) : mêmes broches, et même corps que la fonction dépréciée en 4.27 comme en 5.5 +(simple appel de `K2_GetRandomLocationInNavigableRadius`). `LPFixDeprecatedNodes` (CX-B06) n'est pas réutilisée : elle +ne contrôle rien et embarque aussi la variante R-03 rejetée. + +| Contrôle | Résultat | Preuve | +|---|---|---| +| Simulation | 6 nœuds (`Room::moveToBedIfPossible`, `Room::getStandSpots`, `Person::standIdle` ×2, `Person::jumpOffFnt`, `AI::EventGraph`), 0 cas non pris en charge | `etat/L7b_LPFixNavRandomPoint_20260930-183406.txt` | +| Application (contrôle intégré : liaisons et valeurs par défaut de chaque broche) | 6/6 remplacés, 3 Blueprints sauvegardés, 0 écart | `etat/L7b_LPFixNavRandomPoint_20260930-183707.txt` | +| Diff indépendant `LPDumpAssets` (`DiffRemplacementNoeuds.py … L7b`) | 6 paires ; 7 broches identiques (liaisons et défauts, ex. `Radius` 100 / 200 / 1000 / 2000 / 100000) ; **0 autre objet modifié** ; le profil `L7a` rejoué sur les données L7a donne toujours 56/56 et 0 écart | `Saved/Logs/portage/L7b_assets_{avant,apres}.txt` | +| `CompileAllBlueprints` | 0 erreur, **0 avertissement** (6 avant) | `etat/L7b_CompileAllBlueprints_20260930-183909.txt` | +| Build empaquetée `LifePlay_Build_5.5_L7b` | cuisson 0 erreur, **0 avertissement** (3 avant) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L7b_BuildCookRun.log` | + +Validation en jeu en attente (déplacements des personnages : `standIdle`, `jumpOffFnt`, `moveToBedIfPossible`, IA). diff --git a/Docs/Migration/Registre_Erreurs.md b/Docs/Migration/Registre_Erreurs.md index deaa1fb..e23f875 100644 --- a/Docs/Migration/Registre_Erreurs.md +++ b/Docs/Migration/Registre_Erreurs.md @@ -17,8 +17,8 @@ | ERR-003 | `Attempted to access index 0 from array Float_… of length 0` dans `GI::ReturnStatIfNeeded` (variable DSL `Float` vide) | build 5.5 Development | 2 (4.27 : 0) | haute | L5 | ouvert | | ERR-004 | `index N from CallFunc_ParseIntoArray_ReturnValue of length N` (`GI` EventGraph) | PIE 09-28 | 6 510 | moyenne | L7b | ouvert — origine localisée (L3) : `GI.populateClothes4` appelée par `Person.ReceiveBeginPlay` | | ERR-005 | `getBeautyValue` : `Floats_*` index hors bornes | PIE 09-28 | 1 297 | moyenne | L7b | ouvert | -| ERR-006 | `Accès à None` `NearVec` dans `Person::standIdle` | PIE 09-28 | 273 | moyenne | L7b | ouvert | -| ERR-007 | `Accès à None` `ContactMenu` dans `UpdateDailyPose` | PIE 09-28 | 257 | moyenne | L7b | ouvert | +| ERR-006 | `Accès à None` `NearVec` dans `Person::standIdle` | PIE 09-28 | 273 | moyenne | L7k | ouvert | +| ERR-007 | `Accès à None` `ContactMenu` dans `UpdateDailyPose` | PIE 09-28 | 257 | moyenne | L7k | ouvert | | ERR-008 | `CallFunc_Array_Get_Item` à None dans `GI` | PIE 09-28 | 229 | moyenne | L5 | ouvert | | ERR-009 | `SpawnActor failed because no class was specified` | PIE 09-28 | 11 | moyenne | L6 | ouvert | | ERR-010 | `LogLPModdingTrace` : instrumentation de portage à chaque `LoadTxt` | tous journaux | ≈ 1 300 à 5 400 / session | faible (bruit) | L3 | clos (CHG-016) : `LogModdingExtra` en Verbose | @@ -39,3 +39,4 @@ | ERR-032 | Scintillement à l'approche d'un personnage | retour utilisateur 2026-09-30 | — | moyenne (visuel) | L6a | **corrigé (CHG-031)**, validé en jeu le 2026-09-30 : on peut s'approcher des PNJ sans scintillement | | ERR-033 | Certaines actions demandent deux clics pour démarrer | retour utilisateur 2026-09-30 | — | moyenne (ergonomie) | L6d | **non reproduit** depuis le build L6e (constat utilisateur 2026-09-30) ; aucune modification ciblée — coïncide avec CX-B09 (marges des boutons), sans lien démontré ; sous surveillance, à rouvrir s'il réapparaît | | ERR-034 | La barre de temps est décalée vers la droite | retour utilisateur 2026-09-30 | — | faible (visuel) | L6e | **corrigé (CHG-033, CX-B09)** — cause : marge par défaut des `UButton` passée de 2 px (UE4 `FCoreStyle`) à 12 px horizontaux (UE5 `FUMGCoreStyle`) ; 5 boutons × 20 px = 100 px de débordement ; **validé en jeu le 2026-09-30** (build L6e, confirmé par l'utilisateur) | +| ERR-035 | 6 nœuds `GetRandomPointInNavigableRadius` dépréciés (R-04 : `Person` 3, `Room` 2, `AI` 1) | `LogBlueprint` | 6 (compilation), 3 (cuisson) | faible (dette) | L7b | **corrigé (CHG-037, CX-B11)** : remplacés par `LPNavigationHelpers::GetRandomLocationInNavigableRadius` (pur, même corps) ; `CompileAllBlueprints` 6 → **0 avertissement** ; validation en jeu en attente | diff --git a/Source/LifePlayEditorTools/LPFixNavRandomPointCommandlet.cpp b/Source/LifePlayEditorTools/LPFixNavRandomPointCommandlet.cpp new file mode 100644 index 0000000..76cd919 --- /dev/null +++ b/Source/LifePlayEditorTools/LPFixNavRandomPointCommandlet.cpp @@ -0,0 +1,303 @@ +// Portage UE 5.5 - voir LPFixNavRandomPointCommandlet.h + +#include "LPFixNavRandomPointCommandlet.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 "LPNavigationHelpers.h" +#include "Misc/PackageName.h" +#include "NavigationSystem.h" +#include "UObject/Package.h" +#include "UObject/SavePackage.h" + +DEFINE_LOG_CATEGORY_STATIC(LogLPFixNavRandomPoint, Log, All); + +namespace +{ + const FName OldFunctionName(TEXT("K2_GetRandomPointInNavigableRadius")); + const FName NewFunctionName(TEXT("GetRandomLocationInNavigableRadius")); + + /** 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()); + } + + bool IsDeprecatedCall(const UK2Node_CallFunction* Node) + { + if (!Node || Node->FunctionReference.GetMemberName() != OldFunctionName) + { + return false; + } + const UClass* Owner = Node->FunctionReference.GetMemberParentClass(); + return Owner && Owner->IsChildOf(UNavigationSystemV1::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; + } +} + +ULPFixNavRandomPointCommandlet::ULPFixNavRandomPointCommandlet() +{ + IsClient = false; + IsServer = false; + IsEditor = true; + LogToConsole = true; +} + +int32 ULPFixNavRandomPointCommandlet::Main(const FString& Params) +{ + const bool bDryRun = Params.Contains(TEXT("DryRun"), ESearchCase::IgnoreCase); + + UFunction* NewFunction = ULPNavigationHelpers::StaticClass()->FindFunctionByName(NewFunctionName); + if (!NewFunction) + { + UE_LOG(LogLPFixNavRandomPoint, Error, TEXT("ULPNavigationHelpers::GetRandomLocationInNavigableRadius introuvable.")); + return 1; + } + + UE_LOG(LogLPFixNavRandomPoint, Warning, TEXT("=== GetRandomPointInNavigableRadius (deprecie) -> LPNavigationHelpers%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.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_CallFunction* OldNode = Cast(GraphNode); + if (!IsDeprecatedCall(OldNode)) + { + continue; + } + ++NodesFound; + + UE_LOG(LogLPFixNavRandomPoint, Warning, TEXT(" %s :: %s -> %s (%d, %d)"), + *Blueprint->GetName(), *Graph->GetName(), *OldNode->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 && Pin->PinName != UEdGraphSchema_K2::PN_Self) + { + Before.Add(Pin->PinName, PinState(Pin)); + UE_LOG(LogLPFixNavRandomPoint, Warning, TEXT(" %s %s %s"), + Pin->Direction == EGPD_Input ? TEXT("in ") : TEXT("out"), *Pin->PinName.ToString(), *PinState(Pin)); + } + } + const UEdGraphPin* OldSelf = OldNode->FindPin(UEdGraphSchema_K2::PN_Self, EGPD_Input); + + if (HasExecPins(OldNode) || (OldSelf && OldSelf->LinkedTo.Num() > 0)) + { + ++Unhandled; + ++FailedHere; + UE_LOG(LogLPFixNavRandomPoint, Error, TEXT(" cas non pris en charge (%s) : noeud laisse en l'etat."), + HasExecPins(OldNode) ? TEXT("noeud impur") : 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 || OldPin->PinName == UEdGraphSchema_K2::PN_Self) + { + continue; + } + UEdGraphPin* NewPin = NewNode->FindPin(OldPin->PinName, OldPin->Direction); + if (!NewPin) + { + bOk = false; + UE_LOG(LogLPFixNavRandomPoint, 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(LogLPFixNavRandomPoint, 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 || NewPin->PinName == UEdGraphSchema_K2::PN_Self) + { + continue; + } + const FString* Expected = Before.Find(NewPin->PinName); + if (!Expected || *Expected != PinState(NewPin)) + { + bOk = false; + UE_LOG(LogLPFixNavRandomPoint, Error, TEXT(" apres : %s %s (attendu %s)"), + *NewPin->PinName.ToString(), *PinState(NewPin), Expected ? **Expected : TEXT("")); + } + ++Compared; + } + if (Compared != Before.Num()) + { + bOk = false; + UE_LOG(LogLPFixNavRandomPoint, 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(LogLPFixNavRandomPoint, Error, TEXT(" ecart : Blueprint non sauvegarde.")); + continue; + } + + OldNode->DestroyNode(); + ++NodesFixed; + ++FixedHere; + } + } + + if (FailedHere > 0) + { + UE_LOG(LogLPFixNavRandomPoint, 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(LogLPFixNavRandomPoint, 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(LogLPFixNavRandomPoint, Warning, TEXT(" -> %s sauvegarde (%d noeud(s))."), *Package->GetName(), FixedHere); + } + else + { + ++SaveFailures; + UE_LOG(LogLPFixNavRandomPoint, Error, TEXT(" -> ECHEC de sauvegarde de %s"), *Package->GetName()); + } + } + + UE_LOG(LogLPFixNavRandomPoint, 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/LPFixNavRandomPointCommandlet.h b/Source/LifePlayEditorTools/LPFixNavRandomPointCommandlet.h new file mode 100644 index 0000000..952fd03 --- /dev/null +++ b/Source/LifePlayEditorTools/LPFixNavRandomPointCommandlet.h @@ -0,0 +1,32 @@ +// Portage UE 5.5 - lot L7b (R-04, CX-B11). Voir Source/LifePlay/LPNavigationHelpers.h. +// +// Remplace chaque appel a UNavigationSystemV1::K2_GetRandomPointInNavigableRadius (deprecie, +// BlueprintPure) par ULPNavigationHelpers::GetRandomLocationInNavigableRadius (pur lui aussi, +// memes broches, meme corps qu'en 4.27 et 5.5), au meme emplacement. Liaisons et valeurs par +// defaut sont reportees broche par broche, par nom. +// +// Remplace la partie R-04 de LPFixDeprecatedNodes (CX-B06), qui reaiguillait sans controle et +// embarquait aussi la variante R-03 rejetee (CX-B05). +// +// Controle integre : l'etat de chaque broche (liaisons, valeur par defaut) est compare avant et +// apres. Tout ecart, broche sans equivalent, cible self reliee ou noeud impur : le noeud est laisse +// en l'etat, compte comme non traite, et son Blueprint n'est pas sauvegarde. +// +// Usage : +// UnrealEditor-Cmd.exe -run=LPFixNavRandomPoint [-DryRun] + +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPFixNavRandomPointCommandlet.generated.h" + +UCLASS() +class ULPFixNavRandomPointCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPFixNavRandomPointCommandlet(); + virtual int32 Main(const FString& Params) override; +}; diff --git a/Tools/DiffRemplacementNoeuds.py b/Tools/DiffRemplacementNoeuds.py index 0880ca4..eb1f7d3 100644 --- a/Tools/DiffRemplacementNoeuds.py +++ b/Tools/DiffRemplacementNoeuds.py @@ -1,83 +1,100 @@ -import re,sys,collections -def parse(p): - objs={}; pkg=None; key=None - for line in open(p,encoding='utf-8'): - line=line.rstrip('\n') - if line.startswith('##### '): pkg=line[6:]; continue - m=re.match(r'^ (\S+) \[(\w+)\]$',line) - if m: key=(pkg,m.group(1)); objs[key]={'cls':m.group(2),'lines':[]}; continue - if key: objs[key]['lines'].append(line) - return objs -A=parse(sys.argv[1]); B=parse(sys.argv[2]) -def graph(path): return path.split(':')[-1].split('.')[0] if ':' in path else '' -def iscopy(path): - g=graph(path); return "ExecuteUbergraph" in g or g.endswith("_MERGED") or re.fullmatch(r"EdGraph_[0-9]+",g) is not None -def pos(o): - x=y='0' - for l in o['lines']: - if l.startswith(' NodePosX = '): x=l.split('= ')[1] - if l.startswith(' NodePosY = '): y=l.split('= ')[1] - return x,y -old={k:v for k,v in A.items() if v['cls']=='K2Node_VariableGet' and not iscopy(k[1]) and any('Engine.SkinnedMeshComponent\'",MemberName="SkeletalMesh"' in l for l in v['lines'])} -new={k:v for k,v in B.items() if v['cls']=='K2Node_CallFunction' and not iscopy(k[1]) and any('MemberName="GetSkeletalMesh"' in l and 'LPMeshCompat' in l for l in v['lines'])} -print('anciens noeuds',len(old),'nouveaux',len(new)) -idx={(k[0],k[1].rsplit('.',1)[0],pos(v)):k for k,v in new.items()} -ren={}; miss=0 -for k,v in old.items(): - t=idx.get((k[0],k[1].rsplit('.',1)[0],pos(v))) - if not t: miss+=1; continue - ren[(k[0],graph(k[1]),k[1].rsplit('.',1)[1])]=t[1].rsplit('.',1)[1] -print('apparies',len(ren),'non apparies',miss) -skip=re.compile(r'^ (NodeGuid|GraphGuid|ErrorType|ErrorMsg|bHasCompilerMessage|NodePosX|NodePosY) = ') -def norm(pkg,g,lines): - out=[] - for l in lines: - if skip.match(l): continue - for (p,gg,o),n in ren.items(): - if p==pkg and gg==g: - l=l.replace(o+'.SkeletalMesh',n+'.ReturnValue').replace(o+'.self',n+'.Component') - out.append(canon(l,pkg,g)) - return out -def canon(l,pkg=None,g=None): - if l.startswith(' pin ') and ' -> ' in l: - head,links=l.rsplit(' -> ',1) - return head+' -> '+', '.join(sorted(x for x in links.split(', ') if x)) - if l.startswith(' Nodes = ('): - items=[x.strip('"') for x in l[len(' Nodes = ('):-1].split('","')] - if pkg is not None: - items=[x for x in items if not any(pkg==p2 and ("."+o+"'") in x and (":"+gg+".") in x for (p2,gg,o) in ren)] - items+=[ "NOUVEAU:"+pkg+":"+gg+"."+n for (p2,gg,o),n in ren.items() if p2==pkg and gg==g ] - else: - items=[("NOUVEAU:"+x.split("'/Game/")[0] if False else x) for x in items] - return ' Nodes = SET'+str(sorted(items)) - return l -newnames={(k[0],graph(k[1]),k[1].rsplit('.',1)[1]) for k in new} -def canonB(k,l): - if l.startswith(' Nodes = ('): - items=[x.strip('"') for x in l[len(' Nodes = ('):-1].split('","')] - keep=[]; add=[] - for x in items: - nm=x.rsplit('.',1)[1].rstrip("'") - if (k[0],k[1].split(':')[-1],nm) in newnames: add.append("NOUVEAU:"+k[0]+":"+k[1].split(':')[-1]+"."+nm) - else: keep.append(x) - return ' Nodes = SET'+str(sorted(keep+add)) - return canon(l) -diffs=collections.Counter(); ex=[] -for k in set(A)|set(B): - if iscopy(k[1]) or k in old or k in new: continue - if k[1].startswith('PLACEHOLDER'): continue - a=norm(k[0],graph(k[1]),A[k]['lines']) if k in A else None; b=[canonB(k,l) for l in B[k]['lines'] if not skip.match(l)] if k in B else None - if a!=b: - cls=(A.get(k) or B.get(k))['cls'] - diffs[(k[0],cls,'absent avant' if a is None else 'absent apres' if b is None else 'modifie')]+=1 - if len(ex)<40: ex.append((k,[x for x in (a or []) if x not in (b or [])][:2],[x for x in (b or []) if x not in (a or [])][:2])) -print('objets en ecart hors noeuds remplaces et copies :',sum(diffs.values())) -for k,v in sorted(diffs.items()): print(' ',v,*k) -for e in ex: print(e[0]); [print(' -',x[:160]) for x in e[1]]; [print(' +',x[:160]) for x in e[2]] +import re,sys,collections +def parse(p): + objs={}; pkg=None; key=None + for line in open(p,encoding='utf-8'): + line=line.rstrip('\n') + if line.startswith('##### '): pkg=line[6:]; continue + m=re.match(r'^ (\S+) \[(\w+)\]$',line) + if m: key=(pkg,m.group(1)); objs[key]={'cls':m.group(2),'lines':[]}; continue + if key: objs[key]['lines'].append(line) + return objs +# Profils de remplacement : selection des anciens et nouveaux noeuds, correspondance des broches. +# Usage : DiffRemplacementNoeuds.py avant.txt apres.txt [L7a|L7b] (L7a par defaut) +PROFILS={ + 'L7a':{'old':('K2Node_VariableGet',lambda l:'Engine.SkinnedMeshComponent\'",MemberName="SkeletalMesh"' in l), + 'new':('K2Node_CallFunction',lambda l:'MemberName="GetSkeletalMesh"' in l and 'LPMeshCompat' in l), + 'pins':[('self','Component'),('SkeletalMesh','ReturnValue')]}, + 'L7b':{'old':('K2Node_CallFunction',lambda l:'MemberName="K2_GetRandomPointInNavigableRadius"' in l), + 'new':('K2Node_CallFunction',lambda l:'MemberName="GetRandomLocationInNavigableRadius"' in l and 'LPNavigationHelpers' in l), + 'pins':[(x,x) for x in ('WorldContextObject','Origin','RandomLocation','Radius','NavData','FilterClass','ReturnValue')]}, +} +PROFIL=PROFILS[sys.argv[3] if len(sys.argv)>3 else 'L7a'] +A=parse(sys.argv[1]); B=parse(sys.argv[2]) +def graph(path): return path.split(':')[-1].split('.')[0] if ':' in path else '' +def iscopy(path): + g=graph(path); return "ExecuteUbergraph" in g or g.endswith("_MERGED") or re.fullmatch(r"EdGraph_[0-9]+",g) is not None +def pos(o): + x=y='0' + for l in o['lines']: + if l.startswith(' NodePosX = '): x=l.split('= ')[1] + if l.startswith(' NodePosY = '): y=l.split('= ')[1] + return x,y +def select(D,sel): return {k:v for k,v in D.items() if v['cls']==sel[0] and not iscopy(k[1]) and any(sel[1](l) for l in v['lines'])} +old=select(A,PROFIL['old']); new=select(B,PROFIL['new']) +print('anciens noeuds',len(old),'nouveaux',len(new)) +idx={(k[0],k[1].rsplit('.',1)[0],pos(v)):k for k,v in new.items()} +ren={}; miss=0 +for k,v in old.items(): + t=idx.get((k[0],k[1].rsplit('.',1)[0],pos(v))) + if not t: miss+=1; continue + ren[(k[0],graph(k[1]),k[1].rsplit('.',1)[1])]=t[1].rsplit('.',1)[1] +print('apparies',len(ren),'non apparies',miss) +skip=re.compile(r'^ (NodeGuid|GraphGuid|ErrorType|ErrorMsg|bHasCompilerMessage|NodePosX|NodePosY) = ') +def norm(pkg,g,lines): + out=[] + for l in lines: + if skip.match(l): continue + for (p,gg,o),n in ren.items(): + if p==pkg and gg==g: + for op,np_ in PROFIL['pins']: l=l.replace(o+'.'+op,n+'.'+np_) + out.append(canon(l,pkg,g)) + return out +def canon(l,pkg=None,g=None): + if l.startswith(' pin ') and ' -> ' in l: + head,links=l.rsplit(' -> ',1) + return head+' -> '+', '.join(sorted(x for x in links.split(', ') if x)) + if l.startswith(' Nodes = ('): + items=[x.strip('"') for x in l[len(' Nodes = ('):-1].split('","')] + if pkg is not None: + items=[x for x in items if not any(pkg==p2 and ("."+o+"'") in x and (":"+gg+".") in x for (p2,gg,o) in ren)] + items+=[ "NOUVEAU:"+pkg+":"+gg+"."+n for (p2,gg,o),n in ren.items() if p2==pkg and gg==g ] + else: + items=[("NOUVEAU:"+x.split("'/Game/")[0] if False else x) for x in items] + return ' Nodes = SET'+str(sorted(items)) + return l +newnames={(k[0],graph(k[1]),k[1].rsplit('.',1)[1]) for k in new} +def canonB(k,l): + if l.startswith(' Nodes = ('): + items=[x.strip('"') for x in l[len(' Nodes = ('):-1].split('","')] + keep=[]; add=[] + for x in items: + nm=x.rsplit('.',1)[1].rstrip("'") + if (k[0],k[1].split(':')[-1],nm) in newnames: add.append("NOUVEAU:"+k[0]+":"+k[1].split(':')[-1]+"."+nm) + else: keep.append(x) + return ' Nodes = SET'+str(sorted(keep+add)) + return canon(l) +diffs=collections.Counter(); ex=[] +for k in set(A)|set(B): + if iscopy(k[1]) or k in old or k in new: continue + if k[1].startswith('PLACEHOLDER'): continue + a=norm(k[0],graph(k[1]),A[k]['lines']) if k in A else None; b=[canonB(k,l) for l in B[k]['lines'] if not skip.match(l)] if k in B else None + if a!=b: + cls=(A.get(k) or B.get(k))['cls'] + diffs[(k[0],cls,'absent avant' if a is None else 'absent apres' if b is None else 'modifie')]+=1 + if len(ex)<40: ex.append((k,[x for x in (a or []) if x not in (b or [])][:2],[x for x in (b or []) if x not in (a or [])][:2])) +print('objets en ecart hors noeuds remplaces et copies :',sum(diffs.values())) +for k,v in sorted(diffs.items()): print(' ',v,*k) +for e in ex: print(e[0]); [print(' -',x[:160]) for x in e[1]]; [print(' +',x[:160]) for x in e[2]] def pinlinks(o,name): for l in o['lines']: - m=re.match(r'^ pin (\S+) \[.*\] defaut=.* -> (.*)$',l) - if m and m.group(1)==name: return sorted(x for x in m.group(2).split(', ') if x) + m=re.match(r'^ pin (\S+) (\[.*\] defaut=.*) -> (.*)$',l) + if m and m.group(1)==name: return sorted(x for x in m.group(3).split(', ') if x) + return None +def pinhead(o,name): + # Categorie et valeurs par defaut de la broche (sans les liaisons). + for l in o['lines']: + m=re.match(r'^ pin (\S+) (\[.*\] defaut=.*) -> (.*)$',l) + if m and m.group(1)==name: return m.group(2) return None ok=bad=0 for k,v in old.items(): @@ -90,7 +107,10 @@ for k,v in old.items(): if p2==k[0] and gg==g: x=x.replace(o+'.',nn+'.') if x.split('.')[0]==o else x out.append(x) return sorted(out) - if rn(pinlinks(v,'self'))==pinlinks(n,'Component') and rn(pinlinks(v,'SkeletalMesh'))==pinlinks(n,'ReturnValue'): ok+=1 + same=all(rn(pinlinks(v,op))==pinlinks(n,np_) for op,np_ in PROFIL['pins']) + # Profil a broches homonymes : les valeurs par defaut doivent aussi etre reportees. + if same and all(op==np_ for op,np_ in PROFIL['pins']): same=all(pinhead(v,op)==pinhead(n,op) for op,_ in PROFIL['pins']) + if same: ok+=1 else: - bad+=1; print('ECART',k[1],pinlinks(v,'self'),pinlinks(n,'Component'),pinlinks(v,'SkeletalMesh'),pinlinks(n,'ReturnValue')) + bad+=1; print('ECART',k[1],[(op,pinhead(v,op),rn(pinlinks(v,op)),pinhead(n,np_),pinlinks(n,np_)) for op,np_ in PROFIL['pins']]) print('noeuds remplaces : liaisons identiques',ok,'/ ecarts',bad)