diff --git a/Content/BPs/GI.uasset b/Content/BPs/GI.uasset index 5ba6288..ce5fe60 100644 Binary files a/Content/BPs/GI.uasset and b/Content/BPs/GI.uasset differ diff --git a/Content/BPs/MapMarkers.uasset b/Content/BPs/MapMarkers.uasset index a2052c1..97a7bc1 100644 Binary files a/Content/BPs/MapMarkers.uasset and b/Content/BPs/MapMarkers.uasset differ diff --git a/Content/BPs/PC.uasset b/Content/BPs/PC.uasset index e798c67..47e7627 100644 Binary files a/Content/BPs/PC.uasset and b/Content/BPs/PC.uasset differ diff --git a/Content/BPs/Person.uasset b/Content/BPs/Person.uasset index 7cbfc6d..175b420 100644 Binary files a/Content/BPs/Person.uasset and b/Content/BPs/Person.uasset differ diff --git a/Content/BPs/Room.uasset b/Content/BPs/Room.uasset index 397ab97..5c21322 100644 Binary files a/Content/BPs/Room.uasset and b/Content/BPs/Room.uasset differ diff --git a/Content/BPs/StreetMap_Static.uasset b/Content/BPs/StreetMap_Static.uasset index 6bd6e00..899394e 100644 Binary files a/Content/BPs/StreetMap_Static.uasset and b/Content/BPs/StreetMap_Static.uasset differ diff --git a/Content/UI/Achievement.uasset b/Content/UI/Achievement.uasset index 0092001..f8c8b8e 100644 Binary files a/Content/UI/Achievement.uasset and b/Content/UI/Achievement.uasset differ diff --git a/Content/UI/BodyShape.uasset b/Content/UI/BodyShape.uasset index 3b83fae..803df71 100644 Binary files a/Content/UI/BodyShape.uasset and b/Content/UI/BodyShape.uasset differ diff --git a/Content/UI/BodyShapeMenu.uasset b/Content/UI/BodyShapeMenu.uasset index 026957e..ad506a9 100644 Binary files a/Content/UI/BodyShapeMenu.uasset and b/Content/UI/BodyShapeMenu.uasset differ diff --git a/Content/UI/Bondage.uasset b/Content/UI/Bondage.uasset index a325e70..ecba4c3 100644 Binary files a/Content/UI/Bondage.uasset and b/Content/UI/Bondage.uasset differ diff --git a/Content/UI/Bubble.uasset b/Content/UI/Bubble.uasset index 1f663e5..1805d5a 100644 Binary files a/Content/UI/Bubble.uasset and b/Content/UI/Bubble.uasset differ diff --git a/Content/UI/Builder.uasset b/Content/UI/Builder.uasset index e9ec96b..1423d69 100644 Binary files a/Content/UI/Builder.uasset and b/Content/UI/Builder.uasset differ diff --git a/Content/UI/CharGen.uasset b/Content/UI/CharGen.uasset index a5ea127..bedb495 100644 Binary files a/Content/UI/CharGen.uasset and b/Content/UI/CharGen.uasset differ diff --git a/Content/UI/CharGenSlider.uasset b/Content/UI/CharGenSlider.uasset index 6a6692f..42bf225 100644 Binary files a/Content/UI/CharGenSlider.uasset and b/Content/UI/CharGenSlider.uasset differ diff --git a/Content/UI/City.uasset b/Content/UI/City.uasset index 9bfd850..edecc1d 100644 Binary files a/Content/UI/City.uasset and b/Content/UI/City.uasset differ diff --git a/Content/UI/CityMenu.uasset b/Content/UI/CityMenu.uasset index 44d4afd..9a4c063 100644 Binary files a/Content/UI/CityMenu.uasset and b/Content/UI/CityMenu.uasset differ diff --git a/Content/UI/Contact.uasset b/Content/UI/Contact.uasset index 360adfb..6a71400 100644 Binary files a/Content/UI/Contact.uasset and b/Content/UI/Contact.uasset differ diff --git a/Content/UI/ContactMenu.uasset b/Content/UI/ContactMenu.uasset index d7cb66d..70ecf66 100644 Binary files a/Content/UI/ContactMenu.uasset and b/Content/UI/ContactMenu.uasset differ diff --git a/Content/UI/Cursor.uasset b/Content/UI/Cursor.uasset index a7670e6..d30b9bb 100644 Binary files a/Content/UI/Cursor.uasset and b/Content/UI/Cursor.uasset differ diff --git a/Content/UI/GenitalSlider.uasset b/Content/UI/GenitalSlider.uasset index 6ae13dd..646fed7 100644 Binary files a/Content/UI/GenitalSlider.uasset and b/Content/UI/GenitalSlider.uasset differ diff --git a/Content/UI/Hairstyle.uasset b/Content/UI/Hairstyle.uasset index 5ffe310..ea00118 100644 Binary files a/Content/UI/Hairstyle.uasset and b/Content/UI/Hairstyle.uasset differ diff --git a/Content/UI/Inventory.uasset b/Content/UI/Inventory.uasset index 8ec1b72..f2f3158 100644 Binary files a/Content/UI/Inventory.uasset and b/Content/UI/Inventory.uasset differ diff --git a/Content/UI/Inventory_Cloth.uasset b/Content/UI/Inventory_Cloth.uasset index 507e127..0574520 100644 Binary files a/Content/UI/Inventory_Cloth.uasset and b/Content/UI/Inventory_Cloth.uasset differ diff --git a/Content/UI/LoadExtImg.uasset b/Content/UI/LoadExtImg.uasset index 37a5156..f6e2d36 100644 Binary files a/Content/UI/LoadExtImg.uasset and b/Content/UI/LoadExtImg.uasset differ diff --git a/Content/UI/SexAction.uasset b/Content/UI/SexAction.uasset index 90cbc18..63b638c 100644 Binary files a/Content/UI/SexAction.uasset and b/Content/UI/SexAction.uasset differ diff --git a/Content/UI/StatMenu.uasset b/Content/UI/StatMenu.uasset index 0407d9a..6d0ba13 100644 Binary files a/Content/UI/StatMenu.uasset and b/Content/UI/StatMenu.uasset differ diff --git a/Content/UI/TopRight.uasset b/Content/UI/TopRight.uasset index 417bb54..2a1838d 100644 Binary files a/Content/UI/TopRight.uasset and b/Content/UI/TopRight.uasset differ diff --git a/Content/UI/Tutorial.uasset b/Content/UI/Tutorial.uasset index 5ee6a36..9b426a1 100644 Binary files a/Content/UI/Tutorial.uasset and b/Content/UI/Tutorial.uasset differ diff --git a/Docs/Migration/05_Correctifs_Isoles.md b/Docs/Migration/05_Correctifs_Isoles.md index 0046198..151b073 100644 --- a/Docs/Migration/05_Correctifs_Isoles.md +++ b/Docs/Migration/05_Correctifs_Isoles.md @@ -43,6 +43,7 @@ exporté dans `Docs/Migration/correctifs/` pour relecture. | CX-B09 | `LPFixButtonPadding` (nouveau) | 430 `UButton`, 46 Widget Blueprints | marges des boutons remises au défaut UE 4.27 (Normal 2,2,2,2 ; Pressed 2,3,2,1), composante par composante | le style par défaut de `UButton` est passé à 12 px horizontaux en UE5 (`FUMGCoreStyle`) ; les assets 4.27 ne stockent pas la marge par défaut : cause d'ERR-034 (barre de temps décalée) | 1 | **réimporté** (CHG-033), validé en jeu | | CX-B10 | `LPFixSkeletalMeshGet` (nouveau) | 56 nœuds `Get SkeletalMesh`, 8 Blueprints (`Person` 28, `GI` 17, `CharGen` 3, `Inventory` 3, `Hairstyle` 2, `CharGenSlider`, `Cursor`, `Inventory_Cloth` 1) | remplacés par `LPMeshCompat::GetSkeletalMesh(Component)` = `Cast(GetSkinnedAsset())`, la sémantique exacte du getter déprécié, valable pour tout `USkinnedMeshComponent` | R-03 refait (L7a) ; remplace CX-B05 rejeté | 1 | **réimporté** (CHG-036), validé en jeu | | CX-B11 | `LPFixNavRandomPoint` (nouveau) | 6 nœuds `GetRandomPointInNavigableRadius` (`Person` 3, `Room` 2, `AI` 1) | remplacés par `LPNavigationHelpers::GetRandomLocationInNavigableRadius` : nœud pur, mêmes broches, même corps que la fonction dépréciée (appel de `K2_GetRandomLocationInNavigableRadius`, identique en 4.27 et 5.5) ; liaisons et valeurs par défaut reportées broche par broche | R-04 (L7b) : 6 derniers avertissements de compilation ; le remplaçant moteur est impur (broches d'exécution), d'où l'enrobage pur | 1 | **réimporté** (CHG-037), retiré à tort (CHG-038) puis réappliqué (CHG-039) ; validé en jeu | +| CX-B12 | `LPFixStringSearch` (nouveau) | 385 nœuds `Contains` (160), `Split` (222), `FindSubstring` (3), 28 Blueprints (`GI` 240, `Person` 35, `Room` 16, `CharGen` 14…) | rebranchés sur `LPStringCompat::Contains/Split/FindSubstring` : mêmes broches ; sous-chaîne vide en mode insensible à la casse depuis le début → « non trouvé » comme en 4.27 (UE5 : trouvé en position 0) | ERR-005 (`getBeautyValue` ×38 : `filterMorphList` sans étiquette laissait passer tous les morphs) et toute recherche de texte avec un motif vide | 1 | **réimporté** (CHG-042) ; validation en jeu en attente | | CX-B07 | `LPResavePackages` (nouveau) | 239 paquets UE4 restants (`UI` 147, `Structs` 57, `BPs` 22, `Render` 11, `Characters` 1, `Scenes` 1) | format UE5, contenu inchangé | EX-03 ; T-STAT-01 | 6 | **réimporté** (CHG-035), diff avant/après neutre ; validation en jeu en attente | ## 3. Protocole de réimport sous contrôle renforcé diff --git a/Docs/Migration/Journal_Modifications.md b/Docs/Migration/Journal_Modifications.md index 5c1f601..554c665 100644 --- a/Docs/Migration/Journal_Modifications.md +++ b/Docs/Migration/Journal_Modifications.md @@ -657,3 +657,36 @@ Suite L7i (2026-10-01) : (redémarrages : `CheckingSceneIndex` remis à -1 et liste remélangée à chaque changement de `CurrentAmenity`). Les sessions comparées n'ont pas suivi le même parcours (déplacements, accélération du temps) : une comparaison A/B sur un parcours identique est nécessaire avant de conclure à une régression. + +A/B à parcours identique (2026-10-01, nouvelle partie, vitesse normale, maison puis travail ; 4.27 47 min, +`AB427_20261001.log` ; 5.5 33 min, `AB55_20261001.log`) : + +| Par minute | 4.27 | 5.5 | +|---|---|---| +| Vérifications de scènes | 626 | 614 | +| `AI` introuvable (`getActorVar`…) | 0 | 0 | +| Variables `Person` vides | 124 | 123 | +| `getBeautyValue` (`Floats` vide) | 1,3 | 52 | + +Les erreurs `AI` dépendaient du rythme de jeu (accélération du temps) : absentes à parcours égal, classées comme +préexistantes (même mécanisme en 4.27). Seul écart réel : `getBeautyValue`. + +Cause (ERR-005) : `BeautyStandards` est indexé par forme (`round`, `oval`…, fichier +`Modules/beauty_standards_default.txt` identique dans les deux versions) ; la première recherche par nom de morph +échoue normalement (62 avertissements en 4.27 aussi). L'écart vient du nombre d'appels : `filterMorphList` est +appelé sans étiquette et teste `Contains(tags, "")`. Moteur : `FString::Find` 4.27 → `FCString::Stristr`, qui +cherche le caractère nul et ne trouve rien ; 5.5 → `TCString::Strnistr`, qui renvoie le début de la chaîne. Les +autres modes (sensible à la casse, depuis la fin) et `StartsWith`, `EndsWith`, `Replace` sont identiques. + +| CHG | Date | Lot | Fichiers | Raison | Retour arrière | +|---|---|---|---|---|---| +| CHG-042 | 2026-10-01 | L7i (CX-B12) | `Source/LifePlay/LPStringCompat.h/.cpp` (`FindUE4`, `Contains`, `FindSubstring`, `Split`) ; `Source/LifePlayEditorTools/LPFixStringSearchCommandlet`, `LPTestStringCompatCommandlet`, `LPDumpCDOCommandlet` (nouveaux) ; `Tools/DiffRemplacementNoeuds.py` (profil `CX-B12`, broches absentes des deux côtés) ; 28 Blueprints (`etat/L7i_CX-B12_assets.txt`) | ERR-005 : recherche de texte avec motif vide | `Tools\Restore.ps1 -Lot L7i-CX-B12` ; `git checkout L7i-CX-B12-avant -- Source Tools` | + +| Contrôle | Résultat | Preuve | +|---|---|---| +| Test `LPTestStringCompat` : transcription littérale de `FString::Find` 4.27 contre `LPStringCompat` et contre le moteur 5.5 | 3 120 cas : compat = 4.27 dans 100 % des cas ; moteur 5.5 ≠ 4.27 dans 60 cas, tous « sous-chaîne vide, insensible à la casse, depuis le début » | `Saved/Logs/portage/L7i_LPTestStringCompat_*.log` | +| Simulation | 385 nœuds (`Contains` 160, `Split` 222, `FindSubstring` 3), 28 Blueprints, 0 cas non pris en charge | `etat/L7i_LPFixStringSearch_20261001-191027.txt` | +| Application (contrôle intégré) | 385/385 remplacés, 28 Blueprints sauvegardés, 0 écart | `etat/L7i_LPFixStringSearch_20261001-191311.txt` | +| Diff indépendant (`DiffRemplacementNoeuds.py … CX-B12`) | 385 paires, liaisons et valeurs par défaut identiques ; 0 autre objet modifié ; profils L7a (56/56) et L7b (6/6) inchangés | `Saved/Logs/portage/L7i_CX-B12_assets_{avant,apres}.txt` | +| `CompileAllBlueprints` | 0 erreur, 0 avertissement | `etat/L7i_CompileAllBlueprints_20261001-191653.txt` | +| Build empaquetée `LifePlay_Build_5.5_L7i_B12` | cuisson 0 erreur, 0 avertissement (164 paquets recuits) ; T-PKG-01 : 8561 `.uasset` | `Saved/Logs/portage/L7i_CX-B12_BuildCookRun.log` | diff --git a/Docs/Migration/Registre_Erreurs.md b/Docs/Migration/Registre_Erreurs.md index 51b3023..594a1c5 100644 --- a/Docs/Migration/Registre_Erreurs.md +++ b/Docs/Migration/Registre_Erreurs.md @@ -16,7 +16,7 @@ | ERR-028 | `clearVariable` : index 0 sur tableau `Person` vide de `Variable` (via `GeneratePerson`, `getRelative`, `GetSpecific`) | partie utilisateur L4b | 211 | **bruit** : présent à l'identique en 4.27 — piste écartée | — | accepté | | ERR-003 | `Attempted to access index 0 from array Float_… of length 0` dans `GI::ReturnStatIfNeeded` (variable DSL `Float` vide) | build 5.5 Development | 2 (4.27 : 0) | haute | L5 | ouvert | | ERR-004 | `index N from CallFunc_ParseIntoArray_ReturnValue of length N` (`GI` EventGraph) | PIE 09-28 | 6 510 | moyenne | L7b | ouvert — origine localisée (L3) : `GI.populateClothes4` appelée par `Person.ReceiveBeginPlay` | -| ERR-005 | `getBeautyValue` : `Floats_*` index hors bornes | PIE 09-28 | 1 297 | moyenne | L7b | ouvert | +| ERR-005 | `getBeautyValue` : `Floats_*` index hors bornes | PIE 09-28 ; A/B 2026-10-01 : 0,08/perso en 4.27 contre 3,0 en 5.5 | 1 297 | moyenne | L7i | **corrigé (CHG-042, CX-B12)** — cause : `FString::Find` avec sous-chaîne vide (insensible à la casse, depuis le début) renvoie 0 en UE5 et « non trouvé » en 4.27 ; `Contains(x, "")` devenu vrai : `filterMorphList` appelé sans étiquette ne filtrait plus. 385 nœuds `Contains`/`Split`/`FindSubstring` rebranchés sur `LPStringCompat` ; validation en jeu en attente | | ERR-006 | `Accès à None` `NearVec` dans `Person::standIdle` | PIE 09-28 | 273 | moyenne | L7k | ouvert | | ERR-007 | `Accès à None` `ContactMenu` dans `UpdateDailyPose` | PIE 09-28 | 257 | moyenne | L7k | ouvert | | ERR-008 | `CallFunc_Array_Get_Item` à None dans `GI` | PIE 09-28 | 229 | moyenne | L5 | ouvert | diff --git a/Source/LifePlay/LPStringCompat.cpp b/Source/LifePlay/LPStringCompat.cpp index 7ea1562..461277a 100644 --- a/Source/LifePlay/LPStringCompat.cpp +++ b/Source/LifePlay/LPStringCompat.cpp @@ -25,3 +25,39 @@ TArray ULPStringCompat::ParseIntoArray(const FString& SourceString, con } return OutArray; } + +int32 ULPStringCompat::FindUE4(const FString& Str, const FString& SubStr, ESearchCase::Type SearchCase, ESearchDir::Type SearchDir, int32 StartPosition) +{ + // UE 4.27 : FCString::Stristr(Start, TEXT("")) ne trouve jamais rien (il cherche nul final). + if (SubStr.IsEmpty() && SearchCase == ESearchCase::IgnoreCase && SearchDir == ESearchDir::FromStart) + { + return INDEX_NONE; + } + return Str.Find(SubStr, SearchCase, SearchDir, StartPosition); +} + +int32 ULPStringCompat::FindSubstring(const FString& SearchIn, const FString& Substring, bool bUseCase, bool bSearchFromEnd, int32 StartPosition) +{ + // Meme corps que UKismetStringLibrary::FindSubstring, avec FindUE4. + const ESearchCase::Type Case = bUseCase ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase; + const ESearchDir::Type Dir = bSearchFromEnd ? ESearchDir::FromEnd : ESearchDir::FromStart; + return FindUE4(SearchIn, Substring, Case, Dir, StartPosition); +} + +bool ULPStringCompat::Contains(const FString& SearchIn, const FString& Substring, bool bUseCase, bool bSearchFromEnd) +{ + return FindSubstring(SearchIn, Substring, bUseCase, bSearchFromEnd) != -1; +} + +bool ULPStringCompat::Split(const FString& SourceString, const FString& InStr, FString& LeftS, FString& RightS, ESearchCase::Type SearchCase, ESearchDir::Type SearchDir) +{ + // Meme corps que FString::Split (4.27 et 5.5), avec FindUE4. + const int32 InPos = FindUE4(SourceString, InStr, SearchCase, SearchDir); + if (InPos < 0) + { + return false; + } + LeftS = SourceString.Left(InPos); + RightS = SourceString.Mid(InPos + InStr.Len()); + return true; +} diff --git a/Source/LifePlay/LPStringCompat.h b/Source/LifePlay/LPStringCompat.h index 7aeb225..99730ea 100644 --- a/Source/LifePlay/LPStringCompat.h +++ b/Source/LifePlay/LPStringCompat.h @@ -13,6 +13,14 @@ // Runtime/Core/Private/Containers/String.cpp, FString::ParseIntoArray). Ses parametres et // ses valeurs par defaut sont ceux de UKismetStringLibrary::ParseIntoArray, pour que la // commandlet LPFixParseIntoArray puisse rebrancher les noeuds existants sans perdre de liaison. +// +// Correctif CX-B12 (lot L7i) : FString::Find avec une sous-chaine VIDE, recherche insensible a la +// casse depuis le debut (mode par defaut des noeuds Contains, Find Substring et Split) : +// - UE 4.27 : FCString::Stristr cherche le caractere nul final et ne le trouve jamais -> INDEX_NONE ; +// - UE 5.5 : TCString::Strnistr renvoie le debut de la chaine -> 0. +// "Contains(x, "")" est donc faux en 4.27 et vrai en 5.5. Les autres modes (sensible a la casse, +// depuis la fin) donnent le meme resultat dans les deux moteurs. Contains, FindSubstring et Split +// ci-dessous reprennent les parametres des fonctions de UKismetStringLibrary et appliquent la regle 4.27. #pragma once @@ -29,4 +37,19 @@ public: /** ParseIntoArray avec le comportement exact de UE 4.27 (voir en-tete du fichier). */ UFUNCTION(BlueprintPure, Category = "Utilities|String", meta = (DisplayName = "Parse Into Array (UE4)")) static TArray ParseIntoArray(const FString& SourceString, const FString& Delimiter = FString(TEXT(" ")), const bool CullEmptyStrings = true); + + /** FindSubstring avec le comportement de UE 4.27 pour une sous-chaine vide (voir en-tete). */ + UFUNCTION(BlueprintPure, Category = "Utilities|String", meta = (DisplayName = "Find Substring (UE4)")) + static int32 FindSubstring(const FString& SearchIn, const FString& Substring, bool bUseCase = false, bool bSearchFromEnd = false, int32 StartPosition = -1); + + /** Contains avec le comportement de UE 4.27 pour une sous-chaine vide (voir en-tete). */ + UFUNCTION(BlueprintPure, Category = "Utilities|String", meta = (DisplayName = "Contains (UE4)")) + static bool Contains(const FString& SearchIn, const FString& Substring, bool bUseCase = false, bool bSearchFromEnd = false); + + /** Split avec le comportement de UE 4.27 pour un separateur vide (voir en-tete). */ + UFUNCTION(BlueprintPure, Category = "Utilities|String", meta = (DisplayName = "Split (UE4)")) + static bool Split(const FString& SourceString, const FString& InStr, FString& LeftS, FString& RightS, ESearchCase::Type SearchCase = ESearchCase::IgnoreCase, ESearchDir::Type SearchDir = ESearchDir::FromStart); + + /** FString::Find de UE 4.27 : seul ecart avec UE 5.5, la sous-chaine vide en mode insensible a la casse depuis le debut. */ + static int32 FindUE4(const FString& Str, const FString& SubStr, ESearchCase::Type SearchCase, ESearchDir::Type SearchDir, int32 StartPosition = INDEX_NONE); }; diff --git a/Source/LifePlayEditorTools/LPDumpCDOCommandlet.cpp b/Source/LifePlayEditorTools/LPDumpCDOCommandlet.cpp new file mode 100644 index 0000000..121ce35 --- /dev/null +++ b/Source/LifePlayEditorTools/LPDumpCDOCommandlet.cpp @@ -0,0 +1,91 @@ +// Portage UE 5.5 - voir LPDumpCDOCommandlet.h + +#include "LPDumpCDOCommandlet.h" + +#include "Misc/FileHelper.h" +#include "UObject/UnrealType.h" + +DEFINE_LOG_CATEGORY_STATIC(LogLPDumpCDO, Log, All); + +ULPDumpCDOCommandlet::ULPDumpCDOCommandlet() +{ + IsClient = false; + IsServer = false; + IsEditor = true; + LogToConsole = true; +} + +int32 ULPDumpCDOCommandlet::Main(const FString& Params) +{ + TArray Tokens, Switches; + TMap ParamsMap; + ParseCommandLine(*Params, Tokens, Switches, ParamsMap); + + const FString ClassPath = ParamsMap.FindRef(TEXT("Class")); + const FString Out = ParamsMap.FindRef(TEXT("Out")); + TArray Wanted; + ParamsMap.FindRef(TEXT("Props")).ParseIntoArray(Wanted, TEXT(","), true); + if (ClassPath.IsEmpty() || Out.IsEmpty() || Wanted.Num() == 0) + { + UE_LOG(LogLPDumpCDO, Error, TEXT("Usage : -Class=/Game/X.X_C -Out= -Props=Nom1,Nom2")); + return 1; + } + + UClass* Class = LoadObject(nullptr, *ClassPath); + if (!Class) + { + UE_LOG(LogLPDumpCDO, Error, TEXT("Classe introuvable : %s"), *ClassPath); + return 1; + } + const UObject* CDO = Class->GetDefaultObject(); + + TArray Lines; + Lines.Add(FString::Printf(TEXT("##### %s"), *ClassPath)); + for (const FString& Name : Wanted) + { + const FProperty* Prop = Class->FindPropertyByName(FName(*Name)); + if (!Prop) + { + Lines.Add(FString::Printf(TEXT("%s : introuvable"), *Name)); + continue; + } + const void* Value = Prop->ContainerPtrToValuePtr(CDO); + if (const FMapProperty* MapProp = CastField(Prop)) + { + FScriptMapHelper Helper(MapProp, Value); + Lines.Add(FString::Printf(TEXT("%s [map] : %d entree(s)"), *Name, Helper.Num())); + for (FScriptMapHelper::FIterator It(Helper); It; ++It) + { + FString Key, Val; + MapProp->KeyProp->ExportTextItem_Direct(Key, Helper.GetKeyPtr(It), nullptr, nullptr, PPF_None); + MapProp->ValueProp->ExportTextItem_Direct(Val, Helper.GetValuePtr(It), nullptr, nullptr, PPF_None); + Lines.Add(FString::Printf(TEXT(" %s = %s"), *Key, *Val)); + } + } + else if (const FArrayProperty* ArrayProp = CastField(Prop)) + { + FScriptArrayHelper Helper(ArrayProp, Value); + Lines.Add(FString::Printf(TEXT("%s [tableau] : %d element(s)"), *Name, Helper.Num())); + for (int32 i = 0; i < Helper.Num(); ++i) + { + FString Text; + ArrayProp->Inner->ExportTextItem_Direct(Text, Helper.GetRawPtr(i), nullptr, nullptr, PPF_None); + Lines.Add(FString::Printf(TEXT(" [%d] = %s"), i, *Text)); + } + } + else + { + FString Text; + Prop->ExportTextItem_Direct(Text, Value, nullptr, nullptr, PPF_None); + Lines.Add(FString::Printf(TEXT("%s = %s"), *Name, *Text)); + } + } + + if (!FFileHelper::SaveStringArrayToFile(Lines, *Out, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + UE_LOG(LogLPDumpCDO, Error, TEXT("Ecriture impossible : %s"), *Out); + return 1; + } + UE_LOG(LogLPDumpCDO, Warning, TEXT("=== %s : %d ligne(s) -> %s ==="), *ClassPath, Lines.Num(), *Out); + return 0; +} diff --git a/Source/LifePlayEditorTools/LPDumpCDOCommandlet.h b/Source/LifePlayEditorTools/LPDumpCDOCommandlet.h new file mode 100644 index 0000000..c9aaf3b --- /dev/null +++ b/Source/LifePlayEditorTools/LPDumpCDOCommandlet.h @@ -0,0 +1,23 @@ +// Portage UE 5.5 - lot L7i (diagnostic getBeautyValue). +// +// Ecrit en texte des proprietes de l'objet par defaut (CDO) d'une classe Blueprint : un element +// de tableau ou une entree de map par ligne. Lecture seule. +// +// Usage : +// UnrealEditor-Cmd.exe -run=LPDumpCDO -Class=/Game/BPs/GI.GI_C -Out= -Props=Nom1,Nom2 + +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPDumpCDOCommandlet.generated.h" + +UCLASS() +class ULPDumpCDOCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPDumpCDOCommandlet(); + virtual int32 Main(const FString& Params) override; +}; diff --git a/Source/LifePlayEditorTools/LPFixStringSearchCommandlet.cpp b/Source/LifePlayEditorTools/LPFixStringSearchCommandlet.cpp new file mode 100644 index 0000000..7d4f747 --- /dev/null +++ b/Source/LifePlayEditorTools/LPFixStringSearchCommandlet.cpp @@ -0,0 +1,323 @@ +// Portage UE 5.5 - voir LPFixStringSearchCommandlet.h + +#include "LPFixStringSearchCommandlet.h" + +#include "AssetRegistry/AssetData.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "EdGraph/EdGraph.h" +#include "EdGraph/EdGraphPin.h" +#include "EdGraphSchema_K2.h" +#include "Engine/Blueprint.h" +#include "K2Node_CallFunction.h" +#include "Kismet2/BlueprintEditorUtils.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Kismet/KismetStringLibrary.h" +#include "LPStringCompat.h" +#include "Misc/PackageName.h" +#include "UObject/Package.h" +#include "UObject/SavePackage.h" + +DEFINE_LOG_CATEGORY_STATIC(LogLPFixStringSearch, Log, All); + +namespace +{ + /** Fonctions rebranchees : meme nom, memes broches dans UKismetStringLibrary et ULPStringCompat. */ + const FName FunctionNames[] = { FName(TEXT("Contains")), FName(TEXT("FindSubstring")), FName(TEXT("Split")) }; + + /** Extremites reliees a une broche, triees : "Noeud.Broche". */ + TArray Links(const UEdGraphPin* Pin) + { + TArray Out; + if (Pin) + { + for (const UEdGraphPin* L : Pin->LinkedTo) + { + Out.Add(FString::Printf(TEXT("%s.%s"), + L && L->GetOwningNode() ? *L->GetOwningNode()->GetName() : TEXT("?"), L ? *L->PinName.ToString() : TEXT("?"))); + } + } + Out.Sort(); + return Out; + } + + /** Etat comparable d'une broche : liaisons et valeur par defaut. */ + FString PinState(const UEdGraphPin* Pin) + { + return FString::Printf(TEXT("[%s] defaut='%s' objet=%s texte='%s'"), + *FString::Join(Links(Pin), TEXT(", ")), *Pin->DefaultValue, + Pin->DefaultObject ? *Pin->DefaultObject->GetPathName() : TEXT("None"), *Pin->DefaultTextValue.ToString()); + } + + /** Fonction de remplacement si le noeud appelle une des fonctions visees de UKismetStringLibrary, sinon nullptr. */ + UFunction* ReplacementFor(const UK2Node_CallFunction* Node) + { + if (!Node) + { + return nullptr; + } + const UClass* Owner = Node->FunctionReference.GetMemberParentClass(); + if (!Owner || !Owner->IsChildOf(UKismetStringLibrary::StaticClass())) + { + return nullptr; + } + const FName Name = Node->FunctionReference.GetMemberName(); + for (const FName& Wanted : FunctionNames) + { + if (Name == Wanted) + { + return ULPStringCompat::StaticClass()->FindFunctionByName(Wanted); + } + } + return nullptr; + } + + bool HasExecPins(const UEdGraphNode* Node) + { + for (const UEdGraphPin* Pin : Node->Pins) + { + if (Pin && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) + { + return true; + } + } + return false; + } +} + +ULPFixStringSearchCommandlet::ULPFixStringSearchCommandlet() +{ + IsClient = false; + IsServer = false; + IsEditor = true; + LogToConsole = true; +} + +int32 ULPFixStringSearchCommandlet::Main(const FString& Params) +{ + const bool bDryRun = Params.Contains(TEXT("DryRun"), ESearchCase::IgnoreCase); + + for (const FName& Wanted : FunctionNames) + { + if (!ULPStringCompat::StaticClass()->FindFunctionByName(Wanted)) + { + UE_LOG(LogLPFixStringSearch, Error, TEXT("ULPStringCompat::%s introuvable."), *Wanted.ToString()); + return 1; + } + } + TMap PerFunction; + + UE_LOG(LogLPFixStringSearch, Warning, TEXT("=== Contains / FindSubstring / Split -> LPStringCompat (UE4)%s ==="), + bDryRun ? TEXT(" [DRY RUN]") : TEXT("")); + + FAssetRegistryModule& AssetRegistryModule = + FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + AssetRegistryModule.Get().SearchAllAssets(true); + + TArray BlueprintAssets; + // Sous-classes incluses : couvre les UWidgetBlueprint. + AssetRegistryModule.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), BlueprintAssets, true); + + const UEdGraphSchema_K2* Schema = GetDefault(); + + int32 NodesFound = 0; + int32 NodesFixed = 0; + int32 Unhandled = 0; + int32 BlueprintsModified = 0; + int32 SaveFailures = 0; + + for (const FAssetData& AssetData : BlueprintAssets) + { + if (!AssetData.PackageName.ToString().StartsWith(TEXT("/Game/"))) + { + continue; + } + UBlueprint* Blueprint = Cast(AssetData.GetAsset()); + if (!Blueprint) + { + continue; + } + + TArray Graphs; + Blueprint->GetAllGraphs(Graphs); + + int32 FixedHere = 0; + int32 FailedHere = 0; + + for (UEdGraph* Graph : Graphs) + { + if (!Graph) + { + continue; + } + // Copie : on ajoute et on detruit des noeuds pendant le parcours. + TArray Snapshot = Graph->Nodes; + for (UEdGraphNode* GraphNode : Snapshot) + { + UK2Node_CallFunction* OldNode = Cast(GraphNode); + UFunction* NewFunction = ReplacementFor(OldNode); + if (!NewFunction) + { + continue; + } + ++NodesFound; + ++PerFunction.FindOrAdd(NewFunction->GetFName()); + + UE_LOG(LogLPFixStringSearch, Warning, TEXT(" %s :: %s -> %s %s (%d, %d)"), + *Blueprint->GetName(), *Graph->GetName(), *OldNode->GetName(), *NewFunction->GetName(), OldNode->NodePosX, OldNode->NodePosY); + + // Etat avant, par nom de broche (self exclu : cible implicite de la bibliotheque). + TMap Before; + for (const UEdGraphPin* Pin : OldNode->Pins) + { + if (Pin && Pin->PinName != UEdGraphSchema_K2::PN_Self) + { + Before.Add(Pin->PinName, PinState(Pin)); + } + } + const UEdGraphPin* OldSelf = OldNode->FindPin(UEdGraphSchema_K2::PN_Self, EGPD_Input); + + if (HasExecPins(OldNode) || (OldSelf && OldSelf->LinkedTo.Num() > 0)) + { + ++Unhandled; + ++FailedHere; + UE_LOG(LogLPFixStringSearch, Error, TEXT(" cas non pris en charge (%s) : noeud laisse en l'etat."), + HasExecPins(OldNode) ? TEXT("noeud impur") : TEXT("cible self reliee")); + continue; + } + if (bDryRun) + { + continue; + } + + UK2Node_CallFunction* NewNode = NewObject(Graph); + NewNode->SetFromFunction(NewFunction); + NewNode->NodePosX = OldNode->NodePosX; + NewNode->NodePosY = OldNode->NodePosY; + NewNode->CreateNewGuid(); + NewNode->PostPlacedNewNode(); + NewNode->AllocateDefaultPins(); + Graph->Modify(); + Graph->AddNode(NewNode, false, false); + + bool bOk = true; + for (UEdGraphPin* OldPin : OldNode->Pins) + { + if (!OldPin || OldPin->PinName == UEdGraphSchema_K2::PN_Self) + { + continue; + } + UEdGraphPin* NewPin = NewNode->FindPin(OldPin->PinName, OldPin->Direction); + if (!NewPin) + { + bOk = false; + UE_LOG(LogLPFixStringSearch, Error, TEXT(" broche %s sans equivalent."), *OldPin->PinName.ToString()); + break; + } + NewPin->DefaultValue = OldPin->DefaultValue; + NewPin->DefaultObject = OldPin->DefaultObject; + NewPin->DefaultTextValue = OldPin->DefaultTextValue; + if (OldPin->LinkedTo.Num() > 0 + && Schema->MovePinLinks(*OldPin, *NewPin).Response == CONNECT_RESPONSE_DISALLOW) + { + bOk = false; + UE_LOG(LogLPFixStringSearch, Error, TEXT(" liaison refusee sur %s."), *OldPin->PinName.ToString()); + break; + } + } + + // Meme ensemble de broches, memes liaisons (les noms des voisins ne changent pas), memes valeurs. + if (bOk) + { + int32 Compared = 0; + for (const UEdGraphPin* NewPin : NewNode->Pins) + { + if (!NewPin || NewPin->PinName == UEdGraphSchema_K2::PN_Self) + { + continue; + } + const FString* Expected = Before.Find(NewPin->PinName); + if (!Expected || *Expected != PinState(NewPin)) + { + bOk = false; + UE_LOG(LogLPFixStringSearch, Error, TEXT(" apres : %s %s (attendu %s)"), + *NewPin->PinName.ToString(), *PinState(NewPin), Expected ? **Expected : TEXT("")); + } + ++Compared; + } + if (Compared != Before.Num()) + { + bOk = false; + UE_LOG(LogLPFixStringSearch, Error, TEXT(" %d broche(s) apres, %d avant."), Compared, Before.Num()); + } + } + + if (!bOk) + { + // Le Blueprint ne sera pas sauvegarde : le paquet sur disque reste intact. + ++Unhandled; + ++FailedHere; + UE_LOG(LogLPFixStringSearch, Error, TEXT(" ecart : Blueprint non sauvegarde.")); + continue; + } + + OldNode->DestroyNode(); + ++NodesFixed; + ++FixedHere; + } + } + + if (FailedHere > 0) + { + UE_LOG(LogLPFixStringSearch, Error, TEXT(" -> %s : %d noeud(s) en ecart, Blueprint NON sauvegarde."), + *Blueprint->GetName(), FailedHere); + continue; + } + if (FixedHere == 0 || bDryRun) + { + continue; + } + + ++BlueprintsModified; + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint); + FKismetEditorUtilities::CompileBlueprint(Blueprint); + + UPackage* Package = Blueprint->GetOutermost(); + FString Filename; + if (!FPackageName::TryConvertLongPackageNameToFilename( + Package->GetName(), Filename, FPackageName::GetAssetPackageExtension())) + { + ++SaveFailures; + UE_LOG(LogLPFixStringSearch, Error, TEXT(" chemin introuvable pour %s"), *Package->GetName()); + continue; + } + + ResetLoaders(Package); + + FSavePackageArgs SaveArgs; + SaveArgs.TopLevelFlags = RF_Public | RF_Standalone; + SaveArgs.SaveFlags = SAVE_None; + SaveArgs.bWarnOfLongFilename = true; + // GWarn et non GError : en commandlet, GError rend toute erreur de save fatale. + SaveArgs.Error = GWarn; + + if (UPackage::SavePackage(Package, nullptr, *Filename, SaveArgs)) + { + UE_LOG(LogLPFixStringSearch, Warning, TEXT(" -> %s sauvegarde (%d noeud(s))."), *Package->GetName(), FixedHere); + } + else + { + ++SaveFailures; + UE_LOG(LogLPFixStringSearch, Error, TEXT(" -> ECHEC de sauvegarde de %s"), *Package->GetName()); + } + } + + for (const TPair& It : PerFunction) + { + UE_LOG(LogLPFixStringSearch, Warning, TEXT(" %s : %d noeud(s)"), *It.Key.ToString(), It.Value); + } + UE_LOG(LogLPFixStringSearch, Warning, + TEXT("=== Termine : %d noeud(s) trouve(s), %d remplace(s), %d Blueprint(s) modifie(s), %d non traite(s), %d echec(s) de sauvegarde. ==="), + NodesFound, NodesFixed, BlueprintsModified, Unhandled, SaveFailures); + + return (SaveFailures > 0 || Unhandled > 0) ? 1 : 0; +} diff --git a/Source/LifePlayEditorTools/LPFixStringSearchCommandlet.h b/Source/LifePlayEditorTools/LPFixStringSearchCommandlet.h new file mode 100644 index 0000000..cd91e83 --- /dev/null +++ b/Source/LifePlayEditorTools/LPFixStringSearchCommandlet.h @@ -0,0 +1,28 @@ +// Portage UE 5.5 - lot L7i (CX-B12). Voir Source/LifePlay/LPStringCompat.h. +// +// Rebranche chaque appel a UKismetStringLibrary::Contains, FindSubstring et Split sur la fonction +// homonyme de ULPStringCompat (memes broches, comportement 4.27 pour une sous-chaine vide), au meme +// emplacement. Liaisons et valeurs par defaut sont reportees broche par broche, par nom. +// +// Controle integre (repris de LPFixNavRandomPoint, CX-B11) : l'etat de chaque broche (liaisons, +// valeur par defaut) est compare avant et apres. Tout ecart, broche sans equivalent, cible self +// reliee ou noeud impur : le noeud est laisse en l'etat et son Blueprint n'est pas sauvegarde. +// +// Usage : +// UnrealEditor-Cmd.exe -run=LPFixStringSearch [-DryRun] + +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPFixStringSearchCommandlet.generated.h" + +UCLASS() +class ULPFixStringSearchCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPFixStringSearchCommandlet(); + virtual int32 Main(const FString& Params) override; +}; diff --git a/Source/LifePlayEditorTools/LPTestStringCompatCommandlet.cpp b/Source/LifePlayEditorTools/LPTestStringCompatCommandlet.cpp new file mode 100644 index 0000000..12b0759 --- /dev/null +++ b/Source/LifePlayEditorTools/LPTestStringCompatCommandlet.cpp @@ -0,0 +1,164 @@ +// Portage UE 5.5 - voir LPTestStringCompatCommandlet.h + +#include "LPTestStringCompatCommandlet.h" + +#include "Kismet/KismetStringLibrary.h" +#include "LPStringCompat.h" +#include + +DEFINE_LOG_CATEGORY_STATIC(LogLPTestStringCompat, Log, All); + +namespace +{ + /** TCString::Stristr, UE 4.27 (CString.h l. 622-650), transcrit a l'identique. */ + const TCHAR* Stristr427(const TCHAR* Str, const TCHAR* Find) + { + if (Find == nullptr || Str == nullptr) + { + return nullptr; + } + TCHAR FindInitial = FChar::ToUpper(*Find); + int32 Length = FCString::Strlen(Find++) - 1; + TCHAR StrChar = *Str++; + while (StrChar) + { + StrChar = FChar::ToUpper(StrChar); + if (StrChar == FindInitial && !FCString::Strnicmp(Str, Find, Length)) + { + return Str - 1; + } + StrChar = *Str++; + } + return nullptr; + } + + /** FString::Find, UE 4.27 (String.cpp l. 180-227), transcrit a l'identique. */ + int32 Find427(const FString& This, const TCHAR* SubStr, ESearchCase::Type SearchCase, ESearchDir::Type SearchDir, int32 StartPosition) + { + if (SubStr == nullptr) + { + return INDEX_NONE; + } + if (SearchDir == ESearchDir::FromStart) + { + const TCHAR* Start = *This; + if (StartPosition != INDEX_NONE && This.Len() > 0) + { + Start += FMath::Clamp(StartPosition, 0, This.Len() - 1); + } + // FCString::Strstr 4.27 sous Windows : FWindowsPlatformString::Strstr = _tcsstr (wcsstr). + const TCHAR* Tmp = SearchCase == ESearchCase::IgnoreCase + ? Stristr427(Start, SubStr) + : (const TCHAR*)wcsstr((const wchar_t*)Start, (const wchar_t*)SubStr); + return Tmp ? UE_PTRDIFF_TO_INT32(Tmp - *This) : INDEX_NONE; + } + if (SearchCase == ESearchCase::IgnoreCase) + { + return Find427(This.ToUpper(), *FString(SubStr).ToUpper(), ESearchCase::CaseSensitive, SearchDir, StartPosition); + } + const int32 SearchStringLength = FMath::Max(1, FCString::Strlen(SubStr)); + if (StartPosition == INDEX_NONE || StartPosition >= This.Len()) + { + StartPosition = This.Len(); + } + // En 4.27, (*this)[i+j] pouvait lire le '\0' final sans controle : on lit le tampon brut. + const TCHAR* Data = *This; + for (int32 i = StartPosition - SearchStringLength; i >= 0; i--) + { + int32 j; + for (j = 0; SubStr[j]; j++) + { + if (Data[i + j] != SubStr[j]) + { + break; + } + } + if (!SubStr[j]) + { + return i; + } + } + return INDEX_NONE; + } +} + +ULPTestStringCompatCommandlet::ULPTestStringCompatCommandlet() +{ + IsClient = false; + IsServer = false; + IsEditor = true; + LogToConsole = true; +} + +int32 ULPTestStringCompatCommandlet::Main(const FString& Params) +{ + const TArray Strings = { TEXT(""), TEXT("a"), TEXT("A"), TEXT("abc"), TEXT("ABC"), TEXT("aXbXc"), TEXT("round, oval"), TEXT(" "), TEXT("teen"), TEXT("Daz") }; + const TArray Subs = { TEXT(""), TEXT("a"), TEXT("A"), TEXT("b"), TEXT("X"), TEXT("bc"), TEXT("abc"), TEXT("abcd"), TEXT(", "), TEXT(" "), TEXT("oval"), TEXT("TEEN"), TEXT("daz") }; + const TArray Starts = { -1, 0, 1, 2, 5, 50 }; + + int32 Cases = 0, CompatMismatch = 0, EngineMismatch = 0; + for (const FString& S : Strings) + { + for (const FString& Sub : Subs) + { + for (int32 CaseIdx = 0; CaseIdx < 2; ++CaseIdx) + { + for (int32 DirIdx = 0; DirIdx < 2; ++DirIdx) + { + for (const int32 Start : Starts) + { + const ESearchCase::Type Case = CaseIdx ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase; + const ESearchDir::Type Dir = DirIdx ? ESearchDir::FromEnd : ESearchDir::FromStart; + ++Cases; + + const int32 Ref = Find427(S, *Sub, Case, Dir, Start); + const int32 Compat = ULPStringCompat::FindUE4(S, Sub, Case, Dir, Start); + const int32 Engine = S.Find(Sub, Case, Dir, Start); + + if (Compat != Ref) + { + ++CompatMismatch; + UE_LOG(LogLPTestStringCompat, Error, TEXT("COMPAT Find(\"%s\", \"%s\", %s, %s, %d) : 4.27=%d compat=%d"), + *S, *Sub, CaseIdx ? TEXT("Case") : TEXT("Ignore"), DirIdx ? TEXT("FromEnd") : TEXT("FromStart"), Start, Ref, Compat); + } + if (Engine != Ref) + { + ++EngineMismatch; + UE_LOG(LogLPTestStringCompat, Display, TEXT("moteur Find(\"%s\", \"%s\", %s, %s, %d) : 4.27=%d 5.5=%d"), + *S, *Sub, CaseIdx ? TEXT("Case") : TEXT("Ignore"), DirIdx ? TEXT("FromEnd") : TEXT("FromStart"), Start, Ref, Engine); + } + + // Fonctions Blueprint : uniquement avec la position de depart par defaut. + if (Start == -1) + { + const bool bUseCase = CaseIdx == 1, bFromEnd = DirIdx == 1; + if (ULPStringCompat::Contains(S, Sub, bUseCase, bFromEnd) != (Ref != INDEX_NONE) + || ULPStringCompat::FindSubstring(S, Sub, bUseCase, bFromEnd) != Ref) + { + ++CompatMismatch; + UE_LOG(LogLPTestStringCompat, Error, TEXT("COMPAT Contains/FindSubstring(\"%s\", \"%s\") ecart"), *S, *Sub); + } + FString L1, R1, L2, R2; + const bool B1 = ULPStringCompat::Split(S, Sub, L1, R1, Case, Dir); + const bool B2 = Ref >= 0; + if (B1 != B2 || (B2 && (L1 != S.Left(Ref) || R1 != S.Mid(Ref + Sub.Len())))) + { + ++CompatMismatch; + UE_LOG(LogLPTestStringCompat, Error, TEXT("COMPAT Split(\"%s\", \"%s\") ecart"), *S, *Sub); + } + // Cas Blueprint par defaut, moteur 5.5 contre 4.27 : documente l'ecart vise. + if (!bUseCase && !bFromEnd && UKismetStringLibrary::Contains(S, Sub) != (Ref != INDEX_NONE)) + { + UE_LOG(LogLPTestStringCompat, Display, TEXT("moteur Contains(\"%s\", \"%s\") : 4.27=%d 5.5=%d"), + *S, *Sub, Ref != INDEX_NONE, UKismetStringLibrary::Contains(S, Sub)); + } + } + } + } + } + } + } + + UE_LOG(LogLPTestStringCompat, Warning, TEXT("=== %d cas : compat != 4.27 : %d ; moteur 5.5 != 4.27 : %d ==="), Cases, CompatMismatch, EngineMismatch); + return CompatMismatch > 0 ? 1 : 0; +} diff --git a/Source/LifePlayEditorTools/LPTestStringCompatCommandlet.h b/Source/LifePlayEditorTools/LPTestStringCompatCommandlet.h new file mode 100644 index 0000000..2cd23c9 --- /dev/null +++ b/Source/LifePlayEditorTools/LPTestStringCompatCommandlet.h @@ -0,0 +1,25 @@ +// Portage UE 5.5 - lot L7i (CX-B12). Test de ULPStringCompat::FindUE4 / Contains / FindSubstring / Split. +// +// Compare, sur une grille de cas limites (chaines vides, casse, sens, position de depart) : +// - une transcription litterale de FString::Find de UE 4.27 (String.cpp 4.27 l. 180-227, avec +// TCString::Stristr 4.27 et wcsstr pour FCString::Strstr sous Windows) ; +// - ULPStringCompat (doit lui etre identique) ; +// - le moteur 5.5 (les ecarts sont listes, ils justifient le correctif). +// +// Usage : UnrealEditor-Cmd.exe -run=LPTestStringCompat + +#pragma once + +#include "CoreMinimal.h" +#include "Commandlets/Commandlet.h" +#include "LPTestStringCompatCommandlet.generated.h" + +UCLASS() +class ULPTestStringCompatCommandlet : public UCommandlet +{ + GENERATED_BODY() + +public: + ULPTestStringCompatCommandlet(); + virtual int32 Main(const FString& Params) override; +}; diff --git a/Tools/DiffRemplacementNoeuds.py b/Tools/DiffRemplacementNoeuds.py index eb1f7d3..fe66fb5 100644 --- a/Tools/DiffRemplacementNoeuds.py +++ b/Tools/DiffRemplacementNoeuds.py @@ -9,7 +9,7 @@ def parse(p): if key: objs[key]['lines'].append(line) return objs # Profils de remplacement : selection des anciens et nouveaux noeuds, correspondance des broches. -# Usage : DiffRemplacementNoeuds.py avant.txt apres.txt [L7a|L7b] (L7a par defaut) +# Usage : DiffRemplacementNoeuds.py avant.txt apres.txt [L7a|L7b|CX-B12] (L7a par defaut) PROFILS={ 'L7a':{'old':('K2Node_VariableGet',lambda l:'Engine.SkinnedMeshComponent\'",MemberName="SkeletalMesh"' in l), 'new':('K2Node_CallFunction',lambda l:'MemberName="GetSkeletalMesh"' in l and 'LPMeshCompat' in l), @@ -17,6 +17,9 @@ PROFILS={ 'L7b':{'old':('K2Node_CallFunction',lambda l:'MemberName="K2_GetRandomPointInNavigableRadius"' in l), 'new':('K2Node_CallFunction',lambda l:'MemberName="GetRandomLocationInNavigableRadius"' in l and 'LPNavigationHelpers' in l), 'pins':[(x,x) for x in ('WorldContextObject','Origin','RandomLocation','Radius','NavData','FilterClass','ReturnValue')]}, + 'CX-B12':{'old':('K2Node_CallFunction',lambda l:'KismetStringLibrary' in l and re.search(r'MemberName="(Contains|FindSubstring|Split)"',l) is not None), + 'new':('K2Node_CallFunction',lambda l:'LPStringCompat' in l and re.search(r'MemberName="(Contains|FindSubstring|Split)"',l) is not None), + 'pins':[(x,x) for x in ('SearchIn','Substring','bUseCase','bSearchFromEnd','StartPosition','SourceString','InStr','LeftS','RightS','SearchCase','SearchDir','ReturnValue')]}, } PROFIL=PROFILS[sys.argv[3] if len(sys.argv)>3 else 'L7a'] A=parse(sys.argv[1]); B=parse(sys.argv[2]) @@ -107,7 +110,8 @@ for k,v in old.items(): if p2==k[0] and gg==g: x=x.replace(o+'.',nn+'.') if x.split('.')[0]==o else x out.append(x) return sorted(out) - same=all(rn(pinlinks(v,op))==pinlinks(n,np_) for op,np_ in PROFIL['pins']) + # Broche absente des deux cotes (profil multi-fonctions : Split n'a pas SearchIn...) : egalite. + same=all((pinlinks(v,op) is None and pinlinks(n,np_) is None) or rn(pinlinks(v,op))==pinlinks(n,np_) for op,np_ in PROFIL['pins']) # Profil a broches homonymes : les valeurs par defaut doivent aussi etre reportees. if same and all(op==np_ for op,np_ in PROFIL['pins']): same=all(pinhead(v,op)==pinhead(n,op) for op,_ in PROFIL['pins']) if same: ok+=1