L7b (CX-B11) : 6 noeuds GetRandomPointInNavigableRadius deprecies remplaces par LPNavigationHelpers (R-04, ERR-035)

Compilation 6 -> 0 avertissement, cuisson 3 -> 0 ; diff independant 6/6, 0 autre objet modifie.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
edujiro5972 2026-09-30 18:45:41 +02:00
parent 5739f3f4a5
commit c7f607f094
10 changed files with 465 additions and 85 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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

View file

@ -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<USkeletalMesh>(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é

View file

@ -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).

View file

@ -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 |

View file

@ -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<FString> Links(const UEdGraphPin* Pin)
{
TArray<FString> 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<FAssetRegistryModule>(TEXT("AssetRegistry"));
AssetRegistryModule.Get().SearchAllAssets(true);
TArray<FAssetData> BlueprintAssets;
// Sous-classes incluses : couvre les UWidgetBlueprint.
AssetRegistryModule.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), BlueprintAssets, true);
const UEdGraphSchema_K2* Schema = GetDefault<UEdGraphSchema_K2>();
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<UBlueprint>(AssetData.GetAsset());
if (!Blueprint)
{
continue;
}
TArray<UEdGraph*> 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<UEdGraphNode*> Snapshot = Graph->Nodes;
for (UEdGraphNode* GraphNode : Snapshot)
{
UK2Node_CallFunction* OldNode = Cast<UK2Node_CallFunction>(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<FName, FString> 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<UK2Node_CallFunction>(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("<absent>"));
}
++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;
}

View file

@ -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 <projet> -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;
};

View file

@ -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)