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*`), 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& TargetArray` → `UPARAM(ref) TArray& 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 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\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) ```bash S=""; 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` ```python # U1-T4c : classement 5.8 des membres natifs utilises par les Blueprints (lecture seule). # Usage : python -I bp58.py # Lit les sources UE 5.5 et 5.8 ; n'ecrit que dans . 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` ```python # 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 . # Usage : python -I bp58_final.py 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` ```python # 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 [,...] import json, struct, sys, os, collections def summary(b): o = 0 tag, legacy = struct.unpack_from('= 0 else -2 * ln # PackageName o += 4 # PackageFlags ncount, noff = struct.unpack_from('= 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(' 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` ```python # 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) ```