LifePlay-Revival/Docs/Migration/etat/U58/U1/U1-BP-58.md

1232 lines
74 KiB
Markdown
Raw Normal View 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)
```bash
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`
```python
# 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`
```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 <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`
```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 <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`
```python
# 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`
```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)
```