LifePlay-Revival/Docs/Migration/etat/U58/U1/U1-BP-58.md
edujiro5972 8084622450 LifePlay UE 5.8 : état de développement au 2026-10-19
Tickets : REP1-T4

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 22:57:42 +02:00

74 KiB
Raw Blame History

Fiche U1-T4c — Niveau N3 — Modèle prévu opus-xhigh | utilisé Opus 5.5 (claude-opus-5-5) — Agent lp-cart — Skills sk-protocol, ue-cpp-build, lp-commandlets, lp-paths (préchargés) ; lp-ue-compat (invoqué : outil Skill disponible)

U1-T4c — Membres natifs des Blueprints : état en UE 5.8 (CART, lecture seule)

Légende : établi = lu dans les sources ou rejoué (fichier:ligne, commande) ; estimé = déduit d'éléments établis (raisonnement donné) ; incertain = non vérifié (l'essai qui trancherait est nommé). Les preuves UE_5.x/Engine/... sont relatives à D:\Game\Launcher\Engine\. Données : U1-BP-58.csv (même dossier, 561 lignes, séparateur ;, UTF-8).

0. Résultat

Classement complet : 498 membres + 63 classes de nœud, 0 incertain, 0 retiré, 0 redirigé, 0 membre déprécié. 15 fonctions ont une signature changée (931 nœuds) et 2 classes de nœud ont un mécanisme de dépréciation nouveau (38 nœuds). Aucune de ces différences ne casse un nœud en 5.8 d'après les sources (estimé) : nœuds à corriger en U5 : 0 ; nœuds à surveiller avant un passage en 5.9 : 921 (fonctions de tableau, § 4.1).

Statut 5.8 Fonctions Propriétés Membres Usages (nœuds) Classes de nœud Nœuds
inchangé 411 47 458 13 835 61 46 579
signature changée 15 0 15 931 0 0
déprécié 0 0 0 0 2 38
redirigé 0 0 0 0 0 0
retiré 0 0 0 0 0 0
projet 23 2 25 696 — —
incertain 0 0 0 0 0 0
Total 449 49 498 15 462 63 46 617

Recomptage (seconde formulation, sk-recount) : Import-Csv … -Delimiter ';' | Group-Object type,statut sur le CSV livré → 561 lignes ; fonction 411 / 15 / 23, propriété 47 / 2, classe de nœud 61 / 2 ; somme des usages non « inchangé/projet » = 931 ; lignes sans preuve 5.8 hors « projet » = 0. Les totaux recoupent summary.txt (15 462 références natives, 46 617 nœuds, fonctions 449, propriétés 49, 63 classes).

1. Environnement vérifié (établi)

  • Moteurs : UE_5.5/Engine/Build/Build.version = 5.5.4-40574608 ; UE_5.8/Engine/Build/Build.version = 5.8.3-58210709 (++UE5+Release-5.8). Sources présentes (Engine/Source, Engine/Plugins/*/Source, dont Plugins/Animation/ControlRig/Source/ControlRigDeveloper), UHT C# présent (Source/Programs/Shared/EpicGames.UHT).
  • Entrées : Saved/Audit/BlueprintCalls/calls.json (561 entrées : 449 fonctions, 49 propriétés, 63 nœuds), node_classes.json, per_asset.csv, summary.txt (5.5.4, 110 Blueprints, 0 échec, 0 non résolu, 0 déprécié en 5.5).
  • Outils : Python 3.14.4 (python -I), ripgrep 14.1.1 ; scripts dans le dossier temporaire de session (annexe).
  • Dépôts (avant / après, inchangés par cette fiche) : jeu M Docs/Migration/27_Plan_Migration_UE58.md (antérieur) ; addon M LPModTools.Build.cs, ?? LPAuditBlueprintCallsCommandlet.{h,cpp}, ?? Tools/claude/ (U1-T4a) ; Modules propre. Les deux livrables sont sous /Docs/Migration/etat/, ignoré (git check-ignore -v → .gitignore:74).

2. Méthode

  1. Index des modules (rg --files -g '*.Build.cs', 5.5 et 5.8) ; en-tête = dossier du module + entete (ModuleRelativePath) ; si la classe n'y est pas : recherche de sa définition dans tout Source + Plugins (.h). Résultat : 535/535 membres et classes moteur trouvés dans le même en-tête en 5.8 (colonne localisation58).
  2. Analyse C++ (commentaires et préprocesseur effacés) : corps de la classe, chaque UFUNCTION(...) / UPROPERTY(...) (spécificateurs, meta), UE_DEPRECATED avant ou après la macro, déclaration (retour, static, const, paramètres : type, nom, UPARAM, défaut ; propriétés : type, champ de bits). Même analyse en 5.5 (contrôle : 0 membre introuvable en 5.5, donc l'analyseur retrouve tout ce que l'audit a résolu).
  3. Comparaison 5.5 → 5.8 : pur / appelable (BlueprintPure, const+BlueprintCallable), type d'événement, static, retour, liste des paramètres (un ajout final avec défaut est signalé « non cassant »), méta qui changent les broches (WorldContext, HidePin, AutoCreateRefTerm, ArrayParm, DeprecatedFunction…) ; propriétés : type (TObjectPtr<T> ≡ T*), accès Blueprint (BlueprintReadOnly/ReadWrite/Getter/Setter/Assignable), méta de dépréciation, suffixe _DEPRECATED ; délégués (événements de composant, signature de délégué) : macro DECLARE_DYNAMIC_* comparée.
  4. Redirections : 4 204 entrées lues en 5.8, 3 204 en 5.5 ([CoreRedirects] de Engine/Config et Engine/Plugins/**/Config/*.ini + macros *_REDIRECT de CoreUObject/Private/UObject/CoreRedirects.cpp) ; module contrôlé quand OldName le précise ; « nouvelle » = absente de 5.5. Les autres fichiers C++ qui enregistrent des redirections (rg -l 'ECoreRedirectFlags::|_REDIRECT\(', 64 fichiers en 5.8) ne citent aucune de nos classes hors CoreRedirects.cpp (seul K2Node.cpp:164-167, tolérance de broche identique en 5.5).
  5. Classes de nœud : définition en 5.8, UCLASS(Deprecated), surcharge IsDeprecated() nouvelle, redirection.
  6. Jugement (étape 2, bp58_final.py) : les 17 cas non « inchangé » sont relus à la source ; la colonne casse porte le verdict, casse_auto le verdict automatique, justification les preuves.

Règles du moteur sur lesquelles repose le classement (établi) :

Règle Preuve
UE_DEPRECATED devant une UFUNCTION est ignoré par UHT (pas de méta DeprecatedFunction, nœud intact) UE_5.8/Engine/Source/Programs/Shared/EpicGames.UHT/Parsers/UhtFunctionParser.cs:371 (OptionalAttributes(false) sans action) ; Tokenizer/UhtTokenReaderGeneralExtensions.cs:245-257
Propriété X_DEPRECATED → CPF_Deprecated, nom réfléchi X (sans suffixe) UhtPropertyParser.cs:952-959 ; CoreUObject/Private/UObject/Property.cpp:1011
Une redirection ne s'applique que si le membre n'existe plus dans la portée Runtime/Engine/Private/MemberReference.cpp:536-540 (5.5 et 5.8)
Nom de broche = nom du paramètre ; UPARAM(DisplayName) = nom affiché seulement Editor/BlueprintGraph/Private/K2Node_CallFunction.cpp:1203, 1209-1212 (5.8), :1201 (5.5)
Nœuds Get/Set : seuls BlueprintGetter/BlueprintSetter détournent l'accès ; Getter/Setter natifs sans effet EdGraphSchema_K2.cpp:199-200 ; K2Node_VariableGet.cpp:603 ; K2Node_VariableSet.cpp:501-505 (« todo check … native Setter »)
Broche WorldContext cachée et remplie par le contexte K2Node_CallFunction.cpp:1243-1253 (5.8)

3. Comptes par statut

Voir § 0. Détail « projet » (25 membres, 696 usages, hors moteur, non analysés ici) : LifePlay 15 membres / 594 usages (ULPStringCompat ×4, ULPAssetRegistryHelpers ×3, ULPDataCompat ×2, ULPVictoryCompat ×2, ULPMeshCompat, ULPNavigationHelpers, ULPModDailiesLibrary, ULPModSettingsLibrary) ; ModdingExtra 3 / 80 ; StreetMapRuntime 7 / 22 (plugin du projet Plugins/StreetMap, absent de UE_5.8/Engine/Plugins : rg --files -g 'StreetMap*.uplugin' vide ; sa compatibilité relève de U4). LPAnimCore : aucun membre utilisé par les Blueprints.

4. Tout ce qui n'est pas « inchangé » (17 lignes)

4.1 Signature changée — 15 fonctions, 931 nœuds

Membre Écart 5.5 → 5.8 Preuve 5.8 Preuve 5.5 Nœuds Assets Casse en 5.8
UKismetArrayLibrary::Array_Add const TArray<int32>& TargetArray → UPARAM(ref) TArray<int32>& TargetArray UE_5.8/.../Engine/Classes/Kismet/KismetArrayLibrary.h:37 UE_5.5/…/KismetArrayLibrary.h:37 304 18 non (estimé)
UKismetArrayLibrary::Array_AddUnique idem KismetArrayLibrary.h:47 :47 81 8 non (estimé)
UKismetArrayLibrary::Array_Shuffle idem KismetArrayLibrary.h:55 :55 57 4 non (estimé)
UKismetArrayLibrary::Array_Append idem (SourceArray reste const) KismetArrayLibrary.h:83 :83 59 5 non (estimé)
UKismetArrayLibrary::Array_Insert idem KismetArrayLibrary.h:93 :93 9 3 non (estimé)
UKismetArrayLibrary::Array_Remove idem KismetArrayLibrary.h:103 :103 33 4 non (estimé)
UKismetArrayLibrary::Array_RemoveItem idem KismetArrayLibrary.h:113 :113 89 9 non (estimé)
UKismetArrayLibrary::Array_Clear idem KismetArrayLibrary.h:121 :121 206 16 non (estimé)
UKismetArrayLibrary::Array_Resize idem KismetArrayLibrary.h:130 :130 10 4 non (estimé)
UKismetArrayLibrary::Array_Set idem KismetArrayLibrary.h:196 :196 70 7 non (estimé)
UKismetArrayLibrary::Array_Swap idem KismetArrayLibrary.h:207 :207 3 2 non (estimé)
UKismetSystemLibrary::DrawDebugBox paramètre final ajouté avec défaut EDrawDebugSceneDepthPriorityGroup DepthPriority = …::World UE_5.8/…/Kismet/KismetSystemLibrary.h:1680 :1561 2 2 non
UKismetSystemLibrary::DrawDebugPoint idem KismetSystemLibrary.h:1672 :1553 1 1 non
UWidgetLayoutLibrary::GetMousePositionOnViewport UObject* WorldContextObject → const UObject* UE_5.8/Engine/Source/Runtime/UMG/Public/Blueprint/WidgetLayoutLibrary.h:117 :97 4 2 non (estimé)
UWidgetLayoutLibrary::GetViewportSize idem WidgetLayoutLibrary.h:89 :69 3 3 non (estimé)

Fonctions de tableau (11, 921 nœuds) — pourquoi « non » en 5.8, et pourquoi « à surveiller » :

  • la broche TargetArray est déjà forcée en référence en 5.5 comme en 5.8 (Editor/BlueprintGraph/Private/K2Node_CallArrayFunction.cpp:45 en 5.5, :47 en 5.8) ; son nom vient du paramètre (K2Node_CallFunction.cpp:1201 / :1203) et ne change pas : reconstruction du nœud sans perte de lien (estimé) ;
  • un tableau en lecture seule branché sur ces fonctions déclencherait en 5.8 l'erreur « Cannot pass a read-only variable to a reference parameter » (CallFunctionHandler.cpp:405-421), sauf pour les fonctions de la liste AllowedMutableContainerFunctions : les 11 y sont (UE_5.8/Engine/Config/BaseEditor.ini:203-215), et l'avertissement « Array is changing read-only data … will become an error in a future engine release » n'est émis que si bEnableMutableContainerWarnings (EdGraphSchema_K2.h:474, CallFunctionHandler.cpp:414-416) est vrai : aucun réglage dans le moteur ni dans Config/ des copies 5.5 et 5.8 (rg MutableContainer vide ; 3 occurrences moteur, aucune affectation → false, estimé) ;
  • le commentaire BaseEditor.ini:197-202 (« REMOVE IN UE 5.9 … could result in Blueprint compilation errors if anyone mistakenly connected a read-only container ») annonce l'erreur pour la version suivante. Mécanisme absent de 5.5 (rg AllowedMutableContainerFunctions|bEnableMutableContainerWarnings vide dans UE_5.5).

WorldContext : broche cachée et remplie par le contexte (K2Node_CallFunction.cpp:1243-1253), méta WorldContext inchangée (WidgetLayoutLibrary.h:88 / 116 en 5.8), seul const est ajouté au pointeur.

4.2 Déprécié — 2 classes de nœud, 38 nœuds (tous dans /Game/BPs/PC)

Classe Écart Preuve 5.8 Preuve 5.5 Nœuds Casse en 5.8
UK2Node_InputAction nouvelles surcharges IsDeprecated / HasDeprecatedReference / GetDeprecationResponse UE_5.8/Engine/Source/Editor/BlueprintGraph/Classes/K2Node_InputAction.h:64-66 ; Private/K2Node_InputAction.cpp:132-164 UE_5.5/…/K2Node_InputAction.h:32 (sans surcharge) 32 non (conditionnel, inactif)
UK2Node_InputAxisEvent idem K2Node_InputAxisEvent.h:56-58 ; K2Node_InputAxisEvent.cpp:154-186 UE_5.5/…/K2Node_InputAxisEvent.h:26 6 non (conditionnel, inactif)

Condition (établi) : IsDeprecated() n'est vrai que si UInputSettings::bEnabledLegacyMappingDeprecationWarnings (vrai par défaut, Runtime/Engine/Private/UserInterface/InputSettings.cpp:48 ; InputSettings.h:116-120) et si le nom de la correspondance figure dans DeprecatedActionAndAxisNames (InputSettings.h:208, InputSettings.cpp:356-359) ; message de type avertissement (K2Node_InputAction.cpp:156). rg DeprecatedActionAndAxisNames dans Config/ des deux copies : vide → aucun avertissement. Asset : /Game/BPs/PC seul (rg -l -a "(?-u:K2Node_InputAction\x00)" et K2Node_InputAxisEvent sur Content/**/*.uasset → BPs/PC.uasset ; 1 asset chacun dans node_classes.json).

4.3 Redirigé, retiré, incertain

Aucun. Aucune FunctionRedirects/PropertyRedirects/ClassRedirects nouvelle en 5.8 ne vise un de nos membres ; celles qui les citent existent déjà en 5.5 et ne s'appliquent pas (membre toujours déclaré, règle § 2) : voir § 5.

5. « Inchangé » avec un écart non cassant (relu, pour mémoire)

Membre / classe Écart Preuve
UKismetMathLibrary::FMod redirection FMod → FMod64 présente en 5.5 et 5.8, FMod toujours déclaré : sans effet UE_5.8/Engine/Config/BaseEngine.ini:1343 ; 5.5 BaseEngine.ini:1305 ; KismetMathLibrary.h:816
UKismetMathLibrary::BreakTransform redirection de broche BreakTransform.Translation → Location (ancienne, 5.5 aussi) CoreUObject/Private/UObject/CoreRedirects.cpp:3549
AAIController::GetMoveStatus, MoveToLocation CLASS_REDIRECT("AIController", "/Script/AIModule.AIController") (ancienne) ; défaut FilterClass NULL → {} CoreRedirects.cpp:3808 ; AIController.h:187
UKismetMathLibrary::RandomInteger UPARAM(DisplayName = "Exclusive Max") int32 Max : nom affiché seulement KismetMathLibrary.h:380 (5.8)
UKismetSystemLibrary::GetObjectName méta BlueprintThreadSafe ajoutée (permissive) KismetSystemLibrary.h:136
PrintString, JoinStringArray, K2_GetRandomReachablePointInRadius écriture du défaut (FString(TEXT("x")) → FString("x"), NULL → {}), valeur identique KismetSystemLibrary.h:566, KismetStringLibrary.h:381, NavigationSystem.h:511
UPrimitiveComponent::GetMaterial, GetNumMaterials override ajouté (interface C++ IPhysicsBodyInstanceOwner, pas UINTERFACE) ; UFUNCTION toujours dans UPrimitiveComponent PrimitiveComponent.h:1566, 2326 ; PhysicsEngine/PhysicsBodyInstanceOwnerInterface.h:38, 136, 139
APlayerController::bShowMouseCursor Setter=SetShowMouseCursor natif ajouté ; le nœud Set écrit toujours le champ directement PlayerController.h:526-527 (5.8) / :527-528 (5.5) ; règle § 2
UEditableTextBox::WidgetStyle Getter, Setter natifs ajoutés (sans UE_DEPRECATED) : idem ; signe probable d'une dépréciation C++ future de l'accès direct (estimé) EditableTextBox.h:51-52 (5.8)
UButton::BackgroundColor, ColorAndOpacity, UBorder::ContentColorAndOpacity, UImage::Brush, ColorAndOpacity, USkeletalMesh::Materials, MorphTargets UE_DEPRECATED C++ déjà en 5.5, sans effet Blueprint ex. Button.h:44, 49 (5.5) / :45, 50 (5.8) ; SkeletalMesh.h:905, 1912 (5.5)
12 événements de composant UMG (OnClicked…OnValueCommitted) délégués identiques (macro et paramètres) ex. Button.h:18-21, CheckBox.h:20, SpinBox.h:24-25 (5.8)
ControlRigGraphNode, AnimGraphNode_ControlRig, EdGraphNode_Comment redirections de classe d'un ancien module, présentes en 5.5 Plugins/Animation/ControlRig/Config/DefaultControlRig.ini:4, 6 (5.5 BaseControlRig.ini:4, 6) ; CoreRedirects.cpp:4003
AnimStateTransitionNode (8 nœuds) BlendProfile → BlendProfile_DEPRECATED (5.6), migré automatiquement au chargement AnimStateTransitionNode.h:52-54 ; AnimStateTransitionNode.cpp:118, 164-169
UActorComponent, UPrimitiveComponent, USkinnedMeshComponent interfaces C++ ajoutées à la liste des bases ActorComponent.h:160, PrimitiveComponent.h:307, SkinnedMeshComponent.h:267
UAnimNotify UCLASS(…, editinlinenew, …) ajouté ; l'événement Received_Notify surchargé est inchangé Animation/AnimNotifies/AnimNotify.h:51

Contrôle croisé indépendant (sans l'analyseur, xcheck.py) : comparaison textuelle normalisée du bloc UFUNCTION/UPROPERTY … ; 5.5 / 5.8 pour les 472 membres moteur non-délégués → 445 identiques, 27 différents ; les 27 sont exactement les 15 de § 4.1 + les 12 écarts textuels de § 5 (défauts, override, mot-clé class, UPARAM, méta, accesseurs natifs). uclass_check.py : 70 classes porteuses, 4 UCLASS/bases différentes (lignes ci-dessus), aucune classe dépréciée.

6. Échantillon de 10 « inchangé » (méthode)

Membre Contexte Usages Déclaration 5.5 Déclaration 5.8
AActor::K2_GetActorLocation appel 40 GameFramework/Actor.h:1496 ENGINE_API FVector K2_GetActorLocation() const; Actor.h:1568 FVector K2_GetActorLocation() const; (ENGINE_API retiré, sans effet Blueprint)
AActor::ReceiveBeginPlay événement 14, appel 1 15 Actor.h:2050 Actor.h:2122 (même texte)
UAnimInstance::BlueprintUpdateAnimation événement 7, appel 1 8 Animation/AnimInstance.h:518 (float DeltaTimeX) AnimInstance.h:564 (même)
UKismetMathLibrary::FMod appel 12 Kismet/KismetMathLibrary.h:815 int32 FMod(double, double, double& Remainder) KismetMathLibrary.h:816 (même ; redirection § 5 sans effet)
UKismetStringLibrary::Conv_StringToInt appel 55 Kismet/KismetStringLibrary.h:105 KismetStringLibrary.h:105
UGameplayStatics::SaveGameToSlot appel 4 Kismet/GameplayStatics.h:1168 (USaveGame*, const FString&, const int32) GameplayStatics.h:1167
USceneComponent::K2_SetWorldLocation appel 3 Components/SceneComponent.h:509 SceneComponent.h:560
UWidget::SetVisibility appel 285 UMG/Public/Components/Widget.h:584 virtual void SetVisibility(ESlateVisibility) Widget.h:590
ACharacter::Mesh get 262 GameFramework/Character.h:255 TObjectPtr<USkeletalMeshComponent> Mesh; (BlueprintReadOnly, AllowPrivateAccess) Character.h:352
UButton::OnClicked événement de composant 420 UMG/Public/Components/Button.h:75 + délégué :17 Button.h:76 + délégué DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnButtonClickedEvent) :18

(Chemins UE_5.x/Engine/Source/Runtime/Engine/Classes/… sauf UMG UE_5.x/Engine/Source/Runtime/UMG/Public/….)

7. Assets touchés (25 assets, 969 nœuds « à surveiller », 0 à corriger en 5.8)

Répartition exacte par asset (pernode.py, lecture seule des .uasset : FName du membre comptée comme valeur d'un tag MemberName dans les données d'export ; contrôle : pour chaque membre la somme par asset = usages de calls.json, 13/13 exactes ; GetViewportSize partage son FName avec APlayerController::GetViewportSize, répartition déduite des listes d'assets, 1 nœud par asset). Méthode estimée, totaux établis.

Asset Nœuds dont tableau À corriger en 5.8 Détail
/Game/BPs/GI 514 514 0 Array_Add 152, Array_AddUnique 61, Array_Append 16, Array_Clear 115, Array_Insert 4, Array_Remove 23, Array_RemoveItem 56, Array_Resize 3, Array_Set 49, Array_Shuffle 33, Array_Swap 2
/Game/BPs/Person 206 206 0 Array_Add 65, Array_AddUnique 7, Array_Append 38, Array_Clear 50, Array_Insert 3, Array_Remove 3, Array_RemoveItem 10, Array_Resize 5, Array_Set 8, Array_Shuffle 16, Array_Swap 1
/Game/BPs/Room 59 57 0 Array_Add 27, Array_AddUnique 1, Array_Clear 11, Array_Insert 2, Array_Remove 6, Array_RemoveItem 6, Array_Shuffle 4, DrawDebugBox 1, DrawDebugPoint 1
/Game/BPs/PC 40 0 0 GetMousePositionOnViewport 1, GetViewportSize 1, K2Node_InputAction 32, K2Node_InputAxisEvent 6
/Game/BPs/StreetMap_Static 38 38 0 Array_Add 16, Array_Append 1, Array_Clear 8, Array_Remove 1, Array_RemoveItem 4, Array_Resize 1, Array_Set 3, Array_Shuffle 4
/Game/UI/ContactMenu 37 37 0 Array_Add 10, Array_AddUnique 6, Array_Clear 6, Array_RemoveItem 8, Array_Resize 1, Array_Set 6
/Game/BPs/MapMarkers 12 12 0 Array_Add 7, Array_Append 1, Array_Clear 4
/Game/UI/TopRight 12 12 0 Array_Add 7, Array_Append 3, Array_Clear 1, Array_Set 1
/Game/UI/CharGen 9 9 0 Array_Add 3, Array_AddUnique 2, Array_Clear 2, Array_RemoveItem 2
/Game/UI/Inventory 7 7 0 Array_Add 5, Array_Clear 2
/Game/BPs/Birdview 4 2 0 Array_Add 1, Array_Clear 1, DrawDebugBox 1, GetViewportSize 1
/Game/UI/Cursor 4 0 0 GetMousePositionOnViewport 3, GetViewportSize 1
/Game/UI/SexAction 4 4 0 Array_Add 3, Array_Clear 1
/Game/UI/BodyShapeMenu 3 3 0 Array_Add 1, Array_Set 2
/Game/UI/Inventory_Cloth 3 3 0 Array_Add 1, Array_AddUnique 2
/Game/BPs/MapStreets 2 2 0 Array_Add 2
/Game/BPs/Save 2 2 0 Array_Add 1, Array_Clear 1
/Game/UI/ColorSlot 2 2 0 Array_AddUnique 1, Array_Set 1
/Game/UI/Journal 2 2 0 Array_Add 1, Array_RemoveItem 1
/Game/UI/Recommend 2 2 0 Array_Add 1, Array_Clear 1
/Game/UI/Scene 2 2 0 Array_Add 1, Array_Clear 1
/Game/UI/VisualMorphPair 2 2 0 Array_AddUnique 1, Array_RemoveItem 1
/Game/UI/CityMenu 1 1 0 Array_Clear 1
/Game/UI/SettingCheckbox 1 1 0 Array_Clear 1
/Game/UI/VisualMorph 1 1 0 Array_RemoveItem 1
Total 969 921 0

Seuls les nœuds de tableau branchés sur une donnée en lecture seule deviendront des erreurs après 5.8 ; leur nombre n'est pas connu sans compiler (incertain, essai : § 8.1).

8. Proposition de méthode pour U5 (décision de l'orchestrateur / utilisateur)

  1. Fonctions de tableau (921 nœuds, 23 assets) — diagnostic, pas de correction en 5.8. Dans la copie 5.8 seulement et le temps d'un passage, Config/DefaultEditor.ini : [/Script/BlueprintGraph.EdGraphSchema_K2] bEnableMutableContainerWarnings=True, puis CompileAllBlueprints : chaque nœud branché sur un tableau en lecture seule produit « Array is changing read-only data @@ » (CallFunctionHandler.cpp:416) avec le nœud et l'asset. Retirer le réglage ensuite. Si la liste n'est pas vide : correction manuelle si < ~10 nœuds (copie dans une variable locale avant l'appel), sinon commandlet de recâblage sur le modèle de LPFixDeprecatedNodes (historique, tag ue5.5-final), lancé par Tools/ReimportCorrectif.ps1 (instantané + LPDumpFunction avant/après, -DryRun avec -ExpectCount = nombre d'avertissements, CompileAllBlueprints final). À faire avant tout passage en 5.9 ; inutile pour V2.0 en 5.8.
  2. DrawDebugBox / DrawDebugPoint (3 nœuds) : redirection automatique au sens large — la broche DepthPriority est créée à la reconstruction du nœud au chargement ; LPResavePackages de U5 la rend persistante. Rien d'autre.
  3. GetMousePositionOnViewport / GetViewportSize (7 nœuds) : rien (broche cachée, const seul).
  4. K2Node_InputAction / K2Node_InputAxisEvent (38 nœuds, /Game/BPs/PC) : rien en 5.8 (mécanisme inactif sans liste DeprecatedActionAndAxisNames). La migration vers Enhanced Input relève de la voie UE5 native (U9), pas de U5.
  5. Contrôle général U5 : LPResavePackages puis CompileAllBlueprints 5.8 comparé au relevé de référence 5.5 ; toute erreur ou avertissement de nœud absent de cette liste vient d'une zone non couverte (§ 9) → nouvelle fiche N3. Aucune CoreRedirects à ajouter au projet : aucun membre renommé ni retiré.

9. Limites et incertitudes

  • Non couvert par calls.json (U1-T4a § 5.6) : types portés par les broches, structures natives des Make/Break (ex. FHitResult, FTransform), cibles de DynamicCast, fabriques des nœuds asynchrones, CreateDelegate, liaisons UMG et Property Access, nœuds AnimGraph (FAnimNode_*), unités RigVM des 3 ControlRig (1 939 nœuds), macros moteur. Seule l'existence des 63 classes de nœud est établie. Essai qui tranche : CompileAllBlueprints 5.8 en U5.
  • Blueprints de niveau (2 .umap) : non couverts. Indice (estimé, table de noms) : Scenes/CentralLondon.umap
    contient des nœuds (K2Node_CallFunction, K2Node_InputKey, K2Node_Event…) et, parmi nos membres, seulement des
    « inchangé » (ReceiveBeginPlay, ReceiveTick, GetGameInstance, Delay, PrintString, PrintText…) ; aucun des
    17 membres/classes de § 4 (rg -l -a -g '*.umap' "(?-u:\x00<nom>\x00)" vide) ; FreeAnimationsPack/Map/Overview.umap
    aucun nœud. Un membre utilisé seulement par un niveau échappe à l'inventaire.
  • Comportement : seules les déclarations sont comparées, pas les implémentations (.cpp) ; un changement de comportement à signature égale (ex. SetShowMouseCursor contourné par le nœud Set, comme en 5.5) relève de U6.
  • bEnableMutableContainerWarnings = false par défaut : estimé (aucune affectation trouvée, membre UPROPERTY d'un UObject mis à zéro).
  • Répartition par asset (§ 7) : heuristique sur le binaire, validée par les totaux, non par le moteur.

10. Écarts à la fiche

  1. StreetMapRuntime (7 membres) classé « projet » : plugin du dépôt (Plugins/StreetMap), pas du moteur ; la fiche ne citait que LifePlay, ModdingExtra, LPAnimCore.
  2. « signature changée » regroupe cassant et non cassant ; la colonne casse tranche (0 cassant en 5.8).
  3. « déprécié » des 2 classes de nœud = mécanisme conditionnel nouveau, inactif pour le projet (§ 4.2).
  4. Lecture binaire des .uasset/.umap (lecture seule) ajoutée pour la répartition par asset et l'indice niveaux.
  5. Aucun éditeur, commandlet ni compilation lancé ; aucun fichier du projet, de l'addon ou du moteur modifié ; seuls U1-BP-58.md et U1-BP-58.csv écrits.

11. Commandes (rejouables)

S="<dossier temporaire>"; A="D:/Game/LifePlay-master 5.5/Saved/Audit/BlueprintCalls"
python -I "$S/bp58.py" "$A/calls.json" "$A/node_classes.json" "$S/out2"            # classement automatique (~6 min, D: USB)
python -I "$S/pernode.py" "$A/calls.json" "D:/Game/LifePlay-master 5.5/Content" Array_Add,…,GetViewportSize   # -> pernode.json
python -I "$S/bp58_final.py" "$S/out2/U1-BP-58.csv" "D:/Game/LifePlay-master 5.5/Docs/Migration/etat/U58/U1/U1-BP-58.csv"
python -I "$S/xcheck.py" "$S/out2/U1-BP-58.csv"; python -I "$S/uclass_check.py" "$A/calls.json"   # contrôles croisés

Colonnes du CSV : membre;type;classe;module;statut;casse;preuve58;preuve55;usages;nb_assets;assets;contextes;entete; localisation58;detail;casse_auto;justification (assets = liste complète, toutes ≤ 20 pour les lignes de § 4).

12. Handoff

Suite : l'orchestrateur décide du diagnostic § 8.1 (réglage temporaire dans la copie 5.8) et le confie à la fiche U5 ; U5 rejoue CompileAllBlueprints 5.8 pour les zones non couvertes (§ 9). État : cartographie close, aucun correctif nécessaire en 5.8 pour les nœuds relevés.

Annexe — scripts utilisés (copie exacte)

bp58.py

# U1-T4c : classement 5.8 des membres natifs utilises par les Blueprints (lecture seule).
# Usage : python -I bp58.py <calls.json> <node_classes.json> <sortie_dir>
# Lit les sources UE 5.5 et 5.8 ; n'ecrit que dans <sortie_dir>.
import json, os, re, sys, subprocess, csv, collections

E5 = 'D:/Game/Launcher/Engine/UE_5.5/Engine'
E8 = 'D:/Game/Launcher/Engine/UE_5.8/Engine'
PROJ = 'D:/Game/LifePlay-master 5.5'
RG = os.environ.get('CLAUDE_CODE_EXECPATH') or r'%USERPROFILE%\.local\bin\claude.exe'
PROJECT_MODULES = {'LifePlay', 'ModdingExtra', 'LPAnimCore', 'StreetMapRuntime'}

def rg(args):
    env = dict(os.environ, ARGV0='rg')
    r = subprocess.run([RG] + args, env=env, capture_output=True, text=True, encoding='utf-8', errors='replace')
    return [l for l in r.stdout.splitlines() if l.strip()]

def norm(p):
    return p.replace('\\', '/')

# ---------------------------------------------------------------- modules
def module_index(E):
    idx = collections.defaultdict(list)
    for p in rg(['--files', '-g', '*.Build.cs', E + '/Source', E + '/Plugins']):
        p = norm(p)
        idx[os.path.basename(p)[:-len('.Build.cs')]].append(os.path.dirname(p))
    return idx

# ---------------------------------------------------------------- texte C++
_cache = {}
PARSED = {}
CLS = {}
def load(path):
    """Texte sans commentaires ni lignes de preprocesseur (meme longueur, retours conserves)."""
    if path in _cache:
        return _cache[path]
    raw = open(path, encoding='utf-8-sig', errors='replace').read().replace('\r\n', '\n')
    out = list(raw)
    i, n = 0, len(raw)
    st = 0  # 0 code, 1 chaine, 2 caractere, 3 //, 4 /* */
    while i < n:
        c = raw[i]
        if st == 0:
            if c == '"':
                st = 1
            elif c == "'":
                st = 2
            elif c == '/' and i + 1 < n and raw[i + 1] == '/':
                st = 3; out[i] = ' '
            elif c == '/' and i + 1 < n and raw[i + 1] == '*':
                st = 4; out[i] = ' '; out[i + 1] = ' '; i += 1
        elif st == 1:
            if c == '\\':
                i += 1
            elif c == '"':
                st = 0
        elif st == 2:
            if c == '\\':
                i += 1
            elif c == "'":
                st = 0
        elif st == 3:
            if c == '\n':
                st = 0
            else:
                out[i] = ' '
        elif st == 4:
            if c == '*' and i + 1 < n and raw[i + 1] == '/':
                out[i] = ' '; out[i + 1] = ' '; i += 1; st = 0
            elif c != '\n':
                out[i] = ' '
        i += 1
    txt = ''.join(out)
    # preprocesseur (avec continuations)
    lines = txt.split('\n')
    cont = False
    for k, l in enumerate(lines):
        if cont or l.lstrip().startswith('#'):
            cont = l.rstrip().endswith('\\')
            lines[k] = ' ' * len(l)
    txt = '\n'.join(lines)
    _cache[path] = (raw, txt)
    return _cache[path]

def lineno(txt, pos):
    return txt.count('\n', 0, pos) + 1

def match_paren(txt, i, o='(', c=')'):
    """i = position de l'ouvrant ; retourne la position du fermant correspondant."""
    d = 0; n = len(txt); st = 0
    while i < n:
        ch = txt[i]
        if st == 1:
            if ch == '\\': i += 1
            elif ch == '"': st = 0
        else:
            if ch == '"': st = 1
            elif ch == o: d += 1
            elif ch == c:
                d -= 1
                if d == 0: return i
        i += 1
    return -1

def split_top(s, sep=','):
    parts, d, cur, st = [], 0, [], 0
    for ch in s:
        if st == 1:
            cur.append(ch)
            if ch == '"': st = 0
            continue
        if ch == '"': st = 1
        if ch in '(<[{': d += 1
        elif ch in ')>]}': d -= 1
        if ch == sep and d == 0:
            parts.append(''.join(cur)); cur = []
        else:
            cur.append(ch)
    if ''.join(cur).strip(): parts.append(''.join(cur))
    return [p.strip() for p in parts if p.strip()]

def parse_specs(s):
    """'BlueprintPure, Category="X", meta=(A, B="c")' -> (dict cles minuscules -> valeur, dict meta)"""
    spec, meta = {}, {}
    for p in split_top(s):
        if '=' in p:
            k, v = p.split('=', 1); k = k.strip(); v = v.strip()
            if k.lower() == 'meta':
                v = v.strip()
                if v.startswith('(') and v.endswith(')'): v = v[1:-1]
                for q in split_top(v):
                    if '=' in q:
                        a, b = q.split('=', 1); meta[a.strip().lower()] = b.strip().strip('"')
                    else:
                        meta[q.strip().lower()] = ''
            else:
                spec[k.lower()] = v.strip('"')
        else:
            spec[p.lower()] = ''
    return spec, meta

def ntype(t):
    t = re.sub(r'\s+', ' ', t.strip())
    t = re.sub(r'\s*([*&<>,:])\s*', r'\1', t)
    t = re.sub(r'\b(class|struct|enum)\s+', '', t)
    return t

STRIP_WORDS = {'virtual', 'static', 'inline', 'FORCEINLINE', 'FORCENOINLINE', 'UE_API', 'MINIMALAPI', 'explicit',
               'constexpr', 'UE_NODISCARD', '[[nodiscard]]', 'UE_INTERNAL'}
ATTR_MACROS = ('UE_DEPRECATED', 'UE_DEPRECATED_FORGAME', 'UE_DEPRECATED_FORENGINE', 'DEPRECATED', 'UE_EXPERIMENTAL')

def skip_ws(txt, i):
    while i < len(txt) and txt[i].isspace(): i += 1
    return i

def eat_attr_macros(txt, i, found):
    while True:
        i = skip_ws(txt, i)
        m = re.match(r'(UE_DEPRECATED_FORGAME|UE_DEPRECATED_FORENGINE|UE_DEPRECATED|DEPRECATED|UE_EXPERIMENTAL)\s*\(', txt[i:i + 60])
        if m:
            j = txt.index('(', i)
            k = match_paren(txt, j)
            found.append(re.sub(r'\s+', ' ', txt[i:k + 1]))
            i = k + 1
            continue
        m = re.match(r'(\[\[nodiscard\]\]|UE_NODISCARD|UE_INTERNAL)\b', txt[i:i + 20])
        if m:
            i += len(m.group(0)); continue
        return i

def deprec_before(txt, start):
    """UE_DEPRECATED place avant la macro U* (commentaires deja effaces)."""
    seg = txt[max(0, start - 600):start]
    m = re.search(r'((?:UE_DEPRECATED\w*|DEPRECATED)\s*\((?:[^()]|\([^()]*\))*\))\s*$', seg)
    return [re.sub(r'\s+', ' ', m.group(1))] if m else []

def find_classes(txt, cls):
    """Positions (debut corps, fin corps, mot cle, base) des definitions de cls."""
    res = []
    pat = re.compile(r'\b(class|struct)\s+((?:[A-Za-z_][A-Za-z0-9_]*\s*(?:\([^()]*\))?\s+)*?)' + re.escape(cls) + r'\b\s*(final\s*)?(:[^;{]*)?\{')
    for m in pat.finditer(txt):
        ob = m.end() - 1
        cb = match_paren(txt, ob, '{', '}')
        res.append((ob, cb, m.group(1), (m.group(4) or '').strip(' :'), m.start()))
    return res

def access_at(txt, ob, pos, kw):
    seg = txt[ob:pos]
    ms = list(re.finditer(r'\b(public|protected|private)\s*:(?!:)', seg))
    if ms: return ms[-1].group(1)
    if re.search(r'GENERATED_UCLASS_BODY|GENERATED_USTRUCT_BODY|GENERATED_UINTERFACE_BODY', seg): return 'public'
    return 'private' if kw == 'class' else 'public'

def parse_ufunctions(txt, ob, cb, kw):
    out = {}
    for m in re.finditer(r'\bUFUNCTION\s*\(', txt[ob:cb]):
        s = ob + m.start()
        po = txt.index('(', s)
        pc = match_paren(txt, po)
        spec, meta = parse_specs(txt[po + 1:pc])
        dep = deprec_before(txt, s)
        i = eat_attr_macros(txt, pc + 1, dep)
        j = txt.index('(', i)
        head = txt[i:j]
        mname = re.search(r'([A-Za-z_][A-Za-z0-9_]*)\s*$', head)
        if not mname: continue
        name = mname.group(1)
        rtype_raw = head[:mname.start()]
        toks = [t for t in re.split(r'\s+', rtype_raw.strip()) if t and t not in STRIP_WORDS and not re.fullmatch(r'[A-Z0-9_]+_API', t)]
        static = 'static' in rtype_raw.split()
        virtual = 'virtual' in rtype_raw.split()
        rtype = ntype(' '.join(toks))
        k = match_paren(txt, j)
        params = []
        for p in split_top(txt[j + 1:k]):
            uparam = ''
            mu = re.match(r'UPARAM\s*\(', p)
            if mu:
                e = match_paren(p, p.index('('))
                uparam = re.sub(r'\s+', '', p[p.index('(') + 1:e]); p = p[e + 1:].strip()
            dflt = None
            pp = split_top(p, '=')
            if len(pp) > 1:
                p, dflt = pp[0], '='.join(pp[1:]).strip()
            mn = re.search(r'([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*$', p)
            if not mn:
                params.append({'type': ntype(p), 'name': '', 'def': dflt, 'uparam': uparam}); continue
            ptype = p[:mn.start()]
            if not ptype.strip():  # type sans nom
                ptype, pname = p, ''
            else:
                pname = mn.group(1)
            params.append({'type': ntype(ptype), 'name': pname, 'def': dflt, 'uparam': uparam})
        tail = txt[k + 1:k + 200]
        mt = re.match(r'\s*((?:const|override|final|noexcept|PURE_VIRTUAL\s*\((?:[^()]|\([^()]*\))*\)|\s)*)', tail)
        quals = mt.group(1) if mt else ''
        const = bool(re.search(r'\bconst\b', quals))
        rec = {'name': name, 'line': lineno(txt, ob + (m.start()) if False else j), 'ufunc_line': lineno(txt, s),
               'spec': spec, 'meta': meta, 'dep': dep, 'rtype': rtype, 'params': params, 'const': const,
               'static': static, 'virtual': virtual, 'access': access_at(txt, ob, s, kw)}
        rec['line'] = lineno(txt, i + mname.start())
        out.setdefault(name, rec)
    return out

def parse_uproperties(txt, ob, cb, kw):
    out = {}
    body = txt[ob:cb]
    for m in re.finditer(r'\bUPROPERTY\s*\(', body):
        s = ob + m.start()
        po = txt.index('(', s)
        pc = match_paren(txt, po)
        spec, meta = parse_specs(txt[po + 1:pc])
        dep = deprec_before(txt, s)
        i = eat_attr_macros(txt, pc + 1, dep)
        # declaration jusqu'au ';' de profondeur 0
        d = 0; k = i
        while k < cb:
            ch = txt[k]
            if ch in '({<[': d += 1
            elif ch in ')}>]': d -= 1
            elif ch == ';' and d <= 0: break
            k += 1
        decl = txt[i:k]
        core = decl
        # initialiseur
        mm = re.search(r'\s*(=|\{)', core)
        bit = None
        # bitfield
        mb = re.match(r'(.*?)\b([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(\d+)\s*(=.*|\{.*)?$', core, re.S)
        if mb and '::' not in core[mb.start(2):mb.end(2) + 3]:
            ptype, name, bit = mb.group(1), mb.group(2), mb.group(3)
        else:
            # couper l'initialiseur au premier '=' ou '{' de profondeur 0 apres le type
            cut = len(core); dd = 0
            for idx, ch in enumerate(core):
                if ch in '(<[': dd += 1
                elif ch in ')>]': dd -= 1
                elif ch in '={' and dd == 0:
                    cut = idx; break
            c2 = core[:cut]
            mn = re.search(r'([A-Za-z_][A-Za-z0-9_]*)\s*(\[[^\]]*\])?\s*$', c2)
            if not mn: continue
            ptype, name = c2[:mn.start()], mn.group(1)
        toks = [t for t in re.split(r'\s+', ptype.strip()) if t and t not in ('mutable', 'static')]
        rec = {'name': name, 'line': lineno(txt, i + decl.find(name) if decl.find(name) >= 0 else i),
               'uprop_line': lineno(txt, s), 'spec': spec, 'meta': meta, 'dep': dep,
               'type': ntype(' '.join(toks)), 'bit': bit, 'access': access_at(txt, ob, s, kw)}
        out.setdefault(name, rec)
    return out

def find_delegate_decl(txt, tname):
    m = re.search(r'\b(DECLARE_DYNAMIC_(?:MULTICAST_)?(?:SPARSE_)?DELEGATE\w*)\s*\(\s*' + re.escape(tname) + r'\s*[,)]', txt)
    if not m: return None
    j = txt.index('(', m.start())
    k = match_paren(txt, j)
    return {'macro': m.group(1), 'args': [ntype(a) for a in split_top(txt[j + 1:k])], 'line': lineno(txt, m.start())}

# ---------------------------------------------------------------- recherche de declarations
class Engine:
    def __init__(self, root, tag):
        self.root = root; self.tag = tag
        self.mods = module_index(root)
        self.cls_cache = {}

    def rel(self, p):
        p = norm(p)
        return ('UE_' + self.tag + '/Engine/' + p[len(self.root) + 1:]) if p.startswith(self.root) else p

    def header(self, module, entete):
        for d in self.mods.get(module, []):
            p = d + '/' + entete
            if os.path.isfile(p): return p
        return None

    def class_in(self, path, cls):
        key = (path, cls)
        if key not in CLS:
            raw, txt = load(path)
            CLS[key] = find_classes(txt, cls)
        return CLS[key]

    def search_class(self, cls):
        """Recherche de la definition de cls dans tout Source + Plugins (.h)."""
        if cls in self.cls_cache: return self.cls_cache[cls]
        hits = rg(['-l', '-g', '*.h', '-e', r'\b(class|struct)\s+([A-Za-z_][A-Za-z0-9_]*\s+)*' + cls + r'\b\s*(final\s*)?(:|\{|$)',
                   self.root + '/Source', self.root + '/Plugins'])
        res = []
        for h in hits:
            h = norm(h)
            if self.class_in(h, cls): res.append(h)
        self.cls_cache[cls] = res
        return res

    def locate(self, module, entete, cls):
        """-> (chemin, [classes]) ; d'abord l'en-tete relatif, puis recherche."""
        h = self.header(module, entete) if entete else None
        if h:
            c = self.class_in(h, cls)
            if c: return h, c, 'meme_entete'
        for h2 in self.search_class(cls):
            return h2, self.class_in(h2, cls), 'autre_entete'
        return None, [], 'absente'

# ---------------------------------------------------------------- redirections
def load_redirects(E):
    files = rg(['-l', '-g', '*.ini', '-e', r'^\[CoreRedirects\]', E + '/Config', E + '/Plugins'])
    red = []
    for f in files:
        f = norm(f)
        sec = None
        for n, l in enumerate(open(f, encoding='utf-8-sig', errors='replace'), 1):
            s = l.strip()
            if s.startswith('['):
                sec = s; continue
            if sec != '[CoreRedirects]': continue
            m = re.match(r'^[+.\-!]?(\w+Redirects)\s*=\s*\((.*)\)\s*$', s)
            if not m: continue
            kind, body = m.group(1), m.group(2)
            on = re.search(r'OldName\s*=\s*"([^"]*)"', body)
            nn = re.search(r'NewName\s*=\s*"([^"]*)"', body)
            red.append({'kind': kind, 'old': on.group(1) if on else '', 'new': nn.group(1) if nn else '',
                        'file': f, 'line': n, 'text': s})
    cpp = E + '/Source/Runtime/CoreUObject/Private/UObject/CoreRedirects.cpp'
    kinds = {'CLASS': 'ClassRedirects', 'FUNCTION': 'FunctionRedirects', 'PROPERTY': 'PropertyRedirects',
             'STRUCT': 'StructRedirects', 'ENUM': 'EnumRedirects', 'PACKAGE': 'PackageRedirects'}
    for n, l in enumerate(open(cpp, encoding='utf-8-sig', errors='replace'), 1):
        m = re.match(r'\s*(CLASS|FUNCTION|PROPERTY|STRUCT|ENUM|PACKAGE)_REDIRECT(?:_INSTANCES)?\s*\(\s*"([^"]*)"\s*,\s*"([^"]*)"', l)
        if m:
            red.append({'kind': kinds[m.group(1)], 'old': m.group(2), 'new': m.group(3), 'file': norm(cpp), 'line': n,
                        'text': l.strip()})
    return red

def redirect_matches(reds, kind_set, cls, member=None, module=None):
    """Redirections dont OldName designe cls(.member) ; cls sans prefixe ; module verifie si OldName le precise."""
    out = []
    for r in reds:
        if r['kind'] not in kind_set: continue
        o = r['old']
        mm = re.match(r'^/Script/([^.]+)\.', o)
        if mm and module and mm.group(1) != module: continue
        o2 = re.sub(r'^/Script/[^.]+\.', '', o)
        if member is None:
            if o2 == cls or o == cls: out.append(r)
        else:
            if o2 == cls + '.' + member or o2 == cls + ':' + member: out.append(r)
            elif o2.startswith(cls + '.' + member + '.'): out.append(dict(r, param=True))
    return out

# ---------------------------------------------------------------- comparaison
PIN_META = {'worldcontext', 'defaulttoself', 'hidepin', 'autocreaterefterm', 'callablewithoutworldcontext',
            'determinesoutputtype', 'dynamicoutputparam', 'arrayparm', 'arraytypedependentparams', 'customstructureparam',
            'blueprintinternaluseonly', 'latent', 'latentinfo', 'expandenumasexecs', 'expandboolasexecs', 'nativebreakfunc',
            'nativemakefunc', 'blueprintautocast', 'commutativeassociativebinaryoperator', 'internaluseparam',
            'blueprintprotected', 'mapparam', 'mapkeyparam', 'mapvalueparam', 'setparam', 'advanceddisplay',
            'deprecatedfunction', 'deprecationmessage', 'returndisplayname',
            'compactnodetitle', 'devlopmentonly', 'developmentonly', 'variadic'}
KIND_SPECS = ('blueprintcallable', 'blueprintpure', 'blueprintimplementableevent', 'blueprintnativeevent',
              'blueprintauthorityonly', 'blueprintcosmetic', 'callineditor', 'exec', 'sealedevent')

def fn_kind(r):
    s = r['spec']
    k = [x for x in KIND_SPECS if x in s]
    pure = ('blueprintpure' in s and s.get('blueprintpure', '').lower() != 'false') or \
           (r['const'] and 'blueprintcallable' in s and 'blueprintpure' not in s)
    if s.get('blueprintpure', '').lower() == 'false': pure = False
    return tuple(sorted(k)), pure

def compare_fn(a, b):
    """a = 5.5, b = 5.8 -> (liste de differences, casse oui/non/a verifier)"""
    diffs, casse = [], 'non'
    ka, pa = fn_kind(a); kb, pb = fn_kind(b)
    if pa != pb:
        diffs.append('pure %s -> %s' % (pa, pb)); casse = 'oui'
    if ka != kb:
        diffs.append('specificateurs %s -> %s' % (','.join(ka), ','.join(kb)))
        if casse != 'oui': casse = 'a verifier'
    if a['static'] != b['static']:
        diffs.append('static %s -> %s' % (a['static'], b['static'])); casse = 'oui'
    if a['rtype'] != b['rtype']:
        diffs.append('retour %s -> %s' % (a['rtype'], b['rtype']))
        if casse != 'oui': casse = 'a verifier'
    pa_, pb_ = a['params'], b['params']
    def up(u):
        return re.sub(r'(^|,)DisplayName="[^"]*"', '', u).strip(',')
    sig_a = [(p['type'], p['name'], up(p['uparam'])) for p in pa_]
    sig_b = [(p['type'], p['name'], up(p['uparam'])) for p in pb_]
    for p, q in zip(pa_, pb_):
        if p['uparam'] != q['uparam'] and up(p['uparam']) == up(q['uparam']):
            diffs.append('UPARAM %s : %r -> %r (nom affiche seulement, nom de broche inchange)' % (p['name'], p['uparam'], q['uparam']))
    if sig_a != sig_b:
        if len(pb_) > len(pa_) and sig_b[:len(sig_a)] == sig_a and all(p['def'] is not None for p in pb_[len(pa_):]):
            diffs.append('parametres ajoutes avec defaut : ' + ', '.join('%s %s=%s' % (p['type'], p['name'], p['def']) for p in pb_[len(pa_):]))
        else:
            diffs.append('parametres %s -> %s' % ('; '.join('%s %s%s' % (t, n, (' UPARAM(%s)' % u) if u else '') for t, n, u in sig_a),
                                                   '; '.join('%s %s%s' % (t, n, (' UPARAM(%s)' % u) if u else '') for t, n, u in sig_b)))
            casse = 'oui' if casse != 'oui' else casse
            casse = 'oui'
    else:
        for p, q in zip(pa_, pb_):
            if (p['def'] or '') != (q['def'] or ''):
                diffs.append('defaut %s : %s -> %s' % (p['name'], p['def'], q['def']))
    ma = {k: v for k, v in a['meta'].items() if k in PIN_META}
    mb = {k: v for k, v in b['meta'].items() if k in PIN_META}
    for k in ('blueprintthreadsafe', 'notblueprintthreadsafe'):
        if (k in a['meta']) != (k in b['meta']):
            diffs.append('meta %s %s (surete de fil, sans effet sur les broches)' % (k, 'ajoutee' if k in b['meta'] else 'retiree'))
    if ma != mb:
        ch = sorted(set(ma) ^ set(mb)) + sorted(k for k in set(ma) & set(mb) if ma[k] != mb[k])
        diffs.append('meta ' + ', '.join('%s: %r -> %r' % (k, ma.get(k), mb.get(k)) for k in ch))
        if casse == 'non': casse = 'a verifier'
    return diffs, casse

PROP_BP = ('blueprintreadonly', 'blueprintreadwrite', 'blueprintgetter', 'blueprintsetter', 'blueprintassignable',
           'blueprintcallable', 'blueprintauthorityonly')

def compare_prop(a, b, ctx):
    diffs, casse = [], 'non'
    ta = re.sub(r'TObjectPtr<([^<>]*)>', r'\1*', a['type']); tb = re.sub(r'TObjectPtr<([^<>]*)>', r'\1*', b['type'])
    if ta != tb:
        diffs.append('type %s -> %s' % (a['type'], b['type'])); casse = 'oui'
    elif a['type'] != b['type']:
        diffs.append('type %s -> %s (TObjectPtr, sans effet Blueprint)' % (a['type'], b['type']))
    for k in ('getter', 'setter'):
        if a['spec'].get(k) != b['spec'].get(k):
            diffs.append('accesseur natif %s : %r -> %r (C++/reflexion ; les noeuds Get/Set n utilisent que BlueprintGetter/BlueprintSetter)' % (k, a['spec'].get(k), b['spec'].get(k)))
    sa = {k: a['spec'][k] for k in PROP_BP if k in a['spec']}
    sb = {k: b['spec'][k] for k in PROP_BP if k in b['spec']}
    if sa != sb:
        diffs.append('specificateurs %s -> %s' % (sa, sb))
        ro_b = 'blueprintreadwrite' not in sb and 'blueprintsetter' not in sb
        if 'set' in ctx and ro_b: casse = 'oui'
        elif 'get' in ctx and not any(k in sb for k in ('blueprintreadonly', 'blueprintreadwrite', 'blueprintgetter')): casse = 'oui'
        elif ctx.get('evenement_composant') and 'blueprintassignable' not in sb: casse = 'oui'
        elif casse == 'non': casse = 'a verifier'
    for k in ('deprecatedproperty', 'deprecationmessage', 'allowprivateaccess'):
        if a['meta'].get(k) != b['meta'].get(k):
            diffs.append('meta %s : %r -> %r' % (k, a['meta'].get(k), b['meta'].get(k)))
            if casse == 'non' and k != 'allowprivateaccess': casse = 'a verifier'
    if a['access'] != b['access']:
        diffs.append('acces %s -> %s' % (a['access'], b['access']))
    return diffs, casse

# ---------------------------------------------------------------- traitement
def short(cls_cpp):
    return cls_cpp[1:] if cls_cpp and cls_cpp[0] in 'UAFIS' and len(cls_cpp) > 1 and cls_cpp[1].isupper() else cls_cpp

def main():
    calls = json.load(open(sys.argv[1], encoding='utf-8'))
    nodes = json.load(open(sys.argv[2], encoding='utf-8'))
    outdir = sys.argv[3]
    os.makedirs(outdir, exist_ok=True)
    e5, e8 = Engine(E5, '5.5'), Engine(E8, '5.8')
    r5, r8 = load_redirects(E5), load_redirects(E8)
    keys5 = {(r['kind'], r['old'], r['new']) for r in r5}
    rows, details = [], []
    membres = [m for m in calls['membres'] if m['type'] in ('fonction', 'propriete')]
    for num, m in enumerate(membres):
        if num % 25 == 0: print('...', num, m['classe_cpp'], m['nom'], file=sys.stderr, flush=True)
        row = {'membre': m['nom'], 'type': m['type'], 'classe': m['classe_cpp'] or '(paquet)', 'module': m['module'],
               'entete': m['entete'], 'usages': m['usages'], 'nb_assets': m['assets'],
               'assets': ' '.join(m['premiers_assets']), 'contextes': ';'.join('%s=%d' % kv for kv in sorted(m.get('contextes', {}).items())),
               'statut': '', 'casse': '', 'preuve58': '', 'preuve55': '', 'detail': '', 'localisation58': ''}
        if m['module'] in PROJECT_MODULES:
            row['statut'] = 'projet'; row['casse'] = '-'
            row['detail'] = 'module du projet (%s), hors moteur' % m['module']
            rows.append(row); continue
        ctx = m.get('contextes', {})
        # delegue de paquet (signature de delegue sans classe)
        if not m['classe_cpp'] and m['nom'].endswith('__DelegateSignature'):
            tname = 'F' + m['nom'][:-len('__DelegateSignature')]
            res = {}
            for tag, E in (('5.5', e5), ('5.8', e8)):
                h = E.header(m['module'], m['entete'])
                dd = find_delegate_decl(load(h)[1], tname) if h else None
                res[tag] = (h, dd)
            (h5, d5), (h8, d8) = res['5.5'], res['5.8']
            row['preuve55'] = '%s:%s' % (e5.rel(h5), d5['line']) if d5 else 'introuvable'
            if not d8:
                row['statut'] = 'incertain'; row['detail'] = 'delegue %s introuvable dans l en-tete 5.8' % tname
            else:
                row['preuve58'] = '%s:%s' % (e8.rel(h8), d8['line'])
                if d5 and (d5['macro'], d5['args']) == (d8['macro'], d8['args']):
                    row['statut'] = 'inchange'; row['casse'] = 'non'; row['detail'] = '%s(%s)' % (d8['macro'], ', '.join(d8['args']))
                else:
                    row['statut'] = 'signature changee'; row['casse'] = 'oui'
                    row['detail'] = '%s -> %s' % (d5, d8)
            rows.append(row); continue
        cls = m['classe_cpp']; sc = short(cls)
        h5, c5, how5 = e5.locate(m['module'], m['entete'], cls)
        h8, c8, how8 = e8.locate(m['module'], m['entete'], cls)
        row['localisation58'] = how8
        def find_member(E, h, cl):
            if not h: return None, None
            raw, txt = load(h)
            for (ob, cb, kw, base, cs) in cl:
                key = (h, ob, m['type'])
                if key not in PARSED:
                    PARSED[key] = parse_ufunctions(txt, ob, cb, kw) if m['type'] == 'fonction' else parse_uproperties(txt, ob, cb, kw)
                d = PARSED[key]
                if m['nom'] in d: return d[m['nom']], (ob, cb, kw, base)
                if m['type'] == 'propriete' and m['nom'] + '_DEPRECATED' in d: return d[m['nom'] + '_DEPRECATED'], (ob, cb, kw, base)
            return None, (cl[0][:4] if cl else None)
        a, ca = find_member(e5, h5, c5)
        b, cb_ = find_member(e8, h8, c8)
        reds = redirect_matches(r8, {'FunctionRedirects'} if m['type'] == 'fonction' else {'PropertyRedirects'}, sc, m['nom'], m['module'])
        cred = redirect_matches(r8, {'ClassRedirects'}, sc, None, m['module'])
        oldreds = [r for r in reds + cred if (r['kind'], r['old'], r['new']) in keys5]
        newreds = [r for r in reds + cred if (r['kind'], r['old'], r['new']) not in keys5]
        row['preuve55'] = ('%s:%d' % (e5.rel(h5), a['line'])) if a else ('INTROUVABLE 5.5 (%s)' % (e5.rel(h5) if h5 else 'en-tete absent'))
        notes = []
        if how8 == 'autre_entete':
            notes.append('classe deplacee : %s' % e8.rel(h8))
        if not a:
            row['statut'] = 'incertain'; row['detail'] = 'declaration 5.5 non retrouvee par le script'
            if b: row['preuve58'] = '%s:%d' % (e8.rel(h8), b['line'])
            rows.append(row); continue
        if not b:
            mainred = [r for r in reds if not r.get('param')]
            if mainred or cred:
                r = (mainred or cred)[0]
                row['statut'] = 'redirige'; row['casse'] = 'non (redirection au chargement)'
                row['preuve58'] = '%s:%d' % (e8.rel(r['file']), r['line'])
                row['detail'] = r['text']
            else:
                # recherche large du nom
                hits = rg(['-n', '--no-heading', '-g', '*.h', '-w', '-e', m['nom'], E8 + '/Source', E8 + '/Plugins'])
                hits = [norm(x) for x in hits][:15]
                row['statut'] = 'retire?' if hits else 'retire'
                row['casse'] = 'oui'
                row['preuve58'] = ('%s (classe %s) sans %s' % (e8.rel(h8), cls, m['nom'])) if h8 else 'classe %s absente de 5.8' % cls
                row['detail'] = 'occurrences du nom dans 5.8 (.h) : %d ; %s' % (len(hits), ' | '.join(e8.rel(x.split(':')[0]) + ':' + x.split(':')[1] for x in hits[:6]))
            rows.append(row); continue
        row['preuve58'] = '%s:%d' % (e8.rel(h8), b['line'])
        if m['type'] == 'fonction':
            diffs, casse = compare_fn(a, b)
            dep_bp = 'deprecatedfunction' in b['meta'] and 'deprecatedfunction' not in a['meta']
        else:
            diffs, casse = compare_prop(a, b, ctx)
            dep_bp = (b['name'].endswith('_DEPRECATED') or 'deprecatedproperty' in b['meta']) and not ('deprecatedproperty' in a['meta'])
            # delegue de l'evenement de composant
            if ctx.get('evenement_composant') or 'blueprintassignable' in b['spec']:
                tn = b['type']
                d5 = find_delegate_decl(load(h5)[1], tn); d8 = find_delegate_decl(load(h8)[1], tn)
                if not d8:
                    hh = rg(['-l', '-g', '*.h', '-e', r'DECLARE_DYNAMIC_\w*\(\s*' + tn + r'\s*[,)]', E8 + '/Source', E8 + '/Plugins'])
                    if hh: d8 = find_delegate_decl(load(norm(hh[0]))[1], tn); d8h = norm(hh[0])
                    else: d8h = None
                else: d8h = h8
                if not d5:
                    hh = rg(['-l', '-g', '*.h', '-e', r'DECLARE_DYNAMIC_\w*\(\s*' + a['type'] + r'\s*[,)]', E5 + '/Source', E5 + '/Plugins'])
                    if hh: d5 = find_delegate_decl(load(norm(hh[0]))[1], a['type'])
                if d8:
                    notes.append('delegue %s %s:%d' % (tn, e8.rel(d8h), d8['line']))
                if d5 and d8 and (d5['macro'], d5['args']) != (d8['macro'], d8['args']):
                    diffs.append('signature du delegue %s(%s) -> %s(%s)' % (d5['macro'], ', '.join(d5['args']), d8['macro'], ', '.join(d8['args'])))
                    casse = 'oui'
                elif not d8 or not d5:
                    notes.append('delegue non compare (declaration non trouvee)')
        dep_cpp_new = [x for x in b['dep'] if x not in a['dep']]
        dep_cpp_old = [x for x in b['dep'] if x in a['dep']]
        if dep_cpp_old: notes.append('UE_DEPRECATED C++ deja en 5.5 (%s:%d), sans effet Blueprint' % (e5.rel(h5), a['line']))
        if newreds:
            notes.append('redirection 5.8 : ' + ' | '.join('%s:%d %s' % (e8.rel(r['file']), r['line'], r['text']) for r in newreds))
        if oldreds:
            notes.append('redirection deja presente en 5.5, membre toujours declare : ' + ' | '.join('%s:%d %s' % (e8.rel(r['file']), r['line'], r['text']) for r in oldreds))
        if dep_bp:
            row['statut'] = 'deprecie'; row['casse'] = 'non (avertissement Blueprint)' if casse == 'non' else casse
            diffs.insert(0, 'depreciation Blueprint : %s' % {k: v for k, v in b['meta'].items() if k.startswith('deprecat')})
        elif dep_cpp_new:
            row['statut'] = 'deprecie'; row['casse'] = ('non (C++ seulement : UE_DEPRECATED ignore par UHT pour le noeud)' if casse == 'non' else casse)
            diffs.insert(0, 'UE_DEPRECATED nouveau en 5.8 : %s' % ' '.join(dep_cpp_new))
        elif diffs and any(not d.startswith('defaut ') and 'TObjectPtr, sans effet' not in d and not d.startswith('acces')
                           and not d.startswith('UPARAM ') and 'surete de fil' not in d for d in diffs):
            row['statut'] = 'signature changee'; row['casse'] = casse
        else:
            row['statut'] = 'inchange'; row['casse'] = 'non'
        row['detail'] = ' ; '.join(diffs + notes)
        rows.append(row)
    # classes de noeud
    nrows = []
    for n in nodes['classes']:
        cls = 'U' + n['classe']
        row = {'membre': n['classe'], 'type': 'classe de noeud', 'classe': cls, 'module': n['module'], 'entete': n['entete'],
               'usages': n['noeuds'], 'nb_assets': n['assets'], 'assets': '', 'contextes': '', 'statut': '', 'casse': '',
               'preuve58': '', 'preuve55': '', 'detail': '', 'localisation58': ''}
        h5, c5, how5 = e5.locate(n['module'], n['entete'], cls)
        h8, c8, how8 = e8.locate(n['module'], n['entete'], cls)
        row['localisation58'] = how8
        row['preuve55'] = ('%s:%d' % (e5.rel(h5), lineno(load(h5)[1], c5[0][4]))) if c5 else 'INTROUVABLE 5.5'
        cred = redirect_matches(r8, {'ClassRedirects'}, n['classe'], None, n['module'])
        cred_all = redirect_matches(r8, {'ClassRedirects'}, n['classe'])
        newcred = [r for r in cred if (r['kind'], r['old'], r['new']) not in keys5]
        notes = []
        if c8:
            raw, txt = load(h8)
            ln = lineno(txt, c8[0][4])
            row['preuve58'] = '%s:%d' % (e8.rel(h8), ln)
            # UCLASS(...) precedent
            seg = txt[max(0, c8[0][4] - 1500):c8[0][4]]
            mu = list(re.finditer(r'\bUCLASS\s*\(', seg))
            spec8 = {}
            if mu:
                st = max(0, c8[0][4] - 1500) + mu[-1].start()
                po = txt.index('(', st); pc = match_paren(txt, po)
                spec8, meta8 = parse_specs(txt[po + 1:pc])
            dep = 'deprecated' in spec8 or n['classe'].endswith('_DEPRECATED')
            # meme chose en 5.5
            spec5 = {}
            if c5:
                raw5, txt5 = load(h5)
                seg5 = txt5[max(0, c5[0][4] - 1500):c5[0][4]]
                mu5 = list(re.finditer(r'\bUCLASS\s*\(', seg5))
                if mu5:
                    st5 = max(0, c5[0][4] - 1500) + mu5[-1].start()
                    po5 = txt5.index('(', st5); pc5 = match_paren(txt5, po5)
                    spec5, _ = parse_specs(txt5[po5 + 1:pc5])
            if how8 == 'autre_entete': notes.append('classe deplacee : %s (module 5.5 %s)' % (e8.rel(h8), n['module']))
            if c5 and c8[0][3] != c5[0][3]: notes.append('base %s -> %s' % (c5[0][3], c8[0][3]))
            depcpp = deprec_before(txt, c8[0][4]) + deprec_before(txt, st) if mu else deprec_before(txt, c8[0][4])
            body8 = txt[c8[0][0]:c8[0][1]]
            body5 = load(h5)[1][c5[0][0]:c5[0][1]] if c5 else ''
            isdep8 = re.search(r'\bIsDeprecated\s*\(\s*\)\s*const\s*override', body8)
            isdep5 = re.search(r'\bIsDeprecated\s*\(\s*\)\s*const\s*override', body5)
            for r in cred_all:
                if r not in newcred:
                    notes.append('redirection de classe sans objet (ancien module ou deja en 5.5) : %s:%d %s' % (e8.rel(r['file']), r['line'], r['text']))
            if dep and not ('deprecated' in spec5):
                row['statut'] = 'deprecie'; row['casse'] = 'a verifier'; notes.insert(0, 'UCLASS(%s)' % ','.join(spec8))
            elif isdep8 and not isdep5:
                row['statut'] = 'deprecie'; row['casse'] = 'a verifier'
                notes.insert(0, 'IsDeprecated() surcharge, nouveau en 5.8 : %s:%d' % (e8.rel(h8), lineno(txt, c8[0][0] + isdep8.start())))
            elif newcred:
                row['statut'] = 'redirige'; row['casse'] = 'non'
                notes.append('redirection : ' + ' | '.join('%s:%d %s' % (e8.rel(r['file']), r['line'], r['text']) for r in newcred))
            else:
                row['statut'] = 'inchange'; row['casse'] = 'non'
            if depcpp: notes.append('UE_DEPRECATED : ' + ' '.join(depcpp))
        else:
            if cred:
                r = cred[0]
                row['statut'] = 'redirige'; row['casse'] = 'non'
                row['preuve58'] = '%s:%d' % (e8.rel(r['file']), r['line']); notes.append(r['text'])
            else:
                row['statut'] = 'retire'; row['casse'] = 'oui'; row['preuve58'] = 'classe %s absente (recherche Source+Plugins)' % cls
        row['detail'] = ' ; '.join(notes)
        nrows.append(row)
    allrows = rows + nrows
    cols = ['membre', 'type', 'classe', 'module', 'statut', 'casse', 'preuve58', 'preuve55', 'usages', 'nb_assets',
            'assets', 'contextes', 'entete', 'localisation58', 'detail']
    with open(os.path.join(outdir, 'U1-BP-58.csv'), 'w', newline='', encoding='utf-8') as f:
        w = csv.DictWriter(f, fieldnames=cols, delimiter=';'); w.writeheader()
        for r in allrows: w.writerow({k: r[k] for k in cols})
    c = collections.Counter((r['type'] if r['type'] == 'classe de noeud' else 'membre', r['statut']) for r in allrows)
    for k, v in sorted(c.items()): print(k, v)
    print('total', len(rows), len(nrows))
    json.dump({'redirects58': len(r8), 'redirects55': len(r5)}, open(os.path.join(outdir, 'meta.json'), 'w'))

if __name__ == '__main__':
    main()

bp58_final.py

# U1-T4c, etape 2 : jugement explicite des cas ou le classement automatique de bp58.py ne suffit pas,
# libelles definitifs, ecriture du CSV livrable et des tableaux du .md. Lecture seule hors <sortie.csv>.
# Usage : python -I bp58_final.py <brut.csv> <sortie.csv>
import csv, sys, collections

LIB = {'inchange': 'inchangé', 'signature changee': 'signature changée', 'deprecie': 'déprécié',
       'redirige': 'redirigé', 'retire': 'retiré', 'retire?': 'retiré', 'projet': 'projet', 'incertain': 'incertain'}

ARRAY_J = ("broche TargetArray deja forcee en reference en 5.5 et 5.8 (K2Node_CallArrayFunction.cpp:45 5.5 / :47 5.8), "
           "nom de broche = nom du parametre (K2Node_CallFunction.cpp:1201 5.5 / :1203 5.8) inchange ; un tableau en lecture seule "
           "branche ne donne ni erreur (liste AllowedMutableContainerFunctions, UE_5.8/Engine/Config/BaseEditor.ini:203-215, "
           "commentaire 197-202 'REMOVE IN UE 5.9') ni avertissement (bEnableMutableContainerWarnings non regle, "
           "EdGraphSchema_K2.h:474, CallFunctionHandler.cpp:405-421) ; risque d'erreur au-dela de 5.8")
WCTX_J = ("parametre WorldContext (meta WorldContext) : broche cachee et remplie par le contexte (K2Node_CallFunction.cpp:1243-1253 5.8) ; "
          "seul 'const' est ajoute au pointeur, type d'objet identique")
INPUT_J = ("IsDeprecated() vrai seulement si UInputSettings::bEnabledLegacyMappingDeprecationWarnings (vrai par defaut, "
           "InputSettings.cpp:48) ET nom de la correspondance liste dans DeprecatedActionAndAxisNames (InputSettings.h:208, "
           "InputSettings.cpp:356-359) ; aucune entree dans Config/DefaultInput.ini (rg vide) : avertissement inactif pour le projet")

JUGEMENT = {}
for f in ('Array_Add', 'Array_AddUnique', 'Array_Append', 'Array_Clear', 'Array_Insert', 'Array_Remove', 'Array_RemoveItem',
          'Array_Resize', 'Array_Set', 'Array_Shuffle', 'Array_Swap'):
    JUGEMENT[('UKismetArrayLibrary', f)] = ('non en 5.8 (estimé) ; à surveiller avant 5.9', ARRAY_J)
for f in ('GetMousePositionOnViewport', 'GetViewportSize'):
    JUGEMENT[('UWidgetLayoutLibrary', f)] = ('non (estimé)', WCTX_J)
for f in ('DrawDebugBox', 'DrawDebugPoint'):
    JUGEMENT[('UKismetSystemLibrary', f)] = ('non', 'parametre DepthPriority ajoute en fin avec valeur par defaut : broche creee a la reconstruction du noeud')
for c in ('UK2Node_InputAction', 'UK2Node_InputAxisEvent'):
    JUGEMENT[(c, c[1:])] = ('non (conditionnel, inactif)', INPUT_J)

rows = list(csv.DictReader(open(sys.argv[1], encoding='utf-8'), delimiter=';'))
out = []
for r in rows:
    j = JUGEMENT.get((r['classe'], r['membre']))
    o = dict(r)
    o['casse_auto'] = r['casse']
    o['justification'] = ''
    if j:
        o['casse'], o['justification'] = j
    o['statut'] = LIB[r['statut']]
    out.append(o)
missing = [k for k in JUGEMENT if not any((r['classe'], r['membre']) == k for r in rows)]
assert not missing, missing

cols = ['membre', 'type', 'classe', 'module', 'statut', 'casse', 'preuve58', 'preuve55', 'usages', 'nb_assets', 'assets',
        'contextes', 'entete', 'localisation58', 'detail', 'casse_auto', 'justification']
with open(sys.argv[2], 'w', newline='', encoding='utf-8-sig') as f:
    w = csv.DictWriter(f, fieldnames=cols, delimiter=';')
    w.writeheader()
    for r in out:
        w.writerow({k: r[k] for k in cols})

# comptes
mem = [r for r in out if r['type'] != 'classe de noeud']
nod = [r for r in out if r['type'] == 'classe de noeud']
print('membres', len(mem), dict(collections.Counter(r['statut'] for r in mem)))
print('  par type', dict(collections.Counter((r['type'], r['statut']) for r in mem)))
print('noeuds', len(nod), dict(collections.Counter(r['statut'] for r in nod)))
print('usages par statut (membres)', {s: sum(int(r['usages']) for r in mem if r['statut'] == s) for s in set(r['statut'] for r in mem)})
print('noeuds par statut (classes)', {s: sum(int(r['usages']) for r in nod if r['statut'] == s) for s in set(r['statut'] for r in nod)})

# assets touches : repartition exacte par asset (pernode.json, lecture des .uasset, sommes controlees = usages)
import json, os
pn = json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pernode.json')))
# GetViewportSize : meme FName pour APlayerController (2 usages, Birdview+Room) et UWidgetLayoutLibrary (3 usages,
# Birdview+PC+Cursor) ; au moins 1 noeud par asset liste -> repartition forcee 1/1/1
pn['UWidgetLayoutLibrary::GetViewportSize'] = {'/Game/BPs/Birdview': 1, '/Game/BPs/PC': 1, '/Game/UI/Cursor': 1}
chg = [r for r in mem if r['statut'] not in ('inchangé', 'projet')]
per = collections.defaultdict(dict)
for r in chg:
    d = pn[r['classe'] + '::' + r['membre']]
    assert sum(d.values()) == int(r['usages']) and sorted(d) == sorted(r['assets'].split()), r['membre']
    for a, n in d.items():
        per[a][r['membre']] = n
per['/Game/BPs/PC']['K2Node_InputAction (classe de noeud)'] = 32
per['/Game/BPs/PC']['K2Node_InputAxisEvent (classe de noeud)'] = 6
print('assets touches', len(per), 'noeuds', sum(sum(v.values()) for v in per.values()))
for a in sorted(per, key=lambda x: (-sum(per[x].values()), x)):
    d = per[a]
    arr = sum(n for k, n in d.items() if k.startswith('Array_'))
    print('| `%s` | %d | %d | 0 | %s |' % (a, sum(d.values()), arr, ', '.join('%s %d' % (k, n) for k, n in sorted(d.items()))))

pernode.py

# Repartition par asset des noeuds references (lecture seule des .uasset) : compte les FName (index, 0) du nom du membre
# dans les donnees d'export (apres TotalHeaderSize), seulement comme valeur d'un tag 'MemberName' (48 octets avant). Controle : la somme par membre doit egaler 'usages' de calls.json.
# Usage : python -I pernode.py <calls.json> <Content> <membre>[,<membre>...]
import json, struct, sys, os, collections

def summary(b):
    o = 0
    tag, legacy = struct.unpack_from('<Ii', b, o); o += 8
    assert tag == 0x9E2A83C1, hex(tag)
    if legacy != -4: o += 4                      # LegacyUE3Version
    ue4, = struct.unpack_from('<i', b, o); o += 4
    if legacy <= -8: o += 4                      # FileVersionUE5
    o += 4                                       # licensee
    if legacy <= -2:
        n, = struct.unpack_from('<i', b, o); o += 4
        o += n * 20                              # FGuid + int32 (format optimise)
    total, = struct.unpack_from('<i', b, o); o += 4
    ln, = struct.unpack_from('<i', b, o); o += 4
    o += ln if ln >= 0 else -2 * ln              # PackageName
    o += 4                                       # PackageFlags
    ncount, noff = struct.unpack_from('<ii', b, o)
    return total, ncount, noff

def names(b, ncount, noff):
    o = noff; out = []
    for i in range(ncount):
        ln, = struct.unpack_from('<i', b, o); o += 4
        if ln >= 0:
            s = b[o:o + ln - 1].decode('latin-1'); o += ln
        else:
            s = b[o:o - 2 * ln - 2].decode('utf-16-le'); o += -2 * ln
        o += 4                                   # hashes
        out.append(s)
    return out

calls = json.load(open(sys.argv[1], encoding='utf-8'))
content = sys.argv[2]
want = set(sys.argv[3].split(','))
res = {}
for m in calls['membres']:
    if m['nom'] not in want or m['type'] == 'noeud': continue
    per = {}
    for a in m['premiers_assets']:
        p = os.path.join(content, a[len('/Game/'):] + '.uasset')
        b = open(p, 'rb').read()
        total, nc, no = summary(b)
        nm = names(b, nc, no)
        idx = nm.index(m['nom']) if m['nom'] in nm else -1
        imn = nm.index('MemberName') if 'MemberName' in nm else -1
        cnt = 0
        if idx >= 0 and imn >= 0:
            pat, anc = struct.pack('<ii', idx, 0), struct.pack('<ii', imn, 0)
            pos = total
            while True:
                pos = b.find(pat, pos)
                if pos < 0: break
                if anc in b[pos - 48:pos]: cnt += 1   # valeur d'un tag 'MemberName' (FMemberReference)
                pos += 1
        per[a] = cnt
    ok = sum(per.values()) == m['usages'] and len(m['premiers_assets']) == m['assets']
    res[m['classe_cpp'] + '::' + m['nom']] = per
    print('%-28s usages=%4d somme=%4d %s  %s' % (m['classe_cpp'] + '::' + m['nom'], m['usages'], sum(per.values()), 'OK' if ok else 'ECART',
                                                 ' '.join('%s=%d' % (k.split('/')[-1], v) for k, v in sorted(per.items()))))
json.dump(res, open('pernode.json', 'w'), indent=1)

xcheck.py

# Contre-verification : comparaison textuelle brute des blocs de declaration 5.5 / 5.8 (sans parseur)
import csv, re, sys
ROOT = 'D:/Game/Launcher/Engine/'
def block(ref, kind):
    p, l = ref.rsplit(':', 1); l = int(l)
    lines = open(ROOT + p, encoding='utf-8-sig', errors='replace').read().split('\n')
    i = l - 1
    key = 'UFUNCTION' if kind == 'fonction' else 'UPROPERTY'
    s = i
    while s > max(0, i - 25) and key not in lines[s]: s -= 1
    e = i
    while e < len(lines) - 1 and ';' not in lines[e] and '{' not in lines[e]: e += 1
    t = ' '.join(lines[s:e + 1])
    t = re.sub(r'//.*?(?=$)', '', t)
    t = re.sub(r'/\*.*?\*/', ' ', t)
    t = re.sub(r'\b[A-Z0-9_]+_API\b', '', t)
    t = re.sub(r'\s+', ' ', t).strip()
    t = re.sub(r'\s*([(),=<>*&])\s*', r'\1', t)
    return t, s + 1, e + 1
rows = list(csv.DictReader(open(sys.argv[1], encoding='utf-8'), delimiter=';'))
n = same = 0
for r in rows:
    if r['type'] not in ('fonction', 'propriete') or r['statut'] == 'projet' or r['classe'] == '(paquet)': continue
    n += 1
    a, s5, e5 = block(r['preuve55'], r['type']); b, s8, e8 = block(r['preuve58'], r['type'])
    if a == b: same += 1; continue
    print('DIFF', r['statut'], r['classe'], r['membre'], '\n   5.5:', a, '\n   5.8:', b)
print('compares', n, 'identiques', same, 'differents', n - same)

uclass_check.py

# Compare les UCLASS(...) / UE_DEPRECATED des classes portant les membres (5.5 vs 5.8). Lecture seule.
import sys, os, json, re
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import bp58 as B

calls = json.load(open(sys.argv[1], encoding='utf-8'))
e5, e8 = B.Engine(B.E5, '5.5'), B.Engine(B.E8, '5.8')
seen = set()
n = diff = 0
for m in calls['membres']:
    if m['type'] not in ('fonction', 'propriete') or m['module'] in B.PROJECT_MODULES or not m['classe_cpp']:
        continue
    key = (m['module'], m['entete'], m['classe_cpp'])
    if key in seen:
        continue
    seen.add(key)
    out = []
    for E in (e5, e8):
        h, c, how = E.locate(m['module'], m['entete'], m['classe_cpp'])
        raw, txt = B.load(h)
        cs = c[0][4]
        seg = txt[max(0, cs - 2000):cs]
        mu = list(re.finditer(r'\bUCLASS\s*\(', seg))
        spec = ''
        if mu:
            st = max(0, cs - 2000) + mu[-1].start()
            po = txt.index('(', st)
            pc = B.match_paren(txt, po)
            spec = re.sub(r'\s+', ' ', txt[po + 1:pc])
        dep = B.deprec_before(txt, cs)
        out.append((spec, dep, c[0][3], E.rel(h) + ':' + str(B.lineno(txt, cs))))
    n += 1
    s5, s8 = out
    sp5, sp8 = B.parse_specs(s5[0]), B.parse_specs(s8[0])
    if (sp5, s5[1], s5[2]) != (sp8, s8[1], s8[2]):
        diff += 1
        print('DIFF', m['classe_cpp'])
        print('   5.5', s5)
        print('   5.8', s8)
    elif 'deprecated' in sp8[0] or s8[1]:
        print('DEPR', m['classe_cpp'], s8)
print('classes', n, 'differentes', diff)