// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "EngineDefines.h" #include "Misc/MessageDialog.h" #include "Misc/FileHelper.h" #include "Misc/Paths.h" #include "Misc/ScopedSlowTask.h" #include "Misc/App.h" #include "Modules/ModuleManager.h" #include "UObject/ObjectMacros.h" #include "UObject/Object.h" #include "UObject/Class.h" #include "UObject/Package.h" #include "UObject/UnrealType.h" #include "InputCoreTypes.h" #include "Input/Reply.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/SWidget.h" #include "Widgets/SCompoundWidget.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SWindow.h" #include "Layout/WidgetPath.h" #include "Framework/Application/MenuStack.h" #include "Framework/Application/SlateApplication.h" #include "Textures/SlateIcon.h" #include "Framework/Commands/UIAction.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Text/STextBlock.h" #include "Framework/MultiBox/MultiBoxBuilder.h" #include "Widgets/Input/SButton.h" #include "EditorStyleSet.h" #include "GameFramework/Actor.h" #include "RawIndexBuffer.h" #include "Model.h" #include "CookOnTheSide/CookOnTheFlyServer.h" #include "Builders/CubeBuilder.h" #include "Settings/LevelEditorViewportSettings.h" #include "Settings/LevelEditorMiscSettings.h" #include "Engine/Brush.h" #include "AssetData.h" #include "Editor/EditorEngine.h" #include "ISourceControlModule.h" #include "Editor/UnrealEdEngine.h" #include "Settings/EditorLoadingSavingSettings.h" #include "EditorFramework/AssetImportData.h" #include "Animation/SkeletalMeshActor.h" #include "Components/CapsuleComponent.h" #include "Components/SphereComponent.h" #include "Components/BoxComponent.h" #include "Components/PointLightComponent.h" #include "Engine/StaticMeshActor.h" #include "Components/BrushComponent.h" #include "PhysicsEngine/RadialForceComponent.h" #include "Engine/Polys.h" #include "Engine/Selection.h" #include "Editor.h" #include "LevelEditorViewport.h" #include "EditorModeManager.h" #include "EditorModes.h" #include "EditorDirectories.h" #include "FileHelpers.h" #include "UnrealEdGlobals.h" #include "UObject/UObjectIterator.h" #include "StaticMeshResources.h" #include "EditorSupportDelegates.h" #include "BusyCursor.h" #include "ScopedTransaction.h" #include "LevelUtils.h" #include "ObjectTools.h" #include "PackageTools.h" #include "Interfaces/IMainFrameModule.h" #include "EditorLevelUtils.h" #include "EditorBuildUtils.h" #include "ScriptDisassembler.h" #include "IAssetTools.h" #include "AssetToolsModule.h" #include "IContentBrowserSingleton.h" #include "ContentBrowserModule.h" #include "AssetRegistryModule.h" #include "Matinee/MatineeActor.h" #include "MatineeExporter.h" #include "FbxExporter.h" #include "DesktopPlatformModule.h" #include "SnappingUtils.h" #include "AssetSelection.h" #include "HighResScreenshot.h" #include "ActorEditorUtils.h" #include "Editor/ActorPositioning.h" #include "Matinee/InterpData.h" #include "LandscapeInfo.h" #include "LandscapeInfoMap.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" #include "Logging/LogScopedCategoryAndVerbosityOverride.h" #include "Misc/ConfigCacheIni.h" #include "Misc/FeedbackContext.h" #include "EngineUtils.h" #include "AutoReimport/AssetSourceFilenameCache.h" #if PLATFORM_WINDOWS #include "Windows/WindowsHWrapper.h" #endif #include "ActorGroupingUtils.h" #include "EdMode.h" #include "Subsystems/BrushEditingSubsystem.h" DEFINE_LOG_CATEGORY_STATIC(LogUnrealEdSrv, Log, All); #define LOCTEXT_NAMESPACE "UnrealEdSrv" /** * Dumps a set of selected objects to debugf. */ static void PrivateDumpSelection(USelection* Selection) { for (FSelectionIterator Itor(*Selection); Itor; ++Itor) { UObject* CurObject = *Itor; if (CurObject) { UE_LOG(LogUnrealEdSrv, Log, TEXT(" %s"), *CurObject->GetClass()->GetName()); } else { UE_LOG(LogUnrealEdSrv, Log, TEXT(" NULL object")); } } } class SModalWindowTest: public SCompoundWidget { public: SLATE_BEGIN_ARGS(SModalWindowTest) {} SLATE_END_ARGS() void Construct(const FArguments& InArgs) { this->ChildSlot [SNew(SBorder) .BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder")) [SNew(SVerticalBox) + SVerticalBox::Slot().AutoHeight()[SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().HAlign(HAlign_Center)[SNew(STextBlock).Text(LOCTEXT("ModelTestWindowLabel", "This is a modal window test"))] + SHorizontalBox::Slot().AutoWidth().HAlign(HAlign_Center)[SNew(SButton).Text(LOCTEXT("NewModalTestWindowButtonLabel", "New Modal Window")).OnClicked(this, &SModalWindowTest::OnNewModalWindowClicked)]] + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Right)[SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth()[SNew(SButton).Text(NSLOCTEXT("UnrealEd", "OK", "OK")).OnClicked(this, &SModalWindowTest::OnOKClicked)] + SHorizontalBox::Slot().AutoWidth()[SNew(SButton).Text(NSLOCTEXT("UnrealEd", "Cancel", "Cancel")).OnClicked(this, &SModalWindowTest::OnCancelClicked)]]]]; } SModalWindowTest() : bUserResponse(false) { } void SetWindow(TSharedPtr InWindow) { MyWindow = InWindow; } bool GetResponse() const { return bUserResponse; } private: FReply OnOKClicked() { bUserResponse = true; MyWindow->RequestDestroyWindow(); return FReply::Handled(); } FReply OnCancelClicked() { bUserResponse = false; MyWindow->RequestDestroyWindow(); return FReply::Handled(); } FReply OnNewModalWindowClicked() { TSharedRef ModalWindowContent = SNew(SModalWindowTest); TSharedRef ModalWindow = SNew(SWindow) .Title(LOCTEXT("TestModalWindowTitle", "Modal Window")) .ClientSize(FVector2D(250, 100)) [ModalWindowContent]; ModalWindowContent->SetWindow(ModalWindow); FSlateApplication::Get().AddModalWindow(ModalWindow, AsShared()); UE_LOG(LogUnrealEdSrv, Log, TEXT("Modal Window Returned")); return FReply::Handled(); } FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) { struct Local { static void FillSubMenuEntries(FMenuBuilder& MenuBuilder) { MenuBuilder.AddMenuEntry(LOCTEXT("TestItem2", "Test Item 2"), LOCTEXT("TestToolTip", "TestToolTip"), FSlateIcon(), FUIAction()); MenuBuilder.AddMenuEntry(LOCTEXT("TestItem3", "Test Item 3"), LOCTEXT("TestToolTip", "TestToolTip"), FSlateIcon(), FUIAction()); MenuBuilder.AddSubMenu(LOCTEXT("SubMenu", "Sub Menu"), LOCTEXT("OpensASubmenu", "Opens a submenu"), FNewMenuDelegate::CreateStatic(&Local::FillSubMenuEntries)); MenuBuilder.AddSubMenu(LOCTEXT("SubMenu2", "Sub Menu2"), LOCTEXT("OpensASubmenu", "Opens a submenu"), FNewMenuDelegate::CreateStatic(&Local::FillSubMenuEntries)); } }; FMenuBuilder NewMenu(true, NULL); NewMenu.BeginSection("TestMenuModalWindow", LOCTEXT("MenuInAModalWindow", "Menu in a modal window")); { NewMenu.AddMenuEntry(LOCTEXT("TestItem1", "Test Item 1"), FText::GetEmpty(), FSlateIcon(), FUIAction()); NewMenu.AddSubMenu(LOCTEXT("SubMenu", "Sub Menu"), LOCTEXT("OpenASubmenu", "Opens a sub menu"), FNewMenuDelegate::CreateStatic(&Local::FillSubMenuEntries)); } NewMenu.EndSection(); FWidgetPath WidgetPath = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath(); FSlateApplication::Get().PushMenu(SharedThis(this), WidgetPath, NewMenu.MakeWidget(), MouseEvent.GetScreenSpacePosition(), FPopupTransitionEffect(FPopupTransitionEffect::None)); return FReply::Handled(); } return FReply::Unhandled(); } TSharedPtr MyWindow; bool bUserResponse; }; UPackage* UUnrealEdEngine::GeneratePackageThumbnailsIfRequired(const TCHAR* Str, FOutputDevice& Ar, TArray& GeneratedThumbNamesList) { UPackage* Pkg = NULL; if (FParse::Command(&Str, TEXT("SavePackage"))) { FString TempFname; if (FParse::Value(Str, TEXT("FILE="), TempFname) && ParseObject(Str, TEXT("Package="), Pkg, NULL)) { if (Pkg == nullptr) { return nullptr; } // Update any thumbnails for objects in this package that were modified or generate // new thumbnails for objects that don't have any bool bSilent = false; FParse::Bool(Str, TEXT("SILENT="), bSilent); // Make a list of packages to query (in our case, just the package we're saving) TArray Packages; Packages.Add(Pkg); // Allocate a new thumbnail map if we need one if (!Pkg->ThumbnailMap) { Pkg->ThumbnailMap = MakeUnique(); } // OK, now query all of the browsable objects in the package we're about to save TArray BrowsableObjectsInPackage; // Load the asset tools module to get access to thumbnail tools FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); // NOTE: The package should really be fully loaded before we try to generate thumbnails UPackageTools::GetObjectsInPackages( &Packages, // Packages to search BrowsableObjectsInPackage); // Out: Objects // Check to see if any of the objects need thumbnails generated TSet ObjectsMissingThumbnails; TSet ObjectsWithThumbnails; for (int32 CurObjectIndex = 0; CurObjectIndex < BrowsableObjectsInPackage.Num(); ++CurObjectIndex) { UObject* CurObject = BrowsableObjectsInPackage[CurObjectIndex]; check(CurObject != NULL); bool bUsesGenericThumbnail = AssetToolsModule.Get().AssetUsesGenericThumbnail(FAssetData(CurObject)); // Archetypes always use a shared thumbnail if (CurObject->HasAllFlags(RF_ArchetypeObject)) { bUsesGenericThumbnail = true; } bool bPrintThumbnailDiagnostics = false; GConfig->GetBool(TEXT("Thumbnails"), TEXT("Debug"), bPrintThumbnailDiagnostics, GEditorPerProjectIni); const FObjectThumbnail* ExistingThumbnail = ThumbnailTools::FindCachedThumbnail(CurObject->GetFullName()); if (bPrintThumbnailDiagnostics) { UE_LOG(LogUnrealEdSrv, Log, TEXT("Saving Thumb for %s"), *CurObject->GetFullName()); UE_LOG(LogUnrealEdSrv, Log, TEXT(" Thumb existed = %d"), (ExistingThumbnail != NULL) ? 1 : 0); UE_LOG(LogUnrealEdSrv, Log, TEXT(" Shared Thumb = %d"), (bUsesGenericThumbnail) ? 1 : 0); } // if it's not generatable, let's make sure it doesn't have a custom thumbnail before saving if (!ExistingThumbnail && bUsesGenericThumbnail) { // let it load the custom icons from disk // @todo CB: Batch up requests for multiple thumbnails! TArray ObjectFullNames; FName ObjectFullNameFName(*CurObject->GetFullName()); ObjectFullNames.Add(ObjectFullNameFName); // Load thumbnails FThumbnailMap& LoadedThumbnails = Pkg->AccessThumbnailMap(); if (ThumbnailTools::ConditionallyLoadThumbnailsForObjects(ObjectFullNames, LoadedThumbnails)) { // store off the names of the thumbnails that were loaded as part of a save so we can delete them after the save GeneratedThumbNamesList.Add(ObjectFullNameFName.ToString()); if (bPrintThumbnailDiagnostics) { UE_LOG(LogUnrealEdSrv, Log, TEXT(" Unloaded thumb loaded successfully")); } ExistingThumbnail = LoadedThumbnails.Find(ObjectFullNameFName); if (bPrintThumbnailDiagnostics) { UE_LOG(LogUnrealEdSrv, Log, TEXT(" Newly loaded thumb exists = %d"), (ExistingThumbnail != NULL) ? 1 : 0); if (ExistingThumbnail) { UE_LOG(LogUnrealEdSrv, Log, TEXT(" Thumb created after proper version = %d"), (ExistingThumbnail->IsCreatedAfterCustomThumbsEnabled()) ? 1 : 0); } } if (ExistingThumbnail && !ExistingThumbnail->IsCreatedAfterCustomThumbsEnabled()) { if (bPrintThumbnailDiagnostics) { UE_LOG(LogUnrealEdSrv, Log, TEXT(" WIPING OUT THUMBNAIL!!!!")); } // Casting away const to save memory behind the scenes FObjectThumbnail* ThumbToClear = (FObjectThumbnail*)ExistingThumbnail; ThumbToClear->SetImageSize(0, 0); ThumbToClear->AccessImageData().Empty(); } } else { if (bPrintThumbnailDiagnostics) { UE_LOG(LogUnrealEdSrv, Log, TEXT(" Unloaded thumb does not exist")); } } } if (bUsesGenericThumbnail) { // This is a generic thumbnail object, but it may have a custom thumbnail. if (ExistingThumbnail != NULL && !ExistingThumbnail->IsEmpty()) { ObjectsWithThumbnails.Add(CurObject); } } else { // This is not a generic thumbnail object, so if it is dirty or missing we will render it. if (ExistingThumbnail != NULL && !ExistingThumbnail->IsEmpty() && !ExistingThumbnail->IsDirty()) { ObjectsWithThumbnails.Add(CurObject); } else { ObjectsMissingThumbnails.Add(CurObject); } } } if (BrowsableObjectsInPackage.Num() > 0) { // Missing some thumbnails, so go ahead and try to generate them now // Start a busy cursor const FScopedBusyCursor BusyCursor; if (!bSilent) { const bool bWantProgressMeter = true; GWarn->BeginSlowTask(NSLOCTEXT("UnrealEd", "SavingPackage_GeneratingThumbnails", "Generating thumbnails..."), bWantProgressMeter); } Ar.Logf(TEXT("OBJ SavePackage: Generating thumbnails for [%i] asset(s) in package [%s] ([%i] browsable assets)..."), ObjectsMissingThumbnails.Num(), *Pkg->GetName(), BrowsableObjectsInPackage.Num()); for (int32 CurObjectIndex = 0; CurObjectIndex < BrowsableObjectsInPackage.Num(); ++CurObjectIndex) { UObject* CurObject = BrowsableObjectsInPackage[CurObjectIndex]; check(CurObject != NULL); if (!bSilent) { GWarn->UpdateProgress(CurObjectIndex, BrowsableObjectsInPackage.Num()); } bool bNeedEmptyThumbnail = false; if (ObjectsMissingThumbnails.Contains(CurObject) && !GIsAutomationTesting) { // Generate a thumbnail! FObjectThumbnail* GeneratedThumbnail = ThumbnailTools::GenerateThumbnailForObjectToSaveToDisk(CurObject); if (GeneratedThumbnail != NULL) { Ar.Logf(TEXT("OBJ SavePackage: Rendered thumbnail for [%s]"), *CurObject->GetFullName()); } else { // Couldn't generate a thumb; perhaps this object doesn't support thumbnails? bNeedEmptyThumbnail = true; } } else if (!ObjectsWithThumbnails.Contains(CurObject)) { // Even though this object uses a shared thumbnail, we'll add a "dummy thumbnail" to // the package (zero dimension) for all browsable assets so that the Content Browser // can quickly verify that existence of assets on the fly. bNeedEmptyThumbnail = true; } // Create an empty thumbnail if we need to. All browsable assets need at least a placeholder // thumbnail so the Content Browser can check for non-existent assets in the background if (bNeedEmptyThumbnail) { UPackage* MyOutermostPackage = CurObject->GetOutermost(); ThumbnailTools::CacheEmptyThumbnail(CurObject->GetFullName(), MyOutermostPackage); } } Ar.Logf(TEXT("OBJ SavePackage: Finished generating thumbnails for package [%s]"), *Pkg->GetName()); if (!bSilent) { GWarn->UpdateProgress(1, 1); GWarn->EndSlowTask(); } } } } return Pkg; } bool UUnrealEdEngine::HandleDumpModelGUIDCommand(const TCHAR* Str, FOutputDevice& Ar) { for (TObjectIterator It; It; ++It) { UE_LOG(LogUnrealEdSrv, Log, TEXT("%s Guid = '%s'"), *It->GetFullName(), *It->LightingGuid.ToString()); } return true; } bool UUnrealEdEngine::HandleModalTestCommand(const TCHAR* Str, FOutputDevice& Ar) { TSharedRef MessageBox = SNew(SModalWindowTest); TSharedRef ModalWindow = SNew(SWindow) .Title(LOCTEXT("WindowTitle", "Modal Window")) .ClientSize(FVector2D(250, 100)) [MessageBox]; MessageBox->SetWindow(ModalWindow); GEditor->EditorAddModalWindow(ModalWindow); UE_LOG(LogUnrealEdSrv, Log, TEXT("User response was: %s"), MessageBox->GetResponse() ? TEXT("OK") : TEXT("Cancel")); return true; } bool UUnrealEdEngine::HandleDisallowExportCommand(const TCHAR* Str, FOutputDevice& Ar) { FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked("ContentBrowser"); TArray SelectedAssets; ContentBrowserModule.Get().GetSelectedAssets(SelectedAssets); for (const FAssetData& AssetData: SelectedAssets) { UObject* Object = AssetData.GetAsset(); if (Object) { UPackage* Package = Object->GetOutermost(); Package->SetPackageFlags(EPackageFlags::PKG_DisallowExport); Package->MarkPackageDirty(); UE_LOG(LogUnrealEdSrv, Log, TEXT("Marked '%s' as not exportable"), *Object->GetName()); } } return true; } bool UUnrealEdEngine::HandleDumpBPClassesCommand(const TCHAR* Str, FOutputDevice& Ar) { UE_LOG(LogUnrealEdSrv, Log, TEXT("--- Listing all blueprint generated classes ---")); for (TObjectIterator it; it; ++it) { const UClass* CurrentClass = *it; if (CurrentClass && CurrentClass->ClassGeneratedBy) { UE_LOG(LogUnrealEdSrv, Log, TEXT(" %s (%s)"), *CurrentClass->GetName(), *CurrentClass->GetOutermost()->GetName()); } } return true; } bool UUnrealEdEngine::HandleFindOutdateInstancesCommand(const TCHAR* Str, FOutputDevice& Ar) { UE_LOG(LogUnrealEdSrv, Log, TEXT("--- Finding all actor instances with outdated classes ---")); int32 NumFound = 0; for (TObjectIterator it; it; ++it) { const UObject* CurrentObj = *it; if (CurrentObj->GetClass()->HasAnyClassFlags(CLASS_NewerVersionExists)) { UE_LOG(LogUnrealEdSrv, Log, TEXT(" %s (%s)"), *CurrentObj->GetName(), *CurrentObj->GetClass()->GetName()); NumFound++; } } UE_LOG(LogUnrealEdSrv, Log, TEXT("Found %d instance(s)."), NumFound); return true; } bool UUnrealEdEngine::HandleDumpSelectionCommand(const TCHAR* Str, FOutputDevice& Ar) { UE_LOG(LogUnrealEdSrv, Log, TEXT("Selected Actors:")); PrivateDumpSelection(GetSelectedActors()); UE_LOG(LogUnrealEdSrv, Log, TEXT("Selected Non-Actors:")); PrivateDumpSelection(GetSelectedObjects()); return true; } bool UUnrealEdEngine::HandleBuildLightingCommand(const TCHAR* Str, FOutputDevice& Ar, UWorld* InWorld) { return FEditorBuildUtils::EditorBuild(InWorld, FBuildOptions::BuildLighting); } bool UUnrealEdEngine::HandleBuildPathsCommand(const TCHAR* Str, FOutputDevice& Ar, UWorld* InWorld) { return FEditorBuildUtils::EditorBuild(InWorld, FBuildOptions::BuildAIPaths); } bool UUnrealEdEngine::HandleRecreateLandscapeCollisionCommand(const TCHAR* Str, FOutputDevice& Ar, UWorld* InWorld) { if (!PlayWorld && InWorld && InWorld->GetWorldSettings()) { for (auto It = ULandscapeInfoMap::GetLandscapeInfoMap(InWorld).Map.CreateIterator(); It; ++It) { ULandscapeInfo* Info = It.Value(); Info->RecreateCollisionComponents(); } } return true; } bool UUnrealEdEngine::HandleRemoveLandscapeXYOffsetsCommand(const TCHAR* Str, FOutputDevice& Ar, UWorld* InWorld) { if (!PlayWorld && InWorld && InWorld->GetWorldSettings()) { for (auto It = ULandscapeInfoMap::GetLandscapeInfoMap(InWorld).Map.CreateIterator(); It; ++It) { ULandscapeInfo* Info = It.Value(); Info->RemoveXYOffsets(); } } return true; } bool UUnrealEdEngine::HandleConvertMatineesCommand(const TCHAR* Str, FOutputDevice& Ar, UWorld* InWorld) { FVector StartLocation = FVector::ZeroVector; if (InWorld) { ULevel* Level = InWorld->GetCurrentLevel(); if (!Level) { Level = InWorld->PersistentLevel; } check(Level); for (TObjectIterator It; It; ++It) { UInterpData* InterpData = *It; if (InterpData->IsIn(Level)) { // We dont care about renaming references or adding redirectors. References to this will be old seqact_interps GEditor->RenameObject(InterpData, Level->GetOutermost(), *InterpData->GetName()); AMatineeActor* MatineeActor = Level->OwningWorld->SpawnActor(StartLocation, FRotator::ZeroRotator); StartLocation.Y += 50; MatineeActor->MatineeData = InterpData; FProperty* MatineeDataProp = NULL; for (FProperty* Property = MatineeActor->GetClass()->PropertyLink; Property != NULL; Property = Property->PropertyLinkNext) { if (Property->GetName() == TEXT("MatineeData")) { MatineeDataProp = Property; break; } } FPropertyChangedEvent PropertyChangedEvent(MatineeDataProp); MatineeActor->PostEditChangeProperty(PropertyChangedEvent); } } } return true; } bool UUnrealEdEngine::HandleDisasmScriptCommand(const TCHAR* Str, FOutputDevice& Ar) { FString ClassName; if (FParse::Token(Str, ClassName, false)) { FKismetBytecodeDisassembler::DisassembleAllFunctionsInClasses(Ar, ClassName); } return true; } bool UUnrealEdEngine::Exec(UWorld* InWorld, const TCHAR* Stream, FOutputDevice& Ar) { const TCHAR* Str = Stream; // disallow set commands in the editor as that modifies the default object, affecting object serialization if (FParse::Command(&Str, TEXT("SET")) || FParse::Command(&Str, TEXT("SETNOPEC"))) { Ar.Logf(TEXT("Set commands not allowed in the editor")); return true; } // for thumbnail reclamation post save UPackage* Pkg = NULL; // thumbs that are loaded expressly for the sake of saving. To be deleted again post-save TArray ThumbNamesToUnload; // Peek for the SavePackage command and generate thumbnails for the package if we need to // NOTE: The actual package saving happens in the UEditorEngine::Exec_Obj, but we do the // thumbnail generation here in UnrealEd if (FParse::Command(&Str, TEXT("OBJ")) && !IsRunningCommandlet()) { Pkg = GeneratePackageThumbnailsIfRequired(Str, Ar, ThumbNamesToUnload); } // If we don't have a viewport specified to catch the stat commands, use to the active viewport. If there is a game viewport ignore this as we do not want if (GStatProcessingViewportClient == NULL && (GameViewport == NULL || GameViewport->IsSimulateInEditorViewport())) { GStatProcessingViewportClient = GLastKeyLevelEditingViewportClient ? GLastKeyLevelEditingViewportClient : GCurrentLevelEditingViewportClient; } bool bExecSucceeded = UEditorEngine::Exec(InWorld, Stream, Ar); GStatProcessingViewportClient = NULL; // if we loaded thumbs for saving, purge them back from the package // append loaded thumbs onto the existing thumbs list if (Pkg) { for (int32 ThumbRemoveIndex = 0; ThumbRemoveIndex < ThumbNamesToUnload.Num(); ++ThumbRemoveIndex) { ThumbnailTools::CacheThumbnail(ThumbNamesToUnload[ThumbRemoveIndex], NULL, Pkg); } } if (bExecSucceeded) { return true; } if (FParse::Command(&Str, TEXT("DUMPMODELGUIDS"))) { HandleDumpModelGUIDCommand(Str, Ar); } if (FParse::Command(&Str, TEXT("ModalTest"))) { HandleModalTestCommand(Str, Ar); return true; } if (FParse::Command(&Str, TEXT("DisallowExport"))) { HandleDisallowExportCommand(Str, Ar); return true; } if (FParse::Command(&Str, TEXT("DumpBPClasses"))) { HandleDumpBPClassesCommand(Str, Ar); } if (FParse::Command(&Str, TEXT("FindOutdatedInstances"))) { HandleFindOutdateInstancesCommand(Str, Ar); } if (FParse::Command(&Str, TEXT("DUMPSELECTION"))) { HandleDumpSelectionCommand(Str, Ar); } //---------------------------------------------------------------------------------- // EDIT // if (FParse::Command(&Str, TEXT("EDIT"))) { return Exec_Edit(InWorld, Str, Ar); } //------------------------------------------------------------------------------------ // ACTOR: Actor-related functions // else if (FParse::Command(&Str, TEXT("ACTOR"))) { return Exec_Actor(InWorld, Str, Ar); } //------------------------------------------------------------------------------------ // MODE management (Global EDITOR mode): // else if (FParse::Command(&Str, TEXT("MODE"))) { return Exec_Mode(Str, Ar); } //---------------------------------------------------------------------------------- // PIVOT // else if (FParse::Command(&Str, TEXT("PIVOT"))) { return Exec_Pivot(Str, Ar); } else if (FParse::Command(&Str, TEXT("BUILDLIGHTING"))) { HandleBuildLightingCommand(Str, Ar, InWorld); } // BUILD PATHS else if (FParse::Command(&Str, TEXT("BUILDPATHS"))) { HandleBuildPathsCommand(Str, Ar, InWorld); } #if WITH_EDITOR else if (FParse::Command(&Str, TEXT("RecreateLandscapeCollision"))) { // InWorld above is the PIE world if PIE is active, but this is specifically an editor command UWorld* World = GetEditorWorldContext().World(); return HandleRecreateLandscapeCollisionCommand(Str, Ar, World); } else if (FParse::Command(&Str, TEXT("RemoveLandscapeXYOffsets"))) { // InWorld above is the PIE world if PIE is active, but this is specifically an editor command UWorld* World = GetEditorWorldContext().World(); return HandleRemoveLandscapeXYOffsetsCommand(Str, Ar, World); } #endif // WITH_EDITOR else if (FParse::Command(&Str, TEXT("CONVERTMATINEES"))) { return HandleConvertMatineesCommand(Str, Ar, InWorld); } else if (FParse::Command(&Str, TEXT("DISASMSCRIPT"))) { return HandleDisasmScriptCommand(Str, Ar); } #if WITH_EDITOR else if (FParse::Command(&Str, TEXT("cook"))) { if (CookServer) { return CookServer->Exec(InWorld, Str, Ar); } } #endif else if (FParse::Command(&Str, TEXT("GROUPS"))) { return Exec_Group(Str, Ar); } // #ttp 322815 - GDC, temp exec command for scaling the level else if (FParse::Command(&Str, TEXT("SCALELEVEL"))) { // e.g. ScaleLevel Scale=1,2,3 Snap=4 // Non-uniform scaling // e.g. ScaleLevel Scale=2 Snap=4 // Uniform scaling // We can only scale radii if the level is given uniform scaling bool bScale = false; bool bScaleRadii = false; float Scale = 1.0f; FString ScaleStr; FVector ScaleVec(Scale); if (FParse::Value(Str, TEXT("Scale="), ScaleStr, false) && GetFVECTOR(*ScaleStr, ScaleVec)) { // Update uniform incase the user used uniform scale with a vector parm Scale = ScaleVec.X; bScaleRadii = (Scale == ScaleVec.Y && Scale == ScaleVec.Z ? true : false); bScale = true; } else if (FParse::Value(Str, TEXT("Scale="), Scale)) { // Copy the uniform scale to our vector param ScaleVec = FVector(Scale); bScaleRadii = true; bScale = true; } // Can we scale the level? if (bScale) { // See if a snap value was specified for the grid float NewGridSize; const bool bSnap = FParse::Value(Str, TEXT("Snap="), NewGridSize); const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ScalingLevel", "Scaling Level")); // If it was, force the grid size to be this value temporarily const ULevelEditorViewportSettings* ViewportSettings = GetDefault(); TArray& PosGridSizes = const_cast&>(GetCurrentPositionGridArray()); float& CurGridSize = PosGridSizes[ViewportSettings->CurrentPosGridSize]; const float OldGridSize = CurGridSize; if (bSnap) { CurGridSize = NewGridSize; } // "iterates through each actor in the current level" bool bBuildBSPs = false; for (TActorIterator It(InWorld); It; ++It) { AActor* Actor = *It; // "It should skip all static meshes. The reason for this is that they will scale the static meshes via the static mesh editor with the new BuildScale setting." if (Actor) { /*if (AStaticMeshActor* StaticMesh = Cast< AStaticMeshActor >( Actor )) { // Skip static meshes? } else*/ if (ABrush* Brush = Cast(Actor)) { // "For volumes and brushes scale each vertex by the specified amount." if (!FActorEditorUtils::IsABuilderBrush(Brush) && Brush->Brush) { const FVector OldLocation = Brush->GetActorLocation(); const FVector NewLocation = OldLocation * ScaleVec; Brush->Modify(); Brush->SetActorLocation(NewLocation); Brush->Brush->Modify(); for (int32 poly = 0; poly < Brush->Brush->Polys->Element.Num(); poly++) { FPoly* Poly = &(Brush->Brush->Polys->Element[poly]); Poly->TextureU /= ScaleVec; Poly->TextureV /= ScaleVec; Poly->Base = ((Poly->Base - Brush->GetPivotOffset()) * ScaleVec) + Brush->GetPivotOffset(); for (int32 vtx = 0; vtx < Poly->Vertices.Num(); vtx++) { Poly->Vertices[vtx] = ((Poly->Vertices[vtx] - Brush->GetPivotOffset()) * ScaleVec) + Brush->GetPivotOffset(); // "Then snap the vertices new positions by the specified Snap amount" if (bSnap) { FSnappingUtils::SnapPointToGrid(Poly->Vertices[vtx], FVector(0, 0, 0)); } } Poly->CalcNormal(); } Brush->Brush->BuildBound(); Brush->MarkPackageDirty(); bBuildBSPs = true; } } else { // "Do not scale any child components." if (Actor->GetAttachParentActor() == NULL) { // "Only the root component" if (USceneComponent* RootComponent = Actor->GetRootComponent()) { RootComponent->Modify(); // "scales root component by the specified amount." const FVector OldLocation = RootComponent->GetComponentLocation(); const FVector NewLocation = OldLocation * ScaleVec; RootComponent->SetWorldLocation(NewLocation); // Scale up the triggers if (UBoxComponent* BoxComponent = Cast(RootComponent)) { const FVector OldExtent = BoxComponent->GetUnscaledBoxExtent(); const FVector NewExtent = OldExtent * ScaleVec; BoxComponent->SetBoxExtent(NewExtent); } if (bScaleRadii) { if (USphereComponent* SphereComponent = Cast(RootComponent)) { const float OldRadius = SphereComponent->GetUnscaledSphereRadius(); const float NewRadius = OldRadius * Scale; SphereComponent->SetSphereRadius(NewRadius); } else if (UCapsuleComponent* CapsuleComponent = Cast(RootComponent)) { float OldRadius, OldHalfHeight; CapsuleComponent->GetUnscaledCapsuleSize(OldRadius, OldHalfHeight); const float NewRadius = OldRadius * Scale; const float NewHalfHeight = OldHalfHeight * Scale; CapsuleComponent->SetCapsuleSize(NewRadius, NewHalfHeight); } else if (UPointLightComponent* PointLightComponent = Cast(RootComponent)) { PointLightComponent->AttenuationRadius *= Scale; PointLightComponent->SourceRadius *= Scale; PointLightComponent->SourceLength *= Scale; } else if (URadialForceComponent* RadialForceComponent = Cast(RootComponent)) { RadialForceComponent->Radius *= Scale; } /* Other components that have radii UPathFollowingComponent USmartNavLinkComponent UPawnSensingComponent USphereReflectionCaptureComponent UAIPerceptionComponent */ } } } } } } // Restore snap if (bSnap) { CurGridSize = OldGridSize; } // Kick off a rebuild if any of the bsps have changed if (bBuildBSPs) { GUnrealEd->Exec(InWorld, TEXT("MAP REBUILD ALLVISIBLE")); } } return true; } else if (FParse::Command(&Str, TEXT("ScaleMeshes"))) { bool bScale = false; bool bScaleVec = false; // Was just a scale specified float Scale = 1.0f; FVector BoxVec(Scale); if (FParse::Value(Str, TEXT("Scale="), Scale)) { bScale = true; } else { // or was a bounding box specified instead FString BoxStr; if ((FParse::Value(Str, TEXT("BBOX="), BoxStr, false) || FParse::Value(Str, TEXT("FFD="), BoxStr, false)) && GetFVECTOR(*BoxStr, BoxVec)) { bScaleVec = true; } } if (bScale || bScaleVec) { USelection* SelectedObjects = GetSelectedObjects(); TArray SelectedMeshes; SelectedObjects->GetSelectedObjects(SelectedMeshes); if (SelectedMeshes.Num()) { GWarn->BeginSlowTask(NSLOCTEXT("UnrealEd", "ScalingStaticMeshes", "Scaling Static Meshes"), true, true); for (int32 MeshIndex = 0; MeshIndex < SelectedMeshes.Num(); ++MeshIndex) { UStaticMesh* Mesh = SelectedMeshes[MeshIndex]; if (Mesh && Mesh->GetNumSourceModels() > 0) { Mesh->Modify(); GWarn->StatusUpdate(MeshIndex + 1, SelectedMeshes.Num(), FText::Format(NSLOCTEXT("UnrealEd", "ScalingStaticMeshes_Value", "Static Mesh: {0}"), FText::FromString(Mesh->GetName()))); FStaticMeshSourceModel& Model = Mesh->GetSourceModel(0); FVector ScaleVec(Scale, Scale, Scale); // bScale if (bScaleVec) { FBoxSphereBounds Bounds = Mesh->GetBounds(); ScaleVec = BoxVec / (Bounds.BoxExtent * 2.0f); // x2 as artists wanted length not radius } Model.BuildSettings.BuildScale3D *= ScaleVec; // Scale by the current modification UE_LOG(LogUnrealEdSrv, Log, TEXT("Rescaling mesh '%s' with scale: %s"), *Mesh->GetName(), *Model.BuildSettings.BuildScale3D.ToString()); Mesh->Build(); } } GWarn->EndSlowTask(); } } } else if (FParse::Command(&Str, TEXT("ClearSourceFiles"))) { struct Local { static bool RemoveSourcePath(const FAssetImportInfo& ImportInfo, const FAssetData& AssetData, const TArray* SearchTerms) { FAssetImportInfo AssetImportInfo; bool bModified = false; for (const auto& File: ImportInfo.SourceFiles) { const bool bRemoveFile = File.RelativeFilename.IsEmpty() || !SearchTerms || SearchTerms->ContainsByPredicate([&](const FString& SearchTerm) { return File.RelativeFilename.Contains(SearchTerm); }); if (bRemoveFile) { UE_LOG(LogUnrealEdSrv, Log, TEXT("Removing Path: %s"), *File.RelativeFilename); bModified = true; } else { AssetImportInfo.Insert(File); } } if (bModified) { if (UObject* Asset = AssetData.GetAsset()) { UAssetImportData* ImportData = nullptr; // Root out the asset import data property for (FObjectProperty* Property: TFieldRange(Asset->GetClass())) { ImportData = Cast(Property->GetObjectPropertyValue(Property->ContainerPtrToValuePtr(Asset))); if (ImportData) { Asset->Modify(); ImportData->SourceData = AssetImportInfo; return true; } } } } return false; } static void RemoveSourcePaths(const TArray& AllAssets, const TArray* SearchTerms) { FScopedSlowTask SlowTask(AllAssets.Num(), NSLOCTEXT("UnrealEd", "ClearingSourceFiles", "Clearing Source Files")); SlowTask.MakeDialog(true); for (const FAssetData& Asset: AllAssets) { SlowTask.EnterProgressFrame(); // Optimization - check the asset has import information before loading it TOptional ImportInfo = FAssetSourceFilenameCache::ExtractAssetImportInfo(Asset); if (ImportInfo.IsSet() && ImportInfo->SourceFiles.Num()) { RemoveSourcePath(ImportInfo.GetValue(), Asset, SearchTerms); } } } }; FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); FString Path; FParse::Value(Str, TEXT("Path="), Path, false); TArray AllAssets; if (!Path.IsEmpty()) { AssetRegistryModule.Get().GetAssetsByPath(*Path, AllAssets, true); } else { AssetRegistryModule.Get().GetAllAssets(AllAssets); } FString SearchTermStr; if (FParse::Value(Str, TEXT("Find="), SearchTermStr, false)) { // Searching for particular paths to remove TArray SearchTerms; SearchTermStr.ParseIntoArray(SearchTerms, TEXT(","), true); TArray ModifiedObjects; if (SearchTerms.Num()) { Local::RemoveSourcePaths(AllAssets, &SearchTerms); } } else { // Remove every source path on any asset Local::RemoveSourcePaths(AllAssets, nullptr); } } else if (FParse::Command(&Str, TEXT("RenameAssets"))) { FString SearchTermStr; if (FParse::Value(Str, TEXT("Find="), SearchTermStr)) { FString ReplaceStr; FParse::Value(Str, TEXT("Replace="), ReplaceStr); FString AutoCheckOutStr; FParse::Value(Str, TEXT("AutoCheckOut="), AutoCheckOutStr); AutoCheckOutStr = AutoCheckOutStr.ToLower(); bool bAutoCheckOut = (AutoCheckOutStr == "yes" || AutoCheckOutStr == "true" || AutoCheckOutStr == "1"); GWarn->BeginSlowTask(NSLOCTEXT("UnrealEd", "RenamingAssets", "Renaming Assets"), true, true); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); TArray AllAssets; AssetRegistryModule.Get().GetAllAssets(AllAssets); TArray AssetsToRename; for (const FAssetData& Asset: AllAssets) { bool bRenamedPath = false; bool bRenamedAsset = false; FString NewAssetName = Asset.AssetName.ToString(); FString NewPathName = Asset.PackagePath.ToString(); if (NewAssetName.Contains(SearchTermStr)) { FString TempPathName = NewAssetName.Replace(*SearchTermStr, *ReplaceStr); if (!TempPathName.IsEmpty()) { NewAssetName = TempPathName; bRenamedAsset = true; } } if (NewPathName.Contains(SearchTermStr)) { FString TempPathName = NewPathName.Replace(*SearchTermStr, *ReplaceStr); FPaths::RemoveDuplicateSlashes(TempPathName); if (!TempPathName.IsEmpty()) { NewPathName = TempPathName; bRenamedPath = true; } } if (bRenamedAsset || bRenamedPath) { FAssetRenameData RenameData(Asset.GetAsset(), NewPathName, NewAssetName); AssetsToRename.Add(RenameData); } } if (AssetsToRename.Num() > 0) { AssetTools.RenameAssetsWithDialog(AssetsToRename, bAutoCheckOut); } GWarn->EndSlowTask(); } } else if (FParse::Command(&Str, TEXT("HighResShot"))) { if (GetHighResScreenshotConfig().ParseConsoleCommand(Str, Ar)) { TakeHighResScreenShots(); } return true; } else if (FParse::Command(&Str, TEXT("EditorShot")) || FParse::Command(&Str, TEXT("EditorScreenShot"))) { struct Local { static void TakeScreenShotOfWidget(TSharedRef InWidget) { TArray OutImageData; FIntVector OutImageSize; if (FSlateApplication::Get().TakeScreenshot(InWidget, OutImageData, OutImageSize)) { FString FileName; const FString BaseFileName = GetDefault()->EditorScreenshotSaveDirectory.Path / TEXT("EditorScreenshot"); FFileHelper::GenerateNextBitmapFilename(BaseFileName, TEXT("bmp"), FileName); FFileHelper::CreateBitmap(*FileName, OutImageSize.X, OutImageSize.Y, OutImageData.GetData()); } } }; if (FSlateApplication::IsInitialized()) { if (FParse::Command(&Str, TEXT("All"))) { TArray> OpenWindows; FSlateApplication::Get().GetAllVisibleWindowsOrdered(OpenWindows); for (int32 WindowId = 0; WindowId < OpenWindows.Num(); ++WindowId) { Local::TakeScreenShotOfWidget(OpenWindows[WindowId]); } } else { FString WindowNameStr; if (FParse::Value(Str, TEXT("Name="), WindowNameStr)) { TArray> OpenWindows; FSlateApplication::Get().GetAllVisibleWindowsOrdered(OpenWindows); for (int32 WindowId = 0; WindowId < OpenWindows.Num(); ++WindowId) { FString CurrentWindowName = OpenWindows[WindowId]->GetTitle().ToString(); // Strip off the * from the end if it exists if (CurrentWindowName.EndsWith(TEXT("*"), ESearchCase::CaseSensitive)) { CurrentWindowName.LeftChopInline(1, false); } if (CurrentWindowName == WindowNameStr) { Local::TakeScreenShotOfWidget(OpenWindows[WindowId]); } } } else { TSharedPtr ActiveWindow = FSlateApplication::Get().GetActiveTopLevelWindow(); if (ActiveWindow.IsValid()) { Local::TakeScreenShotOfWidget(ActiveWindow.ToSharedRef()); } } } } return true; } return false; } bool UUnrealEdEngine::AnyWorldsAreDirty(UWorld* InWorld) const { // Get the set of all reference worlds. TArray WorldsArray; EditorLevelUtils::GetWorlds(InWorld, WorldsArray, true); if (WorldsArray.Num() > 0) { FString FinalFilename; for (int32 WorldIndex = 0; WorldIndex < WorldsArray.Num(); ++WorldIndex) { UWorld* World = WorldsArray[WorldIndex]; UPackage* Package = Cast(World->GetOuter()); check(Package); // The world needs saving if... if (Package->IsDirty()) { return true; } } } return false; } bool UUnrealEdEngine::AnyContentPackagesAreDirty() const { const UPackage* TransientPackage = GetTransientPackage(); // Check all packages for dirty, non-map, non-transient packages for (TObjectIterator PackageIter; PackageIter; ++PackageIter) { UPackage* CurPackage = *PackageIter; // The package needs saving if it's not the transient package if (CurPackage && (CurPackage != TransientPackage) && CurPackage->IsDirty()) { return true; } } return false; } bool UUnrealEdEngine::IsTemplateMap(const FString& MapName) const { for (TArray::TConstIterator It(TemplateMapInfos); It; ++It) { if (It->Map == MapName) { return true; } } return false; } bool UUnrealEdEngine::IsUserInteracting() { // Check to see if the user is in the middle of a drag operation. bool bUserIsInteracting = false; for (const FEditorViewportClient* VC: GetAllViewportClients()) { // Check for tracking and capture. If a viewport has mouse capture, it could be locking the mouse to the viewport, which means if we prompt with a dialog // while the mouse is locked to a viewport, we wont be able to interact with the dialog. if (VC->IsTracking() || (VC->Viewport && VC->Viewport->HasMouseCapture())) { bUserIsInteracting = true; break; } } return bUserIsInteracting; } void UUnrealEdEngine::ShowPackageNotification() { if (!FApp::IsUnattended()) { // Defer prompting for checkout if we cant prompt because of the following: // The user is interacting with something, // We are performing a slow task // We have a play world // The user disabled prompting on package modification // A window has capture on the mouse bool bCanPrompt = !IsUserInteracting() && !GIsSlowTask && !PlayWorld && GetDefault()->bPromptForCheckoutOnAssetModification && (FSlateApplication::Get().GetMouseCaptureWindow() == NULL); if (bCanPrompt) { bShowPackageNotification = false; bool bNeedWarningDialog = false; for (const auto& Entry: PackageToNotifyState) { if (Entry.Value == NS_PendingWarning) { bNeedWarningDialog = true; break; } } // The user is not interacting with anything, prompt to checkout packages that have been modified struct Local { static void OpenCheckOutDialog() { GUnrealEd->PromptToCheckoutModifiedPackages(true); } }; if (bNeedWarningDialog) { Local::OpenCheckOutDialog(); } else { int32 NumPackagesToCheckOut = GetNumDirtyPackagesThatNeedCheckout(); FFormatNamedArguments Args; Args.Add(TEXT("NumFiles"), NumPackagesToCheckOut); FText ErrorText = FText::Format(NSLOCTEXT("SourceControl", "CheckOutNotification", "{NumFiles} files need check-out!"), Args); if (!CheckOutNotificationWeakPtr.IsValid()) { FNotificationInfo ErrorNotification(ErrorText); ErrorNotification.bFireAndForget = true; ; ErrorNotification.Hyperlink = FSimpleDelegate::CreateStatic(&Local::OpenCheckOutDialog); ErrorNotification.HyperlinkText = NSLOCTEXT("SourceControl", "CheckOutHyperlinkText", "Check-Out"); ErrorNotification.ExpireDuration = 10.0f; // Need this message to last a little longer than normal since the user will probably want to click the hyperlink to check out files ErrorNotification.bUseThrobber = true; // For adding notifications. CheckOutNotificationWeakPtr = FSlateNotificationManager::Get().AddNotification(ErrorNotification); } else { CheckOutNotificationWeakPtr.Pin()->SetText(ErrorText); CheckOutNotificationWeakPtr.Pin()->ExpireAndFadeout(); } } } } } void UUnrealEdEngine::PromptToCheckoutModifiedPackages(bool bPromptAll) { TArray PackagesToCheckout; if (bPromptAll) { for (TMap, uint8>::TIterator It(PackageToNotifyState); It; ++It) { if (It.Key().IsValid()) { PackagesToCheckout.Add(It.Key().Get()); } } } else { for (TMap, uint8>::TIterator It(PackageToNotifyState); It; ++It) { if (It.Key().IsValid() && (It.Value() == NS_PendingWarning || It.Value() == NS_PendingPrompt)) { PackagesToCheckout.Add(It.Key().Get()); It.Value() = NS_DialogPrompted; } } } const bool bCheckDirty = true; const bool bPromptingAfterModify = true; FEditorFileUtils::PromptToCheckoutPackages(bCheckDirty, PackagesToCheckout, NULL, NULL, bPromptingAfterModify); } int32 UUnrealEdEngine::InternalGetNumDirtyPackagesThatNeedCheckout(bool bCheckIfAny) const { int32 PackageCount = 0; if (ISourceControlModule::Get().IsEnabled()) { ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); for (TMap, uint8>::TConstIterator It(PackageToNotifyState); It; ++It) { const UPackage* Package = It.Key().Get(); if (Package != NULL) { FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Package, EStateCacheUsage::Use); if (SourceControlState.IsValid() && (SourceControlState->CanCheckout() || !SourceControlState->IsCurrent() || SourceControlState->IsCheckedOutOther())) { ++PackageCount; if (bCheckIfAny) { break; } } } } } return PackageCount; } int32 UUnrealEdEngine::GetNumDirtyPackagesThatNeedCheckout() const { return InternalGetNumDirtyPackagesThatNeedCheckout(false); } bool UUnrealEdEngine::DoDirtyPackagesNeedCheckout() const { return InternalGetNumDirtyPackagesThatNeedCheckout(true) > 0; } bool UUnrealEdEngine::Exec_Edit(UWorld* InWorld, const TCHAR* Str, FOutputDevice& Ar) { const bool bComponentsSelected = GetSelectedComponentCount() > 0; if (FParse::Command(&Str, TEXT("CUT"))) { if (GLevelEditorModeTools().ProcessEditCut()) { return true; } if (bComponentsSelected) { // Same transaction language used in CopySelectedActorsToClipboard below const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "Cut", "Cut")); edactCopySelected(InWorld); edactDeleteSelected(InWorld); } else { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "Cut", "Cut")); FEditorDelegates::OnEditCutActorsBegin.Broadcast(); CopySelectedActorsToClipboard(InWorld, true); FEditorDelegates::OnEditCutActorsEnd.Broadcast(); } } else if (FParse::Command(&Str, TEXT("COPY"))) { if (GLevelEditorModeTools().ProcessEditCopy()) { return true; } if (bComponentsSelected) { edactCopySelected(InWorld); } else { FEditorDelegates::OnEditCopyActorsBegin.Broadcast(); CopySelectedActorsToClipboard(InWorld, false); FEditorDelegates::OnEditCopyActorsEnd.Broadcast(); } } else if (FParse::Command(&Str, TEXT("PASTE"))) { if (GLevelEditorModeTools().ProcessEditPaste()) { return true; } if (bComponentsSelected) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "PasteComponents", "Paste Components")); edactPasteSelected(InWorld, false, false, true); } else { // How should this paste be handled EPasteTo PasteTo = PT_OriginalLocation; FText TransDescription = NSLOCTEXT("UnrealEd", "Paste", "Paste"); FString TempStr; if (FParse::Value(Str, TEXT("TO="), TempStr)) { if (!FCString::Strcmp(*TempStr, TEXT("HERE"))) { PasteTo = PT_Here; TransDescription = NSLOCTEXT("UnrealEd", "PasteHere", "Paste Here"); } else { if (!FCString::Strcmp(*TempStr, TEXT("ORIGIN"))) { PasteTo = PT_WorldOrigin; TransDescription = NSLOCTEXT("UnrealEd", "PasteToWorldOrigin", "Paste To World Origin"); } } } const FScopedTransaction Transaction(TransDescription); FEditorDelegates::OnEditPasteActorsBegin.Broadcast(); PasteSelectedActorsFromClipboard(InWorld, TransDescription, PasteTo); FEditorDelegates::OnEditPasteActorsEnd.Broadcast(); } } return false; } bool UUnrealEdEngine::Exec_Pivot(const TCHAR* Str, FOutputDevice& Ar) { if (FParse::Command(&Str, TEXT("HERE"))) { NoteActorMovement(); SetPivot(ClickLocation, false, false); FinishAllSnaps(); SetPivotMovedIndependently(true); RedrawLevelEditingViewports(); } else if (FParse::Command(&Str, TEXT("SNAPPED"))) { NoteActorMovement(); SetPivot(ClickLocation, true, false); FinishAllSnaps(); SetPivotMovedIndependently(true); RedrawLevelEditingViewports(); } else if (FParse::Command(&Str, TEXT("CENTERSELECTION"))) { NoteActorMovement(); // Figure out the center location of all selections int32 Count = 0; FVector Center(0, 0, 0); for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It) { AActor* Actor = CastChecked(*It); if (ABrush* Brush = Cast(Actor)) { // Treat brushes as a special case; calculate an effective position from the center point of the vertices. // This way, "Center on Selection" has a special meaning for brushes. TSet UniqueVertices; FVector VertexCenter = FVector::ZeroVector; if (Brush->Brush && Brush->Brush->Polys) { for (const auto& Element: Brush->Brush->Polys->Element) { for (const auto& Vertex: Element.Vertices) { UniqueVertices.Add(Vertex); } } for (const auto& Vertex: UniqueVertices) { VertexCenter += Vertex; } if (UniqueVertices.Num() > 0) { VertexCenter /= UniqueVertices.Num(); } } Center += Brush->GetTransform().TransformPosition(VertexCenter); } else { Center += Actor->GetActorLocation(); } Count++; } if (Count > 0) { FVector CenterLocation = Center / Count; UnsnappedClickLocation = CenterLocation; ClickLocation = CenterLocation; ClickPlane = FPlane(0.f, 0.f, 0.f, 0.f); SetPivot(ClickLocation, false, false); FinishAllSnaps(); SetPivotMovedIndependently(true); } RedrawLevelEditingViewports(); } return false; } static void MirrorActors(const FVector& MirrorScale) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "MirroringActors", "Mirroring Actors")); // Fires ULevel::LevelDirtiedEvent when falling out of scope. FScopedLevelDirtied LevelDirtyCallback; for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It) { AActor* Actor = static_cast(*It); checkSlow(Actor->IsA(AActor::StaticClass())); const FVector PivotLocation = GLevelEditorModeTools().PivotLocation; Actor->Modify(); Actor->EditorApplyMirror(MirrorScale, PivotLocation); ABrush* Brush = Cast(Actor); if (Brush && Brush->GetBrushComponent()) { Brush->GetBrushComponent()->RequestUpdateBrushCollision(); } Actor->InvalidateLightingCache(); Actor->PostEditMove(true); Actor->MarkPackageDirty(); LevelDirtyCallback.Request(); } if (UBrushEditingSubsystem* BrushSubsystem = GEditor->GetEditorSubsystem()) { BrushSubsystem->UpdateGeometryFromSelectedBrushes(); } GEditor->RedrawLevelEditingViewports(); } /** * Gathers up a list of selection FPolys from selected static meshes. * * @return A TArray containing FPolys representing the triangles in the selected static meshes (note that these * triangles are transformed into world space before being added to the array. */ TArray GetSelectedPolygons() { // Build a list of polygons from all selected static meshes TArray SelectedPolys; for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It) { AActor* Actor = static_cast(*It); checkSlow(Actor->IsA(AActor::StaticClass())); FTransform ActorToWorld = Actor->ActorToWorld(); for (UActorComponent* Component: Actor->GetComponents()) { // If its a static mesh component, with a static mesh UStaticMeshComponent* SMComp = Cast(Component); if (SMComp && SMComp->IsRegistered() && SMComp->GetStaticMesh()) { UStaticMesh* StaticMesh = SMComp->GetStaticMesh(); if (StaticMesh) { int32 NumLods = StaticMesh->GetNumLODs(); if (NumLods) { const FStaticMeshLODResources& MeshLodZero = StaticMesh->GetLODForExport(0); int32 NumTriangles = MeshLodZero.GetNumTriangles(); int32 NumVertices = MeshLodZero.GetNumVertices(); const FPositionVertexBuffer& PositionVertexBuffer = MeshLodZero.VertexBuffers.PositionVertexBuffer; FIndexArrayView Indices = MeshLodZero.DepthOnlyIndexBuffer.GetArrayView(); for (int32 TriangleIndex = 0; TriangleIndex < NumTriangles; TriangleIndex++) { const uint32 Idx0 = Indices[(TriangleIndex * 3) + 0]; const uint32 Idx1 = Indices[(TriangleIndex * 3) + 1]; const uint32 Idx2 = Indices[(TriangleIndex * 3) + 2]; FPoly* Polygon = new FPoly; // Add the poly Polygon->Init(); Polygon->PolyFlags = PF_DefaultFlags; new (Polygon->Vertices) FVector(ActorToWorld.TransformPosition(PositionVertexBuffer.VertexPosition(Idx2))); new (Polygon->Vertices) FVector(ActorToWorld.TransformPosition(PositionVertexBuffer.VertexPosition(Idx1))); new (Polygon->Vertices) FVector(ActorToWorld.TransformPosition(PositionVertexBuffer.VertexPosition(Idx0))); Polygon->CalcNormal(1); Polygon->Fix(); if (Polygon->Vertices.Num() > 2) { if (!Polygon->Finalize(NULL, 1)) { SelectedPolys.Add(Polygon); } } // And add a flipped version of it to account for negative scaling Polygon = new FPoly; Polygon->Init(); Polygon->PolyFlags = PF_DefaultFlags; new (Polygon->Vertices) FVector(ActorToWorld.TransformPosition(PositionVertexBuffer.VertexPosition(Idx2))); new (Polygon->Vertices) FVector(ActorToWorld.TransformPosition(PositionVertexBuffer.VertexPosition(Idx0))); new (Polygon->Vertices) FVector(ActorToWorld.TransformPosition(PositionVertexBuffer.VertexPosition(Idx1))); Polygon->CalcNormal(1); Polygon->Fix(); if (Polygon->Vertices.Num() > 2) { if (!Polygon->Finalize(NULL, 1)) { SelectedPolys.Add(Polygon); } } } } } } } } return SelectedPolys; } /** * Creates an axis aligned bounding box based on the bounds of SelectedPolys. This bounding box * is then copied into the builder brush. This function is a set up function that the blocking volume * creation execs will call before doing anything fancy. * * @param InWorld The world in which the builder brush needs to be created * @param SelectedPolys The list of selected FPolys to create the bounding box from. * @param bSnapVertsToGrid Should the brush verts snap to grid */ void CreateBoundingBoxBuilderBrush(UWorld* InWorld, const TArray SelectedPolys, bool bSnapVertsToGrid) { int x; FPoly* Poly; FBox BBox(ForceInit); FVector Vertex; for (x = 0; x < SelectedPolys.Num(); ++x) { Poly = SelectedPolys[x]; for (int v = 0; v < Poly->Vertices.Num(); ++v) { if (bSnapVertsToGrid) { Vertex = Poly->Vertices[v].GridSnap(GEditor->GetGridSize()); } else { Vertex = Poly->Vertices[v]; } BBox += Vertex; } } // Change the builder brush to match the bounding box so that it exactly envelops the selected meshes { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "BrushSet", "Brush Set")); UCubeBuilder* CubeBuilder = NewObject(GetTransientPackage(), NAME_None, RF_Transactional); FVector Extent = BBox.GetExtent(); CubeBuilder->X = Extent.X * 2; CubeBuilder->Y = Extent.Y * 2; CubeBuilder->Z = Extent.Z * 2; CubeBuilder->Build(InWorld); ABrush* DefaultBrush = InWorld->GetDefaultBrush(); check(DefaultBrush != nullptr); DefaultBrush->SetActorLocation(BBox.GetCenter(), false); DefaultBrush->ReregisterAllComponents(); } } /** * Take a plane and creates a gigantic triangle polygon that lies along it. The blocking * volume creation routines call this when they are cutting geometry and need to create * capping polygons. * * This polygon is so huge that it doesn't matter where the vertices actually land. * * @param InPlane The plane to lay the polygon on * @return An FPoly representing the giant triangle we created (NULL if there was a problem) */ FPoly* CreateHugeTrianglePolygonOnPlane(const FPlane* InPlane) { // Using the plane normal, get 2 good axis vectors FVector A, B; InPlane->GetSafeNormal().FindBestAxisVectors(A, B); // Create 4 vertices from the plane origin and the 2 axis generated above FPoly* Triangle = new FPoly(); FVector Center = FVector(InPlane->X, InPlane->Y, InPlane->Z) * InPlane->W; FVector V0 = Center + (A * WORLD_MAX); FVector V1 = Center + (B * WORLD_MAX); FVector V2 = Center - (((A + B) / 2.0f) * WORLD_MAX); // Create a triangle that lays on InPlane Triangle->Init(); Triangle->PolyFlags = PF_DefaultFlags; new (Triangle->Vertices) FVector(V0); new (Triangle->Vertices) FVector(V2); new (Triangle->Vertices) FVector(V1); Triangle->CalcNormal(1); Triangle->Fix(); if (Triangle->Finalize(NULL, 1)) { delete Triangle; Triangle = NULL; } return Triangle; } bool UUnrealEdEngine::Exec_Actor(UWorld* InWorld, const TCHAR* Str, FOutputDevice& Ar) { // Keep a pointer to the beginning of the string to use for message displaying purposes const TCHAR* const FullStr = Str; // Determine whether or not components are selected (used to properly label transaction names) const bool bComponentsSelected = GetSelectedComponentCount() > 0; if (FParse::Command(&Str, TEXT("ADD"))) { UClass* Class; if (ParseObject(Str, TEXT("CLASS="), Class, ANY_PACKAGE)) { int32 bSnap = 1; FParse::Value(Str, TEXT("SNAP="), bSnap); AActor* Default = Class->GetDefaultObject(); const FTransform ActorTransform = FActorPositioning::GetCurrentViewportPlacementTransform(*Default, !!bSnap); AddActor(InWorld->GetCurrentLevel(), Class, ActorTransform); RedrawLevelEditingViewports(); return true; } } else if (FParse::Command(&Str, TEXT("CREATE_BV_BOUNDINGBOX"))) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "CreateBoundingBoxBlockingVolume", "Create Bounding Box Blocking Volume")); InWorld->GetDefaultBrush()->Modify(); bool bSnapToGrid = 0; FParse::Bool(Str, TEXT("SNAPTOGRID="), bSnapToGrid); // Create a bounding box for the selected static mesh triangles and set the builder brush to match it TArray SelectedPolys = GetSelectedPolygons(); CreateBoundingBoxBuilderBrush(InWorld, SelectedPolys, bSnapToGrid); // Create the blocking volume GUnrealEd->Exec(InWorld, TEXT("BRUSH ADDVOLUME CLASS=BlockingVolume")); // Clean up memory for (int x = 0; x < SelectedPolys.Num(); ++x) { delete SelectedPolys[x]; } SelectedPolys.Empty(); // Finish up RedrawLevelEditingViewports(); return true; } else if (FParse::Command(&Str, TEXT("CREATE_BV_CONVEXVOLUME"))) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "CreateConvexBlockingVolume", "Create Convex Blocking Volume")); InWorld->GetDefaultBrush()->Modify(); bool bSnapToGrid = 0; FParse::Bool(Str, TEXT("SNAPTOGRID="), bSnapToGrid); // The rejection tolerance. When figuring out which planes to cut the blocking volume cube with // the code will reject any planes that are less than "NormalTolerance" different in their normals. // // This cuts down on the number of planes that will be used for generating the cutting planes and, // as a side effect, eliminates duplicates. float NormalTolerance = 0.25f; FParse::Value(Str, TEXT("NORMALTOLERANCE="), NormalTolerance); FVector NormalLimits(1.0f, 1.0f, 1.0f); FParse::Value(Str, TEXT("NLIMITX="), NormalLimits.X); FParse::Value(Str, TEXT("NLIMITY="), NormalLimits.Y); FParse::Value(Str, TEXT("NLIMITZ="), NormalLimits.Z); // Create a bounding box for the selected static mesh triangles and set the builder brush to match it TArray SelectedPolys = GetSelectedPolygons(); CreateBoundingBoxBuilderBrush(InWorld, SelectedPolys, bSnapToGrid); // Get a list of the polygons that make up the builder brush FPoly* poly; TArray* BuilderBrushPolys = new TArray(InWorld->GetDefaultBrush()->Brush->Polys->Element); // Create a list of valid splitting planes TArray SplitterPlanes; for (int p = 0; p < SelectedPolys.Num(); ++p) { // Get a splitting plane from the first poly in our selection poly = SelectedPolys[p]; FPlane* SplittingPlane = new FPlane(poly->Vertices[0], poly->Normal); // Make sure this poly doesn't clip any other polys in the selection. If it does, we can't use it for generating the convex volume. bool bUseThisSplitter = true; for (int pp = 0; pp < SelectedPolys.Num() && bUseThisSplitter; ++pp) { FPoly* ppoly = SelectedPolys[pp]; if (p != pp && !(poly->Normal - ppoly->Normal).IsNearlyZero()) { int res = ppoly->SplitWithPlaneFast(*SplittingPlane, NULL, NULL); if (res == SP_Split || res == SP_Front) { // Whoops, this plane clips polygons (and/or sits between static meshes) in the selection so it can't be used bUseThisSplitter = false; } } } // If this polygons plane doesn't clip the selection in any way, we can carve the builder brush with it. Save it. if (bUseThisSplitter) { // Move the plane into the same coordinate space as the builder brush *SplittingPlane = SplittingPlane->TransformBy(InWorld->GetDefaultBrush()->ActorToWorld().ToMatrixWithScale().InverseFast()); // Before keeping this plane, make sure there aren't any existing planes that have a normal within the rejection tolerance. bool bAddPlaneToList = true; for (int x = 0; x < SplitterPlanes.Num(); ++x) { FPlane* plane = SplitterPlanes[x]; if (plane->GetSafeNormal().Equals(SplittingPlane->GetSafeNormal(), NormalTolerance)) { bAddPlaneToList = false; break; } } // As a final test, make sure that this planes normal falls within the normal limits that were defined if (FMath::Abs(SplittingPlane->GetSafeNormal().X) > NormalLimits.X) { bAddPlaneToList = false; } if (FMath::Abs(SplittingPlane->GetSafeNormal().Y) > NormalLimits.Y) { bAddPlaneToList = false; } if (FMath::Abs(SplittingPlane->GetSafeNormal().Z) > NormalLimits.Z) { bAddPlaneToList = false; } // If this plane passed every test - it's a keeper! if (bAddPlaneToList) { SplitterPlanes.Add(SplittingPlane); } else { delete SplittingPlane; } } } // The builder brush is a bounding box at this point that fully surrounds the selected static meshes. // Now we will carve away at it using the splitting planes we collected earlier. When this process // is complete, we will have a convex volume inside of the builder brush that can then be used to add // a blocking volume. TArray NewBuilderBrushPolys; for (int sp = 0; sp < SplitterPlanes.Num(); ++sp) { FPlane* plane = SplitterPlanes[sp]; // Carve the builder brush with each splitting plane we collected. We place the results into // NewBuilderBrushPolys since we don't want to overwrite the original array just yet. bool bNeedCapPoly = false; for (int bp = 0; bp < BuilderBrushPolys->Num(); ++bp) { poly = &(*BuilderBrushPolys)[bp]; FPoly Front, Back; int res = poly->SplitWithPlane(FVector(plane->X, plane->Y, plane->Z) * plane->W, plane->GetSafeNormal(), &Front, &Back, true); switch (res) { // Ignore these results. We don't want them. case SP_Coplanar: case SP_Front: break; // In the case of a split, keep the polygon on the back side of the plane. case SP_Split: { NewBuilderBrushPolys.Add(Back); bNeedCapPoly = true; } break; // By default, just keep the polygon that we had. default: { NewBuilderBrushPolys.Add((*BuilderBrushPolys)[bp]); } break; } } // NewBuilderBrushPolys contains the newly clipped polygons so copy those into // the real array of polygons. BuilderBrushPolys = new TArray(NewBuilderBrushPolys); NewBuilderBrushPolys.Empty(); // If any splitting occured, we need to generate a cap polygon to cover the hole. if (bNeedCapPoly) { // Create a large triangle polygon that covers the newly formed hole in the builder brush. FPoly* CappingPoly = CreateHugeTrianglePolygonOnPlane(plane); if (CappingPoly) { // Now we do the clipping the other way around. We are going to use the polygons in the builder brush to // create planes which will clip the huge triangle polygon we just created. When this process is over, // we will be left with a new polygon that covers the newly formed hole in the builder brush. for (int bp = 0; bp < BuilderBrushPolys->Num(); ++bp) { poly = &((*BuilderBrushPolys)[bp]); plane = new FPlane(poly->Vertices[0], poly->Vertices[1], poly->Vertices[2]); FPoly Front, Back; int res = CappingPoly->SplitWithPlane(FVector(plane->X, plane->Y, plane->Z) * plane->W, plane->GetSafeNormal(), &Front, &Back, true); switch (res) { case SP_Split: { *CappingPoly = Back; } break; } } // Add that new polygon into the builder brush polys as a capping polygon. BuilderBrushPolys->Add(*CappingPoly); } } } // Create a new builder brush from the freshly clipped polygons. InWorld->GetDefaultBrush()->Brush->Polys->Element.Empty(); for (int x = 0; x < BuilderBrushPolys->Num(); ++x) { InWorld->GetDefaultBrush()->Brush->Polys->Element.Add((*BuilderBrushPolys)[x]); } InWorld->GetDefaultBrush()->ReregisterAllComponents(); // Create the blocking volume GUnrealEd->Exec(InWorld, TEXT("BRUSH ADDVOLUME CLASS=BlockingVolume")); // Clean up memory for (int x = 0; x < SelectedPolys.Num(); ++x) { delete SelectedPolys[x]; } SelectedPolys.Empty(); for (int x = 0; x < SplitterPlanes.Num(); ++x) { delete SplitterPlanes[x]; } SplitterPlanes.Empty(); delete BuilderBrushPolys; // Finish up RedrawLevelEditingViewports(); return true; } else if (FParse::Command(&Str, TEXT("MIRROR"))) { FVector MirrorScale(1, 1, 1); GetFVECTOR(Str, MirrorScale); // We can't have zeroes in the vector if (!MirrorScale.X) MirrorScale.X = 1; if (!MirrorScale.Y) MirrorScale.Y = 1; if (!MirrorScale.Z) MirrorScale.Z = 1; const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "MirroringActors", "Mirroring Actors")); MirrorActors(MirrorScale); RebuildAlteredBSP(); // Update the Bsp of any levels containing a modified brush return true; } else if (FParse::Command(&Str, TEXT("DELTAMOVE"))) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "DeltaMoveActors", "Move Actors by Delta")); FVector DeltaMove = FVector::ZeroVector; GetFVECTOR(Str, DeltaMove); FEditorModeTools& Tools = GLevelEditorModeTools(); Tools.SetPivotLocation(Tools.PivotLocation + DeltaMove, false); if (GCurrentLevelEditingViewportClient) { GCurrentLevelEditingViewportClient->ApplyDeltaToActors(DeltaMove, FRotator::ZeroRotator, FVector::ZeroVector); } RedrawLevelEditingViewports(); return true; } else if (FParse::Command(&Str, TEXT("HIDE"))) { if (FParse::Command(&Str, TEXT("SELECTED"))) // ACTOR HIDE SELECTED { if (FParse::Command(&Str, TEXT("STARTUP"))) // ACTOR HIDE SELECTED STARTUP { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "HideSelectedAtStartup", "Hide Selected at Editor Startup")); edactHideSelectedStartup(InWorld); return true; } else { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "HideSelected", "Hide Selected")); edactHideSelected(InWorld); SelectNone(true, true); return true; } } else if (FParse::Command(&Str, TEXT("UNSELECTED"))) // ACTOR HIDE UNSELECTEED { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "HideUnselected", "Hide Unselected")); edactHideUnselected(InWorld); SelectNone(true, true); return true; } } else if (FParse::Command(&Str, TEXT("UNHIDE"))) { if (FParse::Command(&Str, TEXT("ALL"))) // ACTOR UNHIDE ALL { if (FParse::Command(&Str, TEXT("STARTUP"))) // ACTOR UNHIDE ALL STARTUP { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ShowAllAtStartup", "Show All at Editor Startup")); edactUnHideAllStartup(InWorld); return true; } else { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "UnHideAll", "UnHide All")); edactUnHideAll(InWorld); return true; } } else if (FParse::Command(&Str, TEXT("SELECTED"))) // ACTOR UNHIDE SELECTED { if (FParse::Command(&Str, TEXT("STARTUP"))) // ACTOR UNHIDE SELECTED STARTUP { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ShowSelectedAtStartup", "Show Selected at Editor Startup")); edactUnHideSelectedStartup(InWorld); return true; } else { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "UnhideSelected", "Unhide Selected")); edactUnhideSelected(InWorld); return true; } } } else if (FParse::Command(&Str, TEXT("APPLYTRANSFORM"))) { CommandIsDeprecated(TEXT("ACTOR APPLYTRANSFORM"), Ar); } else if (FParse::Command(&Str, TEXT("REPLACE"))) { UClass* Class; if (FParse::Command(&Str, TEXT("BRUSH"))) // ACTOR REPLACE BRUSH { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ReplaceSelectedBrushActors", "Replace Selected Brush Actors")); edactReplaceSelectedBrush(InWorld); return true; } else if (ParseObject(Str, TEXT("CLASS="), Class, ANY_PACKAGE)) // ACTOR REPLACE CLASS= { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ReplaceSelectedNonBrushActors", "Replace Selected Non-Brush Actors")); edactReplaceSelectedNonBrushWithClass(Class); return true; } } //@todo locked levels - handle the rest of these....is this required, or can we assume that actors in locked levels can't be selected else if (FParse::Command(&Str, TEXT("SELECT"))) { if (FParse::Command(&Str, TEXT("NONE"))) // ACTOR SELECT NONE { return Exec(InWorld, TEXT("SELECT NONE")); } else if (FParse::Command(&Str, TEXT("ALL"))) // ACTOR SELECT ALL { if (FParse::Command(&Str, TEXT("FROMOBJ"))) // ACTOR SELECT ALL FROMOBJ { bool bHasStaticMeshes = false; TArray ClassesToSelect; for (FSelectionIterator It(GEditor->GetSelectedActorIterator()); It; ++It) { AActor* Actor = static_cast(*It); checkSlow(Actor->IsA(AActor::StaticClass())); if (Actor->IsA(AStaticMeshActor::StaticClass())) { bHasStaticMeshes = true; } else { ClassesToSelect.AddUnique(Actor->GetClass()); } } const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectAll", "Select All")); if (bHasStaticMeshes) { edactSelectMatchingStaticMesh(false); } if (ClassesToSelect.Num() > 0) { for (int Index = 0; Index < ClassesToSelect.Num(); ++Index) { edactSelectOfClass(InWorld, ClassesToSelect[Index]); } } return true; } else if (FParse::Command(&Str, TEXT("CHILDREN"))) // ACTOR SELECT ALL CHILDREN { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectAllChildren", "Select All Children")); edactSelectAllChildren(false); return true; } else if (FParse::Command(&Str, TEXT("DESCENDANTS"))) // ACTOR SELECT ALL DESCENDANTS { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectAllDescendants", "Select All Descendants")); edactSelectAllChildren(true); return true; } else { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectAll", "Select All")); edactSelectAll(InWorld); return true; } } else if (FParse::Command(&Str, TEXT("INSIDE"))) // ACTOR SELECT INSIDE { CommandIsDeprecated(TEXT("ACTOR SELECT INSIDE"), Ar); } else if (FParse::Command(&Str, TEXT("INVERT"))) // ACTOR SELECT INVERT { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectInvert", "Select Invert")); edactSelectInvert(InWorld); return true; } else if (FParse::Command(&Str, TEXT("OFCLASS"))) // ACTOR SELECT OFCLASS CLASS= { UClass* Class; if (ParseObject(Str, TEXT("CLASS="), Class, ANY_PACKAGE)) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectOfClass", "Select Of Class")); edactSelectOfClass(InWorld, Class); } else { UE_SUPPRESS(LogExec, Warning, Ar.Log(TEXT("Missing class"))); } return true; } else if (FParse::Command(&Str, TEXT("OFSUBCLASS"))) // ACTOR SELECT OFSUBCLASS CLASS= { UClass* Class; if (ParseObject(Str, TEXT("CLASS="), Class, ANY_PACKAGE)) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectSubclassOfClass", "Select Subclass Of Class")); edactSelectSubclassOf(InWorld, Class); } else { UE_SUPPRESS(LogExec, Warning, Ar.Log(TEXT("Missing class"))); } return true; } else if (FParse::Command(&Str, TEXT("BASED"))) // ACTOR SELECT BASED { // @TODO UE4 - no longer meaningful return true; } else if (FParse::Command(&Str, TEXT("BYPROPERTY"))) // ACTOR SELECT BYPROPERTY { GEditor->SelectByPropertyColoration(InWorld); return true; } else if (FParse::Command(&Str, TEXT("DELETED"))) // ACTOR SELECT DELETED { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectDeleted", "Select Deleted")); edactSelectDeleted(InWorld); return true; } else if (FParse::Command(&Str, TEXT("MATCHINGSTATICMESH"))) // ACTOR SELECT MATCHINGSTATICMESH { const bool bAllClasses = FParse::Command(&Str, TEXT("ALLCLASSES")); const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectMatchingStaticMesh", "Select Matching Static Mesh")); edactSelectMatchingStaticMesh(bAllClasses); return true; } else if (FParse::Command(&Str, TEXT("MATCHINGSKELETALMESH"))) // ACTOR SELECT MATCHINGSKELETALMESH { const bool bAllClasses = FParse::Command(&Str, TEXT("ALLCLASSES")); const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectMatchingSkeletalMesh", "Select Matching Skeletal Mesh")); edactSelectMatchingSkeletalMesh(bAllClasses); return true; } else if (FParse::Command(&Str, TEXT("MATCHINGMATERIAL"))) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectAllWithMatchingMaterial", "Select All With Matching Material")); edactSelectMatchingMaterial(); return true; } else if (FParse::Command(&Str, TEXT("MATCHINGEMITTER"))) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectMatchingEmitter", "Select Matching Emitters")); edactSelectMatchingEmitter(); return true; } else if (FParse::Command(&Str, TEXT("RELEVANTLIGHTS"))) // ACTOR SELECT RELEVANTLIGHTS { UE_LOG(LogUnrealEdSrv, Log, TEXT("Select relevant lights!")); edactSelectRelevantLights(InWorld); } else { // Get actor name. FName ActorName(NAME_None); if (FParse::Value(Str, TEXT("NAME="), ActorName)) { AActor* Actor = FindObject(InWorld->GetCurrentLevel(), *ActorName.ToString()); const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "SelectToggleSingleActor", "Select Toggle Single Actor")); SelectActor(Actor, !(Actor && Actor->IsSelected()), false, true); } return true; } } else if (FParse::Command(&Str, TEXT("DELETE"))) // ACTOR SELECT DELETE { bool bHandled = false; bHandled |= GLevelEditorModeTools().ProcessEditDelete(); // if not specially handled by the current editing mode, if (!bHandled) { const FScopedTransaction Transaction(bComponentsSelected ? NSLOCTEXT("UnrealEd", "DeleteComponents", "Delete Components") : NSLOCTEXT("UnrealEd", "DeleteActors", "Delete Actors")); FEditorDelegates::OnDeleteActorsBegin.Broadcast(); const bool bCheckRef = GetDefault()->bCheckReferencesOnDelete; edactDeleteSelected(InWorld, true, bCheckRef, bCheckRef); FEditorDelegates::OnDeleteActorsEnd.Broadcast(); } return true; } else if (FParse::Command(&Str, TEXT("UPDATE"))) // ACTOR SELECT UPDATE { bool bLockedLevel = false; for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It) { AActor* Actor = static_cast(*It); checkSlow(Actor->IsA(AActor::StaticClass())); if (!Actor->IsTemplate() && FLevelUtils::IsLevelLocked(Actor)) { bLockedLevel = true; } else { Actor->PreEditChange(NULL); Actor->PostEditChange(); } } if (bLockedLevel) { FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Error_OperationDisallowedOnLockedLevelUpdateActor", "Update Actor: The requested operation could not be completed because the level is locked.")); } return true; } else if (FParse::Command(&Str, TEXT("SET"))) { // @todo DB: deprecate the ACTOR SET exec. RedrawLevelEditingViewports(); return true; } else if (FParse::Command(&Str, TEXT("BAKEPREPIVOT"))) { FScopedLevelDirtied LevelDirtyCallback; FScopedActorPropertiesChange ActorPropertiesChangeCallback; // Bakes the current pivot position into all selected actors FEditorModeTools& EditorModeTools = GLevelEditorModeTools(); for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It) { AActor* Actor = static_cast(*It); checkSlow(Actor->IsA(AActor::StaticClass())); FVector Delta(EditorModeTools.PivotLocation - Actor->GetActorLocation()); Actor->Modify(); Actor->SetPivotOffset(Actor->GetTransform().InverseTransformVector(Delta)); SetPivotMovedIndependently(false); Actor->PostEditMove(true); } GUnrealEd->NoteSelectionChange(); } else if (FParse::Command(&Str, TEXT("UNBAKEPREPIVOT"))) { FScopedLevelDirtied LevelDirtyCallback; FScopedActorPropertiesChange ActorPropertiesChangeCallback; // Resets the PrePivot of the selected actors to 0,0,0 while leaving them in the same world location. FEditorModeTools& EditorModeTools = GLevelEditorModeTools(); for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It) { AActor* Actor = static_cast(*It); checkSlow(Actor->IsA(AActor::StaticClass())); Actor->Modify(); Actor->SetPivotOffset(FVector::ZeroVector); SetPivotMovedIndependently(false); Actor->PostEditMove(true); } GUnrealEd->NoteSelectionChange(); } else if (FParse::Command(&Str, TEXT("RESET"))) { FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "ResetActors", "Reset Actors")); bool bLocation = false; bool bPivot = false; bool bRotation = false; bool bScale = false; if (FParse::Command(&Str, TEXT("LOCATION"))) { bLocation = true; ResetPivot(); } else if (FParse::Command(&Str, TEXT("PIVOT"))) { bPivot = true; ResetPivot(); } else if (FParse::Command(&Str, TEXT("ROTATION"))) { bRotation = true; } else if (FParse::Command(&Str, TEXT("SCALE"))) { bScale = true; } else if (FParse::Command(&Str, TEXT("ALL"))) { bLocation = bRotation = bScale = true; ResetPivot(); } // Fires ULevel::LevelDirtiedEvent when falling out of scope. FScopedLevelDirtied LevelDirtyCallback; bool bHadLockedLevels = false; bool bModifiedActors = false; for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It) { AActor* Actor = static_cast(*It); checkSlow(Actor->IsA(AActor::StaticClass())); if (!Actor->IsTemplate() && FLevelUtils::IsLevelLocked(Actor)) { bHadLockedLevels = true; } else { bModifiedActors = true; Actor->PreEditChange(NULL); Actor->Modify(); if (bLocation) { Actor->SetActorLocation(FVector::ZeroVector, false); } if (bPivot) { Actor->SetPivotOffset(FVector::ZeroVector); } if (bScale && Actor->GetRootComponent() != NULL) { Actor->GetRootComponent()->SetRelativeScale3D(FVector(1.f)); } Actor->MarkPackageDirty(); LevelDirtyCallback.Request(); } } if (bHadLockedLevels) { FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Error_OperationDisallowedOnLockedLevelResetActor", "Reset Actor: The requested operation could not be completed because the level is locked.")); } if (bModifiedActors) { RedrawLevelEditingViewports(); } else { Transaction.Cancel(); } return true; } else if (FParse::Command(&Str, TEXT("DUPLICATE"))) { bool bHandled = false; bHandled |= GLevelEditorModeTools().ProcessEditDuplicate(); // if not specially handled by the current editing mode, if (!bHandled) { //@todo locked levels - if all actor levels are locked, cancel the transaction const FScopedTransaction Transaction(bComponentsSelected ? NSLOCTEXT("UnrealEd", "DuplicateComponents", "Duplicate Components") : NSLOCTEXT("UnrealEd", "DuplicateActors", "Duplicate Actors")); FEditorDelegates::OnDuplicateActorsBegin.Broadcast(); // duplicate selected ABrush::SetSuppressBSPRegeneration(true); edactDuplicateSelected(InWorld->GetCurrentLevel(), GetDefault()->GridEnabled); ABrush::SetSuppressBSPRegeneration(false); // Find out if any of the selected actors will change the BSP. // and only then rebuild BSP as this is expensive. const FSelectedActorInfo& SelectedActors = AssetSelectionUtils::GetSelectedActorInfo(); if (SelectedActors.bHaveBrush) { RebuildAlteredBSP(); // Update the Bsp of any levels containing a modified brush } FEditorDelegates::OnDuplicateActorsEnd.Broadcast(); } RedrawLevelEditingViewports(); return true; } else if (FParse::Command(&Str, TEXT("ALIGN"))) { if (FParse::Command(&Str, TEXT("ORIGIN"))) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "Undo_SnapBrushOrigin", "Snap Brush Origin")); edactAlignOrigin(); RedrawLevelEditingViewports(); return true; } else // "VERTS" (default) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "Undo_SnapBrushVertices", "Snap Brush Vertices")); edactAlignVertices(); RedrawLevelEditingViewports(); RebuildAlteredBSP(); // Update the Bsp of any levels containing a modified brush return true; } } else if (FParse::Command(&Str, TEXT("TOGGLE"))) { if (FParse::Command(&Str, TEXT("LOCKMOVEMENT"))) // ACTOR TOGGLE LOCKMOVEMENT { ToggleSelectedActorMovementLock(); } RedrawLevelEditingViewports(); return true; } else if (FParse::Command(&Str, TEXT("LEVELCURRENT"))) { MakeSelectedActorsLevelCurrent(); return true; } else if (FParse::Command(&Str, TEXT("MOVETOCURRENT"))) { UEditorLevelUtils::MoveSelectedActorsToLevel(InWorld->GetCurrentLevel()); return true; } else if (FParse::Command(&Str, TEXT("DESELECT"))) { const FScopedTransaction Transaction(NSLOCTEXT("UnrealEd", "DeselectActors", "Deselect Actor(s)")); GEditor->GetSelectedActors()->Modify(); // deselects everything in UnrealEd GUnrealEd->SelectNone(true, true); return true; } else if (FParse::Command(&Str, TEXT("EXPORT"))) { if (FParse::Command(&Str, TEXT("FBX"))) { TArray SaveFilenames; IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); bool bSave = false; if (DesktopPlatform) { void* ParentWindowWindowHandle = NULL; IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked(TEXT("MainFrame")); const TSharedPtr& MainFrameParentWindow = MainFrameModule.GetParentWindow(); if (MainFrameParentWindow.IsValid() && MainFrameParentWindow->GetNativeWindow().IsValid()) { ParentWindowWindowHandle = MainFrameParentWindow->GetNativeWindow()->GetOSWindowHandle(); } bSave = DesktopPlatform->SaveFileDialog( ParentWindowWindowHandle, NSLOCTEXT("UnrealEd", "StaticMeshEditor_ExportToPromptTitle", "Export to...").ToString(), *FEditorDirectories::Get().GetLastDirectory(ELastDirectory::GENERIC_EXPORT), TEXT(""), TEXT("FBX document|*.fbx"), EFileDialogFlags::None, SaveFilenames); } // Show dialog and execute the export if the user did not cancel out if (bSave) { // Get the filename from dialog FString FileName = SaveFilenames[0]; FEditorDirectories::Get().SetLastDirectory(ELastDirectory::GENERIC_EXPORT, FPaths::GetPath(FileName)); // Save path as default for next time. INodeNameAdapter NodeNameAdapter; UnFbx::FFbxExporter* Exporter = UnFbx::FFbxExporter::GetInstance(); // Show the fbx export dialog options bool ExportCancel = false; bool ExportAll = false; Exporter->FillExportOptions(false, true, FileName, ExportCancel, ExportAll); if (!ExportCancel) { Exporter->CreateDocument(); for (FSelectionIterator It(GetSelectedActorIterator()); It; ++It) { AActor* Actor = static_cast(*It); if (Actor->IsA(AActor::StaticClass())) { if (Actor->IsA(AStaticMeshActor::StaticClass())) { Exporter->ExportStaticMesh(Actor, CastChecked(Actor)->GetStaticMeshComponent(), NodeNameAdapter); } else if (Actor->IsA(ASkeletalMeshActor::StaticClass())) { Exporter->ExportSkeletalMesh(Actor, CastChecked(Actor)->GetSkeletalMeshComponent(), NodeNameAdapter); } else if (Actor->IsA(ABrush::StaticClass())) { Exporter->ExportBrush(CastChecked(Actor), NULL, true, NodeNameAdapter); } } } Exporter->WriteToFile(*FileName); } } return true; } } else if (FParse::Command(&Str, TEXT("SNAP"))) // ACTOR SNAP { FSnappingUtils::EnableActorSnap(!FSnappingUtils::IsSnapToActorEnabled()); return true; } return false; } bool UUnrealEdEngine::Exec_Mode(const TCHAR* Str, FOutputDevice& Ar) { int32 DWord1; if (FParse::Command(&Str, TEXT("WIDGETCOORDSYSTEMCYCLE"))) { const bool bGetRawValue = true; int32 Wk = GLevelEditorModeTools().GetCoordSystem(bGetRawValue); Wk++; if (Wk == COORD_Max) { Wk -= COORD_Max; } GLevelEditorModeTools().SetCoordSystem((ECoordSystem)Wk); FEditorSupportDelegates::RedrawAllViewports.Broadcast(); FEditorSupportDelegates::UpdateUI.Broadcast(); } if (FParse::Command(&Str, TEXT("WIDGETMODECYCLE"))) { GLevelEditorModeTools().CycleWidgetMode(); } if (FParse::Value(Str, TEXT("GRID="), DWord1)) { FinishAllSnaps(); ULevelEditorViewportSettings* ViewportSettings = GetMutableDefault(); ViewportSettings->GridEnabled = DWord1; ViewportSettings->PostEditChange(); FEditorDelegates::OnGridSnappingChanged.Broadcast(ViewportSettings->GridEnabled, GetGridSize()); FEditorSupportDelegates::UpdateUI.Broadcast(); } if (FParse::Value(Str, TEXT("ROTGRID="), DWord1)) { FinishAllSnaps(); ULevelEditorViewportSettings* ViewportSettings = GetMutableDefault(); ViewportSettings->RotGridEnabled = DWord1; ViewportSettings->PostEditChange(); FEditorSupportDelegates::UpdateUI.Broadcast(); } if (FParse::Value(Str, TEXT("SCALEGRID="), DWord1)) { FinishAllSnaps(); ULevelEditorViewportSettings* ViewportSettings = GetMutableDefault(); ViewportSettings->SnapScaleEnabled = DWord1; ViewportSettings->PostEditChange(); FEditorSupportDelegates::UpdateUI.Broadcast(); } if (FParse::Value(Str, TEXT("SNAPVERTEX="), DWord1)) { FinishAllSnaps(); ULevelEditorViewportSettings* ViewportSettings = GetMutableDefault(); ViewportSettings->bSnapVertices = !!DWord1; ViewportSettings->PostEditChange(); FEditorSupportDelegates::UpdateUI.Broadcast(); } if (FParse::Value(Str, TEXT("SHOWBRUSHMARKERPOLYS="), DWord1)) { FinishAllSnaps(); bShowBrushMarkerPolys = DWord1; } if (FParse::Value(Str, TEXT("SELECTIONLOCK="), DWord1)) { FinishAllSnaps(); // If -1 is passed in, treat it as a toggle. Otherwise, use the value as a literal assignment. if (DWord1 == -1) { GEdSelectionLock = (GEdSelectionLock == 0) ? 1 : 0; } else { GEdSelectionLock = !!DWord1; } } if (FParse::Value(Str, TEXT("USESIZINGBOX="), DWord1)) { FinishAllSnaps(); // If -1 is passed in, treat it as a toggle. Otherwise, use the value as a literal assignment. if (DWord1 == -1) UseSizingBox = (UseSizingBox == 0) ? 1 : 0; else UseSizingBox = DWord1; } if (GCurrentLevelEditingViewportClient) { int32 NewCameraSpeed = 1; if (FParse::Value(Str, TEXT("SPEED="), NewCameraSpeed)) { NewCameraSpeed = FMath::Clamp(NewCameraSpeed, 1, FLevelEditorViewportClient::MaxCameraSpeeds); GetMutableDefault()->CameraSpeed = NewCameraSpeed; } } FParse::Value(Str, TEXT("SNAPDIST="), GetMutableDefault()->SnapDistance); // // Major modes: // FEditorModeID EditorMode = FBuiltinEditorModes::EM_None; FString CommandToken = FParse::Token(Str, false); FEdMode* FoundMode = GLevelEditorModeTools().GetActiveMode(FName(*CommandToken)); if (FoundMode != NULL) { EditorMode = FName(*CommandToken); } if (EditorMode != FBuiltinEditorModes::EM_None) { FEditorDelegates::ChangeEditorMode.Broadcast(EditorMode); } // Reset the roll on all viewport cameras for (FLevelEditorViewportClient* ViewportClient: GetLevelViewportClients()) { if (ViewportClient->IsPerspective()) { ViewportClient->RemoveCameraRoll(); } } FEditorSupportDelegates::RedrawAllViewports.Broadcast(); return true; } bool UUnrealEdEngine::Exec_Group(const TCHAR* Str, FOutputDevice& Ar) { if (UActorGroupingUtils::IsGroupingActive()) { if (FParse::Command(&Str, TEXT("REGROUP"))) { UActorGroupingUtils::Get()->GroupSelected(); return true; } else if (FParse::Command(&Str, TEXT("UNGROUP"))) { UActorGroupingUtils::Get()->UngroupSelected(); return true; } } return false; } #undef LOCTEXT_NAMESPACE