#include "IronsMigrationToolset.h" #include "IronsGame.h" #include "Editor.h" #include "FileHelpers.h" #include "Engine/World.h" #include "Engine/Engine.h" #include "Engine/StaticMesh.h" #include "GameFramework/WorldSettings.h" #include "AssetToolsModule.h" #include "AssetImportTask.h" #include "Factories/MaterialFactoryNew.h" #include "WidgetBlueprintFactory.h" #include "WidgetBlueprint.h" #include "Kismet2/KismetEditorUtilities.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Materials/Material.h" #include "Materials/MaterialExpressionVectorParameter.h" #include "Materials/MaterialExpressionConstant.h" #include "UObject/SavePackage.h" #include "Misc/Paths.h" #include "Misc/FileHelper.h" #include "HAL/FileManager.h" #include "Kismet/GameplayStatics.h" #include "Serialization/JsonSerializer.h" #include "Misc/PackageName.h" #include "Modules/ModuleManager.h" #include "ToolsetRegistry/UToolsetRegistry.h" namespace { bool Bound(){return FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath()).Equals(TEXT("D:/vibeCoding/ueRouge/ueRouge.uproject"),ESearchCase::IgnoreCase);} bool Save(UObject* Object){auto Package=Object->GetOutermost();FString File=FPackageName::LongPackageNameToFilename(Package->GetName(),FPackageName::GetAssetPackageExtension());FSavePackageArgs Args;Args.TopLevelFlags=RF_Public|RF_Standalone;return UPackage::SavePackage(Package,Object,*File,Args);} AIronsController* Controller(){return GEditor&&GEditor->PlayWorld?Cast(UGameplayStatics::GetPlayerController(GEditor->PlayWorld,0)):nullptr;} } FString UIronsMigrationToolset::Bootstrap(){ if(!Bound()||!GEditor||GEditor->PlayWorld)return TEXT("ERROR: exact project required, PIE must be stopped"); FAssetToolsModule& Module=FModuleManager::LoadModuleChecked(TEXT("AssetTools"));auto& Tools=Module.Get(); for(const TCHAR* Path:{TEXT("/Game/IRONSPORE/Art/Mechs/SM_MechScout.SM_MechScout"),TEXT("/Game/IRONSPORE/Art/Environment/SM_HangarBay.SM_HangarBay")})if(auto Mesh=LoadObject(nullptr,Path)){if(Mesh->GetNumSourceModels()>0){auto& Settings=Mesh->GetSourceModel(0).BuildSettings;Settings.bRecomputeNormals=true;Settings.bRecomputeTangents=true;Settings.bRemoveDegenerates=true;Settings.bUseMikkTSpace=false;Mesh->PostEditChange();Save(Mesh);}} FString MaterialPath=TEXT("/Game/IRONSPORE/Art/Materials/M_Industrial"); if(!LoadObject(nullptr,*(MaterialPath+TEXT(".M_Industrial")))){ auto Factory=NewObject();auto M=Cast(Tools.CreateAsset(TEXT("M_Industrial"),TEXT("/Game/IRONSPORE/Art/Materials"),UMaterial::StaticClass(),Factory));if(!M)return TEXT("ERROR: material"); auto Color=NewObject(M);Color->ParameterName=TEXT("BaseColor");Color->DefaultValue=FLinearColor(.13f,.19f,.23f);M->GetExpressionCollection().AddExpression(Color);M->GetEditorOnlyData()->BaseColor.Expression=Color; auto Rough=NewObject(M);Rough->R=.75f;M->GetExpressionCollection().AddExpression(Rough);M->GetEditorOnlyData()->Roughness.Expression=Rough;M->PostEditChange();if(!Save(M))return TEXT("ERROR: save material"); } FString WidgetPath=TEXT("/Game/IRONSPORE/UI/WBP_IronsRoot.WBP_IronsRoot");if(!LoadObject(nullptr,*WidgetPath)){auto F=NewObject();F->ParentClass=UIronsScreen::StaticClass();auto W=Tools.CreateAsset(TEXT("WBP_IronsRoot"),TEXT("/Game/IRONSPORE/UI"),UWidgetBlueprint::StaticClass(),F);if(!W||!Save(W))return TEXT("ERROR: widget");} const TPair Classes[]={ {TEXT("BP_IronsCharacter"),AIronsCharacter::StaticClass()},{TEXT("BP_IronsGameMode"),AIronsGameMode::StaticClass()} }; for(const auto& C:Classes){FString Path=TEXT("/Game/IRONSPORE/Blueprints/")+C.Key;if(!LoadObject(nullptr,*(Path+TEXT(".")+C.Key))){auto Package=CreatePackage(*Path);auto BP=FKismetEditorUtilities::CreateBlueprint(C.Value,Package,*C.Key,BPTYPE_Normal,UBlueprint::StaticClass(),UBlueprintGeneratedClass::StaticClass());if(!BP)return TEXT("ERROR: blueprint");FAssetRegistryModule::AssetCreated(BP);FKismetEditorUtilities::CompileBlueprint(BP);if(!Save(BP))return TEXT("ERROR: save blueprint");}} const TCHAR* Names[]={TEXT("L_Hangar"),TEXT("L_TrainingRange"),TEXT("L_Warzone")};for(auto Name:Names){FString Path=FString(TEXT("/Game/IRONSPORE/Maps/"))+Name;if(FPackageName::DoesPackageExist(Path))continue;UWorld* W=UEditorLoadingAndSavingUtils::NewBlankMap(false);if(!W)return TEXT("ERROR: new map");W->GetWorldSettings()->DefaultGameMode=AIronsGameMode::StaticClass();if(!UEditorLoadingAndSavingUtils::SaveMap(W,Path))return TEXT("ERROR: save map");} UEditorLoadingAndSavingUtils::LoadMap(TEXT("/Game/IRONSPORE/Maps/L_Hangar"));UEditorLoadingAndSavingUtils::SaveDirtyPackages(true,true);return TEXT("OK: three maps, UMG and Blueprint children, industrial material saved"); } FString UIronsMigrationToolset::ImportVisuals(){ if(!Bound()||!GEditor||GEditor->PlayWorld)return TEXT("ERROR: exact project required, PIE must be stopped"); auto& Tools=FModuleManager::LoadModuleChecked(TEXT("AssetTools")).Get();TArray Tasks; for(const auto& Spec:TArray>{{TEXT("SourceAssets/Derived/UI"),TEXT("/Game/IRONSPORE/UI/Icons")},{TEXT("SourceAssets/Audio/Generated"),TEXT("/Game/IRONSPORE/Audio")}}){TArray Files;IFileManager::Get().FindFilesRecursive(Files,*(FPaths::ProjectDir()/Spec.Key),TEXT("*.*"),true,false);for(const auto& File:Files){FString Ext=FPaths::GetExtension(File);if(Ext!=TEXT("png")&&Ext!=TEXT("wav"))continue;auto T=NewObject();T->Filename=File;T->DestinationPath=Spec.Value;T->DestinationName=FPaths::GetBaseFilename(File);T->bAutomated=true;T->bReplaceExisting=true;T->bSave=true;Tasks.Add(T);}} Tools.ImportAssetTasks(Tasks);int Count=0;for(auto T:Tasks)Count+=T->GetObjects().Num();UEditorLoadingAndSavingUtils::SaveDirtyPackages(true,true);return FString::Printf(TEXT("OK: %d imported objects"),Count); } FString UIronsMigrationToolset::CleanupTemplateRemainders(){ if(!Bound()||!GEditor||GEditor->PlayWorld)return TEXT("ERROR: exact project required, PIE must be stopped"); if(!FPackageName::DoesPackageExist(TEXT("/Game/IRONSPORE/Maps/L_Hangar"))||FPackageName::DoesPackageExist(TEXT("/Game/DemoTemplate/_Core/Lvl_IntroRoom")))return TEXT("ERROR: replacement must exist and template world must already be removed"); FString Json;TSharedPtr Root;if(!FFileHelper::LoadFileToString(Json,*(FPaths::ProjectDir()/TEXT("Docs/QA/UE-Migration/template-manifest.json")))||!FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(Json),Root))return TEXT("ERROR: reviewed template manifest required"); const TArray>* Roots=nullptr;if(!Root->TryGetArrayField(TEXT("roots"),Roots))return TEXT("ERROR: no manifest roots"); FString Content=FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir());FPaths::NormalizeDirectoryName(Content);TArray DeleteFiles;TArray Directories; const TArray Allowed={TEXT("DemoTemplate/"),TEXT("FirstPerson/"),TEXT("Characters/"),TEXT("Input/"),TEXT("LevelPrototyping/"),TEXT("__ExternalActors__/"),TEXT("__ExternalObjects__/"),TEXT("Localization/Game/")}; for(auto& Value:*Roots){auto Entry=Value->AsObject();if(!Entry)return TEXT("ERROR: malformed entry");FString Virtual=Entry->GetStringField(TEXT("root"));FString RelativeRoot=Virtual;RelativeRoot.RemoveFromStart(TEXT("/Game/"));FString Directory=Content/TEXT("")+RelativeRoot;Directories.Add(Directory); const TArray>* Files=nullptr;if(!Entry->TryGetArrayField(TEXT("files"),Files))return TEXT("ERROR: no files"); for(auto& Item:*Files){FString Relative=Item->AsString();bool Approved=false;for(auto& Prefix:Allowed)if(Relative.StartsWith(Prefix))Approved=true;if(!Approved||Relative.Contains(TEXT("..")))return TEXT("ERROR: manifest path outside explicit template roots"); FString File=FPaths::ConvertRelativePathToFull(Content/Relative);FPaths::NormalizeFilename(File);FPaths::CollapseRelativeDirectories(File);if(!File.StartsWith(Content+TEXT("/")))return TEXT("ERROR: absolute target escaped Content");if(!IFileManager::Get().FileExists(*File))continue; FString Ext=FPaths::GetExtension(File);if(Ext==TEXT("uasset")||Ext==TEXT("umap")){if(!Relative.StartsWith(TEXT("__ExternalActors__/DemoTemplate/_Core/Lvl_IntroRoom/")))return TEXT("ERROR: remaining regular assets must be removed by EditorAssetSubsystem first");} DeleteFiles.Add(File); } } CollectGarbage(RF_NoFlags); int Removed=0;for(auto& File:DeleteFiles)if(IFileManager::Get().Delete(*File,true,false,true))Removed++;else return TEXT("ERROR: file deletion failed inside reviewed template manifest"); for(auto& Directory:Directories){TArray Remain;IFileManager::Get().FindFilesRecursive(Remain,*Directory,TEXT("*.*"),true,false);if(Remain.IsEmpty()&&Directory.StartsWith(Content+TEXT("/")))IFileManager::Get().DeleteDirectory(*Directory,false,true);} FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get().ScanPathsSynchronous({TEXT("/Game")},true); return FString::Printf(TEXT("OK: %d manifest-listed remnants removed through bound native editor tool"),Removed); } FString UIronsMigrationToolset::Snapshot(){ if(!Bound())return TEXT("ERROR: wrong project");FString Result=TEXT("project=")+FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath());Result+=TEXT("\nmap=")+(GEditor&&GEditor->GetEditorWorldContext().World()?GEditor->GetEditorWorldContext().World()->GetPathName():TEXT("none"));Result+=FString::Printf(TEXT("\nPIE=%s"),GEditor&&GEditor->PlayWorld?TEXT("true"):TEXT("false"));if(auto C=Controller()){auto H=C->Host();Result+=TEXT("\npage=")+C->Page+TEXT("\ndataRoot=")+H->DataRoot+TEXT("\nprofile=")+UTF8_TO_TCHAR(gf::encode(H->Core().state()).c_str());}return Result; } FString UIronsMigrationToolset::Acceptance(const FString& Command,const FString& Argument){ if(!Bound())return TEXT("ERROR: wrong project");auto C=Controller();if(!C)return TEXT("ERROR: no PIE controller");auto H=C->Host();if(!H||!H->DataRoot.Contains(TEXT("/QA/")))return TEXT("ERROR: isolated QA data root required"); if(Command==TEXT("page"))C->SetPage(Argument); else if(Command==TEXT("language"))H->SetLanguage(Argument==TEXT("en")); else if(Command==TEXT("stock"))H->Accept(H->Core().seedAcceptanceStock()); else if(Command==TEXT("launch"))C->Launch(Argument==TEXT("training")); else if(Command==TEXT("loot"))H->Accept(H->Core().addLoot("alloy",4,"ue-acceptance-crate")); else if(Command==TEXT("pod")){std::vector V;for(auto& I:H->Core().state().run.cargo)if(gf::Grid::material(I.itemId))V.push_back(I.uid);H->Accept(H->Core().recoverPod(V));} else if(Command==TEXT("return")){if(H->FinishSortie(false))C->ReturnHome();} else if(Command==TEXT("destroy")){if(H->FinishSortie(true))C->ReturnHome();} else if(Command==TEXT("home"))C->ReturnHome(); else if(Command==TEXT("view")){if(C->Mech())C->Mech()->SwitchView();} else return TEXT("ERROR: unknown acceptance command"); return TEXT("OK: ")+Command; } class FIronsEditorModule : public IModuleInterface { void StartupModule()override{UToolsetRegistry::RegisterToolsetClass(UIronsMigrationToolset::StaticClass());} void ShutdownModule()override{UToolsetRegistry::UnregisterToolsetClass(UIronsMigrationToolset::StaticClass());} }; IMPLEMENT_MODULE(FIronsEditorModule,IronsEditor)