L4-CX-B08 : ParseIntoArray au comportement UE 4.27 (cause d'ERR-027)

LPStringCompat::ParseIntoArray (algorithme 4.27 : chaine vide -> [], separateur
sensible a la casse) ; commandlet LPFixParseIntoArray : 114 noeuds rebranches dans
17 Blueprints, 0 ecart de liaison (controle integre), 0 broche orpheline,
CompileAllBlueprints 0 erreur. LPDumpFunction : option -Exec.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
edujiro5972 2026-09-29 23:56:16 +02:00
parent d4eea7397b
commit 0d703d47a7
22 changed files with 314 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -325,3 +325,20 @@ entre 4.27 et 5.5 ; la divergence est dans le moteur.
Preuves : `Saved/Logs/portage/L5c_partie_utilisateur2_20260929.log`, `etat/L5-diag_dump55exec_GI_GeneratePerson.txt`,
`etat/L5-diag_dump55exec_GI_GenerateAndApplyStrings.txt`.
### Sous-lot L4-CX-B08 — `ParseIntoArray` au comportement UE 4.27 (ERR-027) — validé statiquement
Tag d'entrée `L4-CX-B08-avant`. Sauvegarde : `D:\Game\LifePlay_Backup_5.5\L4-CX-B08\` (17 Blueprints ; 4 fichiers
source créés, retirés par `Restore.ps1` en cas de retour).
| Étape | Résultat | Preuve (`etat/`) |
|---|---|---|
| Fonction de remplacement | `ULPStringCompat::ParseIntoArray` : transcription de l'algorithme 4.27 (chaîne vide → `[]` ; séparateur sensible à la casse), même signature que `UKismetStringLibrary::ParseIntoArray` | `Source/LifePlay/LPStringCompat.*` |
| Simulation | 114 nœuds, 17 Blueprints (GI 73, Person 10, Room 7, StreetMap_Static 4, 13 widgets) ; rien écrit | `L4-CX-B08_dryrun.txt` |
| Application | 114 rebranchés, 0 écart de liaison (contrôle intégré : liaisons et valeurs par défaut de chaque broche identiques avant/après), 0 échec | `L4-CX-B08_application.txt` |
| Contrôles | 0 broche orpheline ; `CompileAllBlueprints` 0 erreur, 95 avertissements (inchangé), 0 échec de chargement | résumés `L4-CX-B08_*` |
| ID | Date | Lot | Fichiers | Raison | Retour arrière |
|---|---|---|---|---|---|
| CHG-029 | 2026-09-29 | L4-CX-B08 | `Source/LifePlay/LPStringCompat.h/.cpp`, `Source/LifePlayEditorTools/LPFixParseIntoArrayCommandlet.h/.cpp` (nouveaux) ; 17 Blueprints | `ParseIntoArray` UE5 renvoie `[""]` pour une chaîne vide (UE4 : `[]`) : `generatePerson()` sans argument chargeait les 141 presets | `Tools\Restore.ps1 -Lot L4-CX-B08` ou `git checkout L4-CX-B08-avant -- Content Source` |
| CHG-030 | 2026-09-29 | L5 | `Source/LifePlayEditorTools/LPDumpFunctionCommandlet.cpp` | option `-Exec` (broches d'exécution) | `Tools\Restore.ps1 -Lot L5-dumpexec` |

View file

@ -0,0 +1,27 @@
// Portage UE 5.5 - voir LPStringCompat.h
#include "LPStringCompat.h"
TArray<FString> ULPStringCompat::ParseIntoArray(const FString& SourceString, const FString& Delimiter, const bool CullEmptyStrings)
{
// Transcription de FString::ParseIntoArray, UE 4.27 (String.cpp, lignes 940-963).
TArray<FString> OutArray;
const TCHAR* Start = *SourceString;
const int32 DelimLength = Delimiter.Len();
if (Start && *Start != TEXT('\0') && DelimLength)
{
while (const TCHAR* At = FCString::Strstr(Start, *Delimiter))
{
if (!CullEmptyStrings || At - Start)
{
OutArray.Emplace(UE_PTRDIFF_TO_INT32(At - Start), Start);
}
Start = At + DelimLength;
}
if (!CullEmptyStrings || *Start)
{
OutArray.Emplace(Start);
}
}
return OutArray;
}

View file

@ -0,0 +1,32 @@
// Portage UE 5.5 (2026.9 beta) - correctif CX-B08 (ERR-027).
//
// UE 5.x a reecrit FString::ParseIntoArray sur UE::String::ParseTokens. Deux comportements
// ont change par rapport a UE 4.27, sans aucun avertissement :
// 1. chaine source vide, CullEmpty=false : UE4 renvoie [] ; UE5 renvoie [""] ;
// 2. recherche du separateur : UE4 respecte la casse (FCString::Strstr) ; UE5 l'ignore.
//
// Le cas 1 casse generatePerson() sans argument : "()" donne Presets=[""], GeneratePerson
// charge alors les 141 presets (suffixe "" + ".lpcharacter") et finit sur un corps legacy
// jamais charge -> personnage invisible et devetu.
//
// ULPStringCompat::ParseIntoArray reproduit a l'identique l'algorithme 4.27 (Source 4.27,
// 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.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "LPStringCompat.generated.h"
UCLASS()
class LIFEPLAY_API ULPStringCompat : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
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<FString> ParseIntoArray(const FString& SourceString, const FString& Delimiter = FString(TEXT(" ")), const bool CullEmptyStrings = true);
};

View file

@ -0,0 +1,210 @@
// Portage UE 5.5 - voir LPFixParseIntoArrayCommandlet.h
#include "LPFixParseIntoArrayCommandlet.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
#include "Engine/Blueprint.h"
#include "K2Node_CallFunction.h"
#include "Kismet/KismetStringLibrary.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "LPStringCompat.h"
#include "Misc/PackageName.h"
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
#include "UObject/UObjectGlobals.h"
DEFINE_LOG_CATEGORY_STATIC(LogLPFixParse, Log, All);
namespace
{
const FName ParseIntoArrayName(TEXT("ParseIntoArray"));
/** Liaisons et valeur par defaut de chaque broche, par nom : l'empreinte a retrouver. */
TMap<FName, FString> Topology(const UEdGraphNode* Node)
{
TMap<FName, FString> Out;
for (const UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bOrphanedPin)
{
continue;
}
TArray<FString> Links;
for (const UEdGraphPin* L : Pin->LinkedTo)
{
Links.Add(FString::Printf(TEXT("%s.%s"),
L && L->GetOwningNode() ? *L->GetOwningNode()->GetName() : TEXT("?"),
L ? *L->PinName.ToString() : TEXT("?")));
}
Links.Sort();
Out.Add(Pin->PinName, FString::Printf(TEXT("[%s] defaut='%s'"), *FString::Join(Links, TEXT(", ")), *Pin->DefaultValue));
}
return Out;
}
}
ULPFixParseIntoArrayCommandlet::ULPFixParseIntoArrayCommandlet()
{
IsClient = false;
IsServer = false;
IsEditor = true;
LogToConsole = true;
}
int32 ULPFixParseIntoArrayCommandlet::Main(const FString& Params)
{
const bool bDryRun = Params.Contains(TEXT("DryRun"), ESearchCase::IgnoreCase);
UFunction* OldFunction = UKismetStringLibrary::StaticClass()->FindFunctionByName(ParseIntoArrayName);
UFunction* NewFunction = ULPStringCompat::StaticClass()->FindFunctionByName(ParseIntoArrayName);
if (!OldFunction || !NewFunction)
{
UE_LOG(LogLPFixParse, Error, TEXT("Fonction introuvable (ancienne=%s, nouvelle=%s)."),
OldFunction ? TEXT("ok") : TEXT("NULL"), NewFunction ? TEXT("ok") : TEXT("NULL"));
return 1;
}
UE_LOG(LogLPFixParse, Warning, TEXT("=== KismetStringLibrary::ParseIntoArray -> LPStringCompat::ParseIntoArray%s ==="),
bDryRun ? TEXT(" [DRY RUN]") : TEXT(""));
FAssetRegistryModule& AssetRegistryModule =
FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
AssetRegistryModule.Get().SearchAllAssets(true);
TArray<FAssetData> BlueprintAssets;
AssetRegistryModule.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), BlueprintAssets, true);
int32 NodesFound = 0;
int32 NodesFixed = 0;
int32 Unhandled = 0;
int32 BlueprintsModified = 0;
int32 SaveFailures = 0;
for (const FAssetData& AssetData : BlueprintAssets)
{
if (!AssetData.PackageName.ToString().StartsWith(TEXT("/Game/")))
{
continue;
}
UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset());
if (!Blueprint)
{
continue;
}
TArray<UEdGraph*> Graphs;
Blueprint->GetAllGraphs(Graphs);
int32 FixedHere = 0;
int32 FailedHere = 0;
for (UEdGraph* Graph : Graphs)
{
if (!Graph)
{
continue;
}
TArray<UEdGraphNode*> Snapshot = Graph->Nodes;
for (UEdGraphNode* GraphNode : Snapshot)
{
UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(GraphNode);
if (!CallNode || CallNode->GetTargetFunction() != OldFunction)
{
continue;
}
++NodesFound;
const TMap<FName, FString> Before = Topology(CallNode);
UE_LOG(LogLPFixParse, Warning, TEXT(" %s :: %s -> %s | Delimiter %s | CullEmptyStrings %s"),
*Blueprint->GetName(), *Graph->GetName(), *CallNode->GetName(),
*Before.FindRef(TEXT("Delimiter")), *Before.FindRef(TEXT("CullEmptyStrings")));
if (bDryRun)
{
continue;
}
CallNode->Modify();
CallNode->SetFromFunction(NewFunction);
CallNode->ReconstructNode();
const TMap<FName, FString> After = Topology(CallNode);
bool bSame = Before.Num() == After.Num();
for (const TPair<FName, FString>& It : Before)
{
const FString* A = After.Find(It.Key);
if (!A || *A != It.Value)
{
bSame = false;
UE_LOG(LogLPFixParse, Error, TEXT(" broche %s : avant %s | apres %s"),
*It.Key.ToString(), *It.Value, A ? **A : TEXT("(absente)"));
}
}
if (bSame && CallNode->GetTargetFunction() == NewFunction)
{
++NodesFixed;
++FixedHere;
}
else
{
++Unhandled;
++FailedHere;
}
}
}
if (FailedHere > 0)
{
// Rien n'est sauvegarde : le paquet reste tel qu'il est sur disque.
UE_LOG(LogLPFixParse, Error, TEXT(" -> %s : %d noeud(s) en ecart, Blueprint NON sauvegarde."),
*Blueprint->GetName(), FailedHere);
continue;
}
if (FixedHere > 0 && !bDryRun)
{
++BlueprintsModified;
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
FKismetEditorUtilities::CompileBlueprint(Blueprint);
UPackage* Package = Blueprint->GetOutermost();
FString Filename;
if (!FPackageName::TryConvertLongPackageNameToFilename(
Package->GetName(), Filename, FPackageName::GetAssetPackageExtension()))
{
++SaveFailures;
UE_LOG(LogLPFixParse, 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(LogLPFixParse, Warning, TEXT(" -> %s sauvegarde (%d noeud(s))."), *Package->GetName(), FixedHere);
}
else
{
++SaveFailures;
UE_LOG(LogLPFixParse, Error, TEXT(" -> ECHEC de sauvegarde de %s"), *Package->GetName());
}
}
}
UE_LOG(LogLPFixParse, Warning,
TEXT("=== Termine : %d noeud(s) trouve(s), %d rebranche(s), %d Blueprint(s) modifie(s), %d en ecart, %d echec(s) de sauvegarde. ==="),
NodesFound, NodesFixed, BlueprintsModified, Unhandled, SaveFailures);
return (SaveFailures > 0 || Unhandled > 0) ? 1 : 0;
}

View file

@ -0,0 +1,28 @@
// Portage UE 5.5 - correctif CX-B08 (ERR-027). Voir Source/LifePlay/LPStringCompat.h.
//
// Rebranche chaque noeud KismetStringLibrary::ParseIntoArray des Blueprints de /Game vers
// LPStringCompat::ParseIntoArray (comportement UE 4.27). Memes noms de broches : le
// rebranchement se limite a un changement de reference de fonction suivi d'un ReconstructNode.
//
// Controle integre : avant modification, les liaisons et valeurs par defaut de chaque broche
// sont relevees ; apres reconstruction, elles doivent etre retrouvees a l'identique. Tout
// ecart compte le noeud comme non traite et le Blueprint n'est pas sauvegarde.
//
// Usage :
// UnrealEditor-Cmd.exe <projet> -run=LPFixParseIntoArray [-DryRun]
#pragma once
#include "CoreMinimal.h"
#include "Commandlets/Commandlet.h"
#include "LPFixParseIntoArrayCommandlet.generated.h"
UCLASS()
class ULPFixParseIntoArrayCommandlet : public UCommandlet
{
GENERATED_BODY()
public:
ULPFixParseIntoArrayCommandlet();
virtual int32 Main(const FString& Params) override;
};