// Portage UE 5.5 - voir LPFixEditableTextBoxFontCommandlet.h #include "LPFixEditableTextBoxFontCommandlet.h" #include "AssetRegistry/AssetRegistryModule.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphPin.h" #include "EdGraphSchema_K2.h" #include "Engine/Blueprint.h" #include "K2Node_BreakStruct.h" #include "K2Node_MakeStruct.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/KismetEditorUtilities.h" #include "Misc/PackageName.h" #include "Styling/SlateTypes.h" #include "UObject/Package.h" #include "UObject/SavePackage.h" #include "UObject/UObjectGlobals.h" DEFINE_LOG_CATEGORY_STATIC(LogLPFixFont, Log, All); namespace { const TCHAR* OrphanPinName = TEXT("Font"); const TCHAR* TextStylePinName = TEXT("TextStyle"); /** Decrit une broche et ses liaisons, pour le rapport. */ FString DescribeLinks(const UEdGraphPin* Pin) { if (!Pin) { return TEXT("(broche absente)"); } if (Pin->LinkedTo.Num() == 0) { return FString::Printf(TEXT("non connectee (defaut: '%s')"), *Pin->DefaultValue); } TArray Targets; for (const UEdGraphPin* Linked : Pin->LinkedTo) { Targets.Add(FString::Printf(TEXT("%s.%s"), Linked->GetOwningNode() ? *Linked->GetOwningNode()->GetName() : TEXT("?"), *Linked->PinName.ToString())); } return FString::Printf(TEXT("%d liaison(s) -> %s"), Pin->LinkedTo.Num(), *FString::Join(Targets, TEXT(", "))); } } ULPFixEditableTextBoxFontCommandlet::ULPFixEditableTextBoxFontCommandlet() { IsClient = false; IsServer = false; IsEditor = true; LogToConsole = true; } int32 ULPFixEditableTextBoxFontCommandlet::Main(const FString& Params) { const bool bDryRun = Params.Contains(TEXT("DryRun"), ESearchCase::IgnoreCase); UScriptStruct* EditableStruct = TBaseStructure::Get(); if (!EditableStruct) { UE_LOG(LogLPFixFont, Error, TEXT("Structure FEditableTextBoxStyle introuvable.")); return 1; } UE_LOG(LogLPFixFont, Warning, TEXT("=== Rebranchement de la broche Font (EditableTextBoxStyle)%s ==="), bDryRun ? TEXT(" [DRY RUN]") : TEXT("")); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); AssetRegistryModule.Get().SearchAllAssets(true); TArray BlueprintAssets; AssetRegistryModule.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), BlueprintAssets, true); int32 NodesFound = 0; int32 NodesFixed = 0; int32 BlueprintsModified = 0; int32 SaveFailures = 0; int32 UnhandledLinks = 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; struct FPendingLink { UK2Node_BreakStruct* Break; UK2Node_MakeStruct* Make; }; TArray PendingLinks; TArray NodesToReconstruct; TArray FontPinsToClear; for (UEdGraph* Graph : Graphs) { if (!Graph) { continue; } TArray Snapshot = Graph->Nodes; for (UEdGraphNode* GraphNode : Snapshot) { UK2Node_MakeStruct* MakeNode = Cast(GraphNode); UK2Node_BreakStruct* BreakNode = Cast(GraphNode); const bool bIsTarget = (MakeNode && MakeNode->StructType == EditableStruct) || (BreakNode && BreakNode->StructType == EditableStruct); if (!bIsTarget) { continue; } UEdGraphPin* FontPin = GraphNode->FindPin(OrphanPinName); if (!FontPin) { continue; // deja corrige } ++NodesFound; UE_LOG(LogLPFixFont, Warning, TEXT(" %s :: %s -> %s"), *Blueprint->GetName(), *Graph->GetName(), *GraphNode->GetName()); UE_LOG(LogLPFixFont, Warning, TEXT(" broche Font : %s"), *DescribeLinks(FontPin)); UE_LOG(LogLPFixFont, Warning, TEXT(" broche TextStyle : %s"), *DescribeLinks(GraphNode->FindPin(TextStylePinName))); if (bDryRun) { continue; } // Topologie relevee sur CharGen et StatMenu : // [Break EditableTextBoxStyle].Font -> [Make EditableTextBoxStyle].Font // La police n'est pas construite, elle est recopiee d'un style vers un autre. // Les deux extremites etant la meme structure et le champ ayant migre dans // TextStyle, la correspondance fidele est TextStyle -> TextStyle : aucun // noeud intermediaire n'est necessaire, et le reste du style suit. if (MakeNode) { for (UEdGraphPin* Source : FontPin->LinkedTo) { UK2Node_BreakStruct* SourceBreak = Cast(Source->GetOwningNode()); if (SourceBreak && SourceBreak->StructType == EditableStruct) { PendingLinks.Add({ SourceBreak, MakeNode }); } else { UE_LOG(LogLPFixFont, Error, TEXT(" source Font inattendue (%s) : rebranchement a faire a la main."), Source->GetOwningNode() ? *Source->GetOwningNode()->GetName() : TEXT("?")); ++UnhandledLinks; } } } // Les liaisons ne sont coupees qu'APRES le releve complet (voir plus bas) : // couper ici la broche Font d'un Break effacait aussi la liaison vue depuis // les Make non encore parcourus, qui perdaient alors leur police sans // rebranchement (CharGen, Make_64 - defaut releve le 2026-09-29, L4-CX-B02). FontPinsToClear.AddUnique(FontPin); NodesToReconstruct.AddUnique(GraphNode); ++NodesFixed; ++FixedHere; } } // ReconstructNode conserve une broche orpheline dans DEUX cas : si elle // porte des liaisons, et si elle porte une valeur differente du defaut. // C'est le garde-fou d'UE contre la perte de donnees. Il faut donc lever // les deux, APRES avoir releve toute la topologie et AVANT de reconstruire, // sans quoi la broche Font survit et l'erreur persiste. for (UEdGraphPin* FontPin : FontPinsToClear) { FontPin->BreakAllPinLinks(); GetDefault()->ResetPinToAutogeneratedDefaultValue(FontPin, false); } // La reconstruction invalide les broches : on la fait en bloc, puis on relie. for (UEdGraphNode* Node : NodesToReconstruct) { Node->ReconstructNode(); } for (const FPendingLink& Link : PendingLinks) { UEdGraphPin* From = Link.Break->FindPin(TextStylePinName, EGPD_Output); UEdGraphPin* To = Link.Make->FindPin(TextStylePinName, EGPD_Input); if (From && To) { // Idempotent : une seconde execution ne doit pas doubler la liaison. if (!From->LinkedTo.Contains(To)) { From->MakeLinkTo(To); } UE_LOG(LogLPFixFont, Warning, TEXT(" %s.TextStyle -> %s.TextStyle"), *Link.Break->GetName(), *Link.Make->GetName()); } else { ++UnhandledLinks; UE_LOG(LogLPFixFont, Error, TEXT(" TextStyle introuvable sur %s ou %s."), *Link.Break->GetName(), *Link.Make->GetName()); } } 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(LogLPFixFont, 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(LogLPFixFont, Warning, TEXT(" -> %s sauvegarde (%d noeud(s))."), *Package->GetName(), FixedHere); } else { ++SaveFailures; UE_LOG(LogLPFixFont, Error, TEXT(" -> ECHEC de sauvegarde de %s"), *Package->GetName()); } } } UE_LOG(LogLPFixFont, Warning, TEXT("=== Termine : %d noeud(s) trouve(s), %d traite(s) dans %d Blueprint(s), %d liaison(s) non geree(s), %d echec(s) de sauvegarde. ==="), NodesFound, NodesFixed, BlueprintsModified, UnhandledLinks, SaveFailures); return (SaveFailures > 0 || UnhandledLinks > 0) ? 1 : 0; }