// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #if USE_EDITOR #include "Editor.h" #include "ProjectInfo.h" #include "Engine/Core/Log.h" #include "Scripting/ScriptsBuilder.h" #include "Windows/SplashScreen.h" #include "Managed/ManagedEditor.h" #include "Engine/Scripting/ManagedCLR/MClass.h" #include "Engine/Scripting/ManagedCLR/MMethod.h" #include "Engine/Serialization/FileWriteStream.h" #include "Engine/Serialization/FileReadStream.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Platform/File.h" #include "Engine/Platform/MessageBox.h" #include "Engine/Engine/CommandLine.h" #include "Engine/Engine/Globals.h" #include "Engine/Engine/Engine.h" #include "Engine/ShadowsOfMordor/Builder.h" #include "Engine/Profiler/ProfilerCPU.h" #include "FlaxEngine.Gen.h" #if PLATFORM_LINUX #include "Engine/Tools/TextureTool/TextureTool.h" #endif namespace EditorImpl { bool IsOldProjectXmlFormat = false; bool HasFocus = false; SplashScreen* Splash = nullptr; void OnUpdate(); } ManagedEditor* Editor::Managed = nullptr; ProjectInfo* Editor::Project = nullptr; bool Editor::IsPlayMode = false; bool Editor::IsOldProjectOpened = true; int32 Editor::LastProjectOpenedEngineBuild = 0; void Editor::CloseSplashScreen() { SAFE_DELETE(EditorImpl::Splash); } bool Editor::CheckProjectUpgrade() { const auto versionFilePath = Globals::ProjectCacheFolder / TEXT("version"); // Load version cache file int32 lastMajor = FLAXENGINE_VERSION_MAJOR; int32 lastMinor = FLAXENGINE_VERSION_MINOR; int32 lastBuild = FLAXENGINE_VERSION_BUILD; if (FileSystem::FileExists(versionFilePath)) { auto file = FileReadStream::Open(versionFilePath); if (file) { file->ReadInt32(&lastMajor); file->ReadInt32(&lastMinor); file->ReadInt32(&lastBuild); // Invalidate results if data has issues if (file->HasError() || lastMajor < 0 || lastMinor < 0 || lastMajor > 100 || lastMinor > 1000) { lastMajor = FLAXENGINE_VERSION_MAJOR; lastMinor = FLAXENGINE_VERSION_MINOR; lastBuild = FLAXENGINE_VERSION_BUILD; LOG(Warning, "Invalid version cache data"); } else { LOG(Info, "Last project open version: {0}.{1}.{2}", lastMajor, lastMinor, lastBuild); LastProjectOpenedEngineBuild = lastBuild; } Delete(file); } } else { LOG(Warning, "Missing version cache file"); } // Check if project is in the old, deprecated layout if (EditorImpl::IsOldProjectXmlFormat) { // [Deprecated: 16.04.2020, expires 16.04.2021] LOG(Warning, "The project is in an old format and will be upgraded."); const auto result = MessageBox::Show(TEXT("The Flax project is in an old format and will be upgraded. Loading it may modify existing data so older editor version won't open it. Do you want to perform a backup before or cancel operation?"), TEXT("Project upgrade"), MessageBoxButtons::YesNoCancel, MessageBoxIcon::Question); if (result == DialogResult::Yes) { if (BackupProject()) { LOG(Warning, "Backup failed"); return true; } } else if (result == DialogResult::No) { // Don't backup, just load } else { // Cancel return true; } const String& root = Globals::ProjectFolder; const String& name = Project->Name; String codeName; for (int32 i = 0; i < name.Length(); i++) { Char c = name[i]; if (StringUtils::IsAlnum(c) && c != ' ' && c != '.') codeName += c; } const String& sourceFolder = Globals::ProjectSourceFolder; const String gameModuleFolder = sourceFolder / codeName; const String gameEditorModuleFolder = sourceFolder / codeName + TEXT("Editor"); const String tempSourceSetup = Globals::ProjectCacheFolder / TEXT("UpgradeSource"); const String tempSourceSetupGame = tempSourceSetup / codeName; const String tempSourceSetupGameEditor = tempSourceSetup / codeName + TEXT("Editor"); // Remove old project files FileSystem::DeleteFile(root / name + TEXT(".sln")); FileSystem::DeleteFile(root / name + TEXT(".csproj")); FileSystem::DeleteFile(root / name + TEXT(".csproj.user")); FileSystem::DeleteFile(root / name + TEXT(".Editor.csproj")); FileSystem::DeleteFile(root / name + TEXT(".Editor.csproj.user")); // Remove old cache files FileSystem::DeleteDirectory(root / TEXT("Cache/Assemblies")); FileSystem::DeleteDirectory(root / TEXT("Cache/bin")); FileSystem::DeleteDirectory(root / TEXT("Cache/obj")); FileSystem::DeleteDirectory(root / TEXT("Cache/Shaders")); // Move C# files to new locations FileSystem::DeleteDirectory(tempSourceSetup); FileSystem::CreateDirectory(tempSourceSetup); Array files; FileSystem::DirectoryGetFiles(files, sourceFolder, TEXT("*"), DirectorySearchOption::AllDirectories); bool useEditorModule = false; for (auto& file : files) { String tempSourceFile; if (file.Contains(TEXT("/Editor/"))) { useEditorModule = true; tempSourceFile = tempSourceSetupGameEditor / StringUtils::GetFileName(file); } else { tempSourceFile = tempSourceSetupGame / FileSystem::ConvertAbsolutePathToRelative(sourceFolder, file); } FileSystem::CreateDirectory(StringUtils::GetDirectoryName(tempSourceFile)); FileSystem::CopyFile(tempSourceFile, file); } FileSystem::DeleteDirectory(sourceFolder); FileSystem::CopyDirectory(sourceFolder, tempSourceSetup, true); FileSystem::DeleteDirectory(tempSourceSetup); // Generate module files File::WriteAllText(gameModuleFolder / String::Format(TEXT("{0}.Build.cs"), codeName), String::Format(TEXT( "using Flax.Build;\n" "using Flax.Build.NativeCpp;\n" "\n" "public class {0} : GameModule\n" "{{\n" " /// \n" " public override void Setup(BuildOptions options)\n" " {{\n" " base.Setup(options);\n" "\n" " // Ignore compilation warnings due to missing code documentation comments\n" " options.ScriptingAPI.IgnoreMissingDocumentationWarnings = true;\n" "\n" " // Here you can modify the build options for your game module\n" " // To reference another module use: options.PublicDependencies.Add(\"Audio\");\n" " // To add C++ define use: options.PublicDefinitions.Add(\"COMPILE_WITH_FLAX\");\n" " // To learn more see scripting documentation.\n" " BuildNativeCode = false;\n" " }}\n" "}}\n" ), codeName), Encoding::Unicode); if (useEditorModule) { File::WriteAllText(gameEditorModuleFolder / String::Format(TEXT("{0}Editor.Build.cs"), codeName), String::Format(TEXT( "using Flax.Build;\n" "using Flax.Build.NativeCpp;\n" "\n" "public class {0}Editor : GameEditorModule\n" "{{\n" " /// \n" " public override void Setup(BuildOptions options)\n" " {{\n" " base.Setup(options);\n" "\n" " // Reference game source module to access game code types\n" " options.PublicDependencies.Add(\"{0}\");\n" "\n" " // Ignore compilation warnings due to missing code documentation comments\n" " options.ScriptingAPI.IgnoreMissingDocumentationWarnings = true;\n" "\n" " // Here you can modify the build options for your game editor module\n" " // To reference another module use: options.PublicDependencies.Add(\"Audio\");\n" " // To add C++ define use: options.PublicDefinitions.Add(\"COMPILE_WITH_FLAX\");\n" " // To learn more see scripting documentation.\n" " BuildNativeCode = false;\n" " }}\n" "}}\n" ), codeName), Encoding::Unicode); } // Generate target files File::WriteAllText(sourceFolder / String::Format(TEXT("{0}Target.Build.cs"), codeName), String::Format(TEXT( "using Flax.Build;\n" "\n" "public class {0}Target : GameProjectTarget\n" "{{\n" " /// \n" " public override void Init()\n" " {{\n" " base.Init();\n" "\n" " // Reference the modules for game\n" " Modules.Add(\"{0}\");\n" " }}\n" "}}\n" ), codeName), Encoding::Unicode); const String editorTargetGameEditorModule = useEditorModule ? String::Format(TEXT(" Modules.Add(\"{0}Editor\");\n"), codeName) : String::Empty; File::WriteAllText(sourceFolder / String::Format(TEXT("{0}EditorTarget.Build.cs"), codeName), String::Format(TEXT( "using Flax.Build;\n" "\n" "public class {0}EditorTarget : GameProjectEditorTarget\n" "{{\n" " /// \n" " public override void Init()\n" " {{\n" " base.Init();\n" "\n" " // Reference the modules for editor\n" " Modules.Add(\"{0}\");\n" "{1}" " }}\n" "}}\n" ), codeName, editorTargetGameEditorModule), Encoding::Unicode); // Generate new project file Project->ProjectPath = root / String::Format(TEXT("{0}.flaxproj"), codeName); Project->GameTarget = codeName + TEXT("Target"); Project->EditorTarget = codeName + TEXT("EditorTarget"); Project->SaveProject(); // Remove old project file FileSystem::DeleteFile(root / TEXT("Project.xml")); LOG(Warning, "Project layout upgraded!"); } // Check if last version was the same else if (lastMajor == FLAXENGINE_VERSION_MAJOR && lastMinor == FLAXENGINE_VERSION_MINOR) { // Do nothing IsOldProjectOpened = false; } // Check if last version was older else if (lastMajor < FLAXENGINE_VERSION_MAJOR || (lastMajor == FLAXENGINE_VERSION_MAJOR && lastMinor < FLAXENGINE_VERSION_MINOR)) { LOG(Warning, "The project was opened with the older editor version last time"); const auto result = MessageBox::Show(TEXT("The project was opened with the older editor version last time. Loading it may modify existing data so older editor version won't open it. Do you want to perform a backup before or cancel operation?"), TEXT("Project upgrade"), MessageBoxButtons::YesNoCancel, MessageBoxIcon::Question); if (result == DialogResult::Yes) { if (BackupProject()) { LOG(Warning, "Backup failed"); return true; } } else if (result == DialogResult::No) { // Don't backup, just load } else { // Cancel return true; } } // Check if last version was newer else if (lastMajor > FLAXENGINE_VERSION_MAJOR || (lastMajor == FLAXENGINE_VERSION_MAJOR && lastMinor > FLAXENGINE_VERSION_MINOR)) { LOG(Warning, "The project was opened with the newer editor version last time"); const auto result = MessageBox::Show(TEXT("The project was opened with the newer editor version last time. Loading it may fail and corrupt existing data. Do you want to perform a backup before or cancel operation?"), TEXT("Project upgrade"), MessageBoxButtons::YesNoCancel, MessageBoxIcon::Warning); if (result == DialogResult::Yes) { if (BackupProject()) { LOG(Warning, "Backup failed"); return true; } } else if (result == DialogResult::No) { // Don't backup, just load } else { // Cancel return true; } } // Upgrade old 0.7 projects // [Deprecated: 01.11.2020, expires 01.11.2021] if (lastMajor == 0 && lastMinor == 7 && lastBuild <= 6197) { Array files; FileSystem::DirectoryGetFiles(files, Globals::ProjectSourceFolder, TEXT("*.Gen.cs")); for (auto& file : files) FileSystem::DeleteFile(file); } // Update version the cache file { auto file = FileWriteStream::Open(versionFilePath); if (file) { file->WriteInt32(FLAXENGINE_VERSION_MAJOR); file->WriteInt32(FLAXENGINE_VERSION_MINOR); file->WriteInt32(FLAXENGINE_VERSION_BUILD); Delete(file); } else { LOG(Warning, "Failed to create version cache file"); } } return false; } bool Editor::BackupProject() { // Create backup directory auto dstPath = Globals::ProjectFolder + TEXT(" - Backup"); { int32 count = 0; while (count < 1000 && FileSystem::DirectoryExists(dstPath)) { dstPath = Globals::ProjectFolder + TEXT(" - Backup") + StringUtils::ToString(count++); } } LOG(Info, "Backup project to \"{0}\"", dstPath); // Copy everything return FileSystem::CopyDirectory(dstPath, Globals::ProjectFolder, true); } int32 Editor::LoadProduct() { // Flax Editor product Globals::ProductName = TEXT("Flax Editor"); Globals::CompanyName = TEXT("Flax"); #if FLAX_TESTS // Flax Tests use auto-generated temporary project CommandLine::Options.Project = Globals::TemporaryFolder / TEXT("Project"); CommandLine::Options.NewProject = true; #endif // Gather project directory from the command line String projectPath = CommandLine::Options.Project.TrimTrailing(); const int32 startIndex = projectPath.StartsWith('\"') || projectPath.StartsWith('\'') ? 1 : 0; const int32 length = projectPath.Length() - (projectPath.EndsWith('\"') || projectPath.EndsWith('\'') ? 1 : 0) - startIndex; if (length > 0) { projectPath = projectPath.Substring(startIndex, length - startIndex); StringUtils::PathRemoveRelativeParts(projectPath); if (FileSystem::IsRelative(projectPath)) { projectPath = Platform::GetWorkingDirectory() / projectPath; StringUtils::PathRemoveRelativeParts(projectPath); } if (projectPath.EndsWith(TEXT(".flaxproj"))) { projectPath = StringUtils::GetDirectoryName(projectPath); } } else { projectPath.Clear(); } // Create new project option if (CommandLine::Options.NewProject) { if (projectPath.IsEmpty()) projectPath = Platform::GetWorkingDirectory(); else if (!FileSystem::DirectoryExists(projectPath)) FileSystem::CreateDirectory(projectPath); FileSystem::NormalizePath(projectPath); String folderName = StringUtils::GetFileName(projectPath); String tmpName; for (int32 i = 0; i < folderName.Length(); i++) { Char c = folderName[i]; if (StringUtils::IsAlnum(c) && c != ' ' && c != '.') tmpName += c; } // Create project file ProjectInfo newProject; newProject.Name = MoveTemp(tmpName); newProject.ProjectPath = projectPath / newProject.Name + TEXT(".flaxproj"); newProject.ProjectFolderPath = projectPath; newProject.Version = Version(1, 0); newProject.Company = TEXT("My Company"); newProject.MinEngineVersion = FLAXENGINE_VERSION; newProject.GameTarget = TEXT("GameTarget"); newProject.EditorTarget = TEXT("GameEditorTarget"); auto& flaxRef = newProject.References.AddOne(); flaxRef.Name = TEXT("$(EnginePath)/Flax.flaxproj"); flaxRef.Project = nullptr; if (newProject.SaveProject()) return 10; // Generate source files if (FileSystem::CreateDirectory(projectPath / TEXT("Content"))) return 11; if (FileSystem::CreateDirectory(projectPath / TEXT("Source/Game"))) return 11; bool failed = File::WriteAllText(projectPath / TEXT("Source/GameTarget.Build.cs"),TEXT( "using Flax.Build;\n" "\n" "public class GameTarget : GameProjectTarget\n" "{\n" " /// \n" " public override void Init()\n" " {\n" " base.Init();\n" "\n" " // Reference the modules for game\n" " Modules.Add(\"Game\");\n" " }\n" "}\n"), Encoding::Unicode); failed |= File::WriteAllText(projectPath / TEXT("Source/GameEditorTarget.Build.cs"),TEXT( "using Flax.Build;\n" "\n" "public class GameEditorTarget : GameProjectEditorTarget\n" "{\n" " /// \n" " public override void Init()\n" " {\n" " base.Init();\n" "\n" " // Reference the modules for editor\n" " Modules.Add(\"Game\");\n" " }\n" "}\n"), Encoding::Unicode); failed |= File::WriteAllText(projectPath / TEXT("Source/Game/Game.Build.cs"),TEXT( "using Flax.Build;\n" "using Flax.Build.NativeCpp;\n" "\n" "public class Game : GameModule\n" "{\n" " /// \n" " public override void Init()\n" " {\n" " base.Init();\n" "\n" " // C#-only scripting\n" " BuildNativeCode = false;\n" " }\n" "\n" " /// \n" " public override void Setup(BuildOptions options)\n" " {\n" " base.Setup(options);\n" "\n" " options.ScriptingAPI.IgnoreMissingDocumentationWarnings = true;\n" "\n" " // Here you can modify the build options for your game module\n" " // To reference another module use: options.PublicDependencies.Add(\"Audio\");\n" " // To add C++ define use: options.PublicDefinitions.Add(\"COMPILE_WITH_FLAX\");\n" " // To learn more see scripting documentation.\n" " }\n" "}\n"), Encoding::Unicode); if (failed) return 12; } // Missing project case if (projectPath.IsEmpty()) { #if PLATFORM_HAS_HEADLESS_MODE if (CommandLine::Options.Headless) { Platform::Fatal(TEXT("Missing project path.")); return -1; } #endif // Ask user to pick a project to open Array files; if (FileSystem::ShowOpenFileDialog( nullptr, StringView::Empty, TEXT("Project files (*.flaxproj)\0*.flaxproj\0All files (*.*)\0*.*\0"), false, TEXT("Select project to open in Editor"), files) || files.Count() != 1) { return -1; } if (!FileSystem::FileExists(files[0])) { Platform::Fatal(TEXT("Cannot opoen selected project file because it doesn't exist.")); return -1; } projectPath = StringUtils::GetDirectoryName(files[0]); StringUtils::PathRemoveRelativeParts(projectPath); } // Check folder with project exists if (!FileSystem::DirectoryExists(projectPath)) { Platform::Fatal(String::Format(TEXT("Project folder '{0}' is missing"), projectPath)); return -1; } Globals::ProjectFolder = projectPath; ASSERT(!FileSystem::IsRelative(Globals::ProjectFolder)); // Check if opening old project (the one with Project.xml file) // [Deprecated: 16.04.2020, expires 16.04.2021] const String projectXmlPath = projectPath / TEXT("Project.xml"); Project = New(); ProjectInfo::ProjectsCache.Add(Project); if (FileSystem::FileExists(projectXmlPath)) { // Load project const bool loadResult = Project->LoadOldProject(projectXmlPath); if (loadResult) { Platform::Fatal(TEXT("Cannot load project.")); return -2; } EditorImpl::IsOldProjectXmlFormat = true; } else { // Load project Array projectFiles; FileSystem::DirectoryGetFiles(projectFiles, projectPath, TEXT("*.flaxproj"), DirectorySearchOption::TopDirectoryOnly); if (projectFiles.Count() == 0) { Platform::Fatal(TEXT("Missing project file (*.flaxproj).")); return -2; } if (projectFiles.Count() > 1) { Platform::Fatal(TEXT("Too many project files.")); return -2; } const bool loadResult = Project->LoadProject(projectFiles[0]); if (loadResult) { Platform::Fatal(TEXT("Cannot load project.")); return -2; } } HashSet projects; Project->GetAllProjects(projects); // Validate project min supported version (older engine may try to load newer project) // Special check if project specifies only build number, then major/minor fields are set to 0 const auto engineVersion = FLAXENGINE_VERSION; for (auto e : projects) { const auto project = e.Item; if (project->MinEngineVersion > engineVersion || (project->MinEngineVersion.Major() == 0 && project->MinEngineVersion.Minor() == 0 && project->MinEngineVersion.Build() > engineVersion.Build()) ) { Platform::Fatal(String::Format(TEXT("Cannot open project \"{0}\".\nIt requires version {1} but editor has version {2}.\nPlease update the editor."), project->Name, project->MinEngineVersion.ToString(), engineVersion.ToString())); return -2; } } return 0; } Window* Editor::CreateMainWindow() { Window* window = Managed->GetMainWindow(); #if PLATFORM_LINUX // Set window icon const String iconPath = Globals::BinariesFolder / TEXT("Logo.png"); if (FileSystem::FileExists(iconPath)) { TextureData icon; if (TextureTool::ImportTexture(iconPath, icon)) { LOG(Warning, "Failed to load icon file."); } else { window->SetIcon(icon); } } else { LOG(Warning, "Missing icon file."); } #endif return window; } bool Editor::Init() { // Scripts project files generation from command line if (CommandLine::Options.GenProjectFiles) { const String customArgs = TEXT("-verbose -log -logfile=\"Cache/Intermediate/ProjectFileLog.txt\""); const bool failed = ScriptsBuilder::GenerateProject(customArgs); exit(failed ? 1 : 0); return true; } PROFILE_CPU(); // If during last lightmaps baking engine crashed we could try to restore the progress ShadowsOfMordor::Builder::Instance()->CheckIfRestoreState(); Engine::Update.Bind(&EditorImpl::OnUpdate); Managed = New(); // Show splash screen { PROFILE_CPU_NAMED("Splash"); if (EditorImpl::Splash == nullptr) EditorImpl::Splash = New(); EditorImpl::Splash->SetTitle(Project->Name); EditorImpl::Splash->Show(); } // Initialize managed editor Managed->Init(); // Start play if requested by cmd line if (CommandLine::Options.Play.HasValue()) { Managed->RequestStartPlayOnEditMode(); } return false; } void Editor::BeforeRun() { // If during last lightmaps baking engine crashed we could try to restore the progress if (ShadowsOfMordor::Builder::Instance()->RestoreState()) Managed->GetClass()->GetMethod("Internal_StartLightingBake")->Invoke(Managed->GetOrCreateManagedInstance(), nullptr, nullptr); } void Editor::BeforeExit() { CloseSplashScreen(); Managed->Exit(); SAFE_DELETE(Managed); Project = nullptr; ProjectInfo::ProjectsCache.ClearDelete(); } void EditorImpl::OnUpdate() { // Update c# editor Editor::Managed->Update(); // If editor thread doesn't have the focus, don't suck up too much CPU time const auto hasFocus = Engine::HasFocus; if (HasFocus && !hasFocus) { // Drop our priority to speed up whatever is in the foreground Platform::SetThreadPriority(ThreadPriority::BelowNormal); } else if (hasFocus && !HasFocus) { // Boost our priority back to normal Platform::SetThreadPriority(ThreadPriority::Normal); } HasFocus = hasFocus; } #endif