// Copyright (c) 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 "Engine/Profiler/ProfilerMemory.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() { PROFILE_MEM(Editor); const auto versionFilePath = Globals::ProjectCacheFolder / TEXT("version"); // Load version cache file struct VersionCache { // When changing this ensure that Flax Launcher properly reads the version int32 Major = FLAXENGINE_VERSION_MAJOR; int32 Minor = FLAXENGINE_VERSION_MINOR; int32 Build = FLAXENGINE_VERSION_BUILD; int32 RealSize = sizeof(Real); // Rebuild when changing between Large Worlds }; VersionCache lastVersion; if (FileSystem::FileExists(versionFilePath)) { auto file = FileReadStream::Open(versionFilePath); if (file) { file->ReadBytes(&lastVersion, sizeof(lastVersion)); // Invalidate results if data has issues if (file->HasError() || lastVersion.Major < 0 || lastVersion.Minor < 0 || lastVersion.Major > 100 || lastVersion.Minor > 1000) { lastVersion = VersionCache(); LOG(Warning, "Invalid version cache data"); } else { LOG(Info, "Last project open version: {0}.{1}.{2}", lastVersion.Major, lastVersion.Minor, lastVersion.Build); LastProjectOpenedEngineBuild = lastVersion.Build; } Delete(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); 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); 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::UTF8); 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::UTF8); } // 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(nameof({0}));\n" " }}\n" "}}\n" ), codeName), Encoding::UTF8); 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(nameof({0}));\n" "{1}" " }}\n" "}}\n" ), codeName, editorTargetGameEditorModule), Encoding::UTF8); // 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 (lastVersion.Major == FLAXENGINE_VERSION_MAJOR && lastVersion.Minor == FLAXENGINE_VERSION_MINOR) { // Do nothing IsOldProjectOpened = false; } // Check if last version was older else if (lastVersion.Major < FLAXENGINE_VERSION_MAJOR || (lastVersion.Major == FLAXENGINE_VERSION_MAJOR && lastVersion.Minor < FLAXENGINE_VERSION_MINOR)) { LOG(Warning, "The project was last opened with an older editor version"); const auto result = MessageBox::Show(TEXT("The project was last opened with an older editor version.\nLoading it may modify existing data, which can result in older editor versions being unable to open it.\n\nDo you want to perform a backup before or cancel the 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 (lastVersion.Major > FLAXENGINE_VERSION_MAJOR || (lastVersion.Major == FLAXENGINE_VERSION_MAJOR && lastVersion.Minor > FLAXENGINE_VERSION_MINOR)) { LOG(Warning, "The project was last opened with a newer editor version"); const auto result = MessageBox::Show(TEXT("The project was last opened with a newer editor version.\nLoading it may fail and corrupt existing data.\n\nDo you want to perform a backup before loading or cancel the 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; } } // When changing between major/minor version clear some caches to prevent possible issues if (lastVersion.Major != FLAXENGINE_VERSION_MAJOR || lastVersion.Minor != FLAXENGINE_VERSION_MINOR || lastVersion.RealSize != sizeof(Real)) { LOG(Info, "Cleaning cache files from different engine version"); FileSystem::DeleteDirectory(Globals::ProjectFolder / TEXT("Cache/Cooker")); FileSystem::DeleteDirectory(Globals::ProjectFolder / TEXT("Cache/Intermediate")); } // Upgrade old 0.7 projects // [Deprecated: 01.11.2020, expires 01.11.2021] if (lastVersion.Major == 0 && lastVersion.Minor == 7 && lastVersion.Build <= 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) { lastVersion = VersionCache(); file->WriteBytes(&lastVersion, sizeof(lastVersion)); Delete(file); } else { LOG(Error, "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); } int32 Editor::LoadProduct() { PROFILE_MEM(Editor); // 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.IsTrue()) { Array projectFiles; FileSystem::DirectoryGetFiles(projectFiles, projectPath, TEXT("*.flaxproj"), DirectorySearchOption::TopDirectoryOnly); if (projectFiles.Count() > 1) { Platform::Fatal(TEXT("Too many project files.")); return -2; } else if (projectFiles.Count() == 1) { LOG(Info, "Skip creating new project because it already exists"); CommandLine::Options.NewProject.Reset(); } else { Array files; FileSystem::DirectoryGetFiles(files, projectPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); if (files.Count() > 0) { Platform::Fatal(String::Format(TEXT("Target project folder '{0}' is not empty."), projectPath)); return -1; } } } if (CommandLine::Options.NewProject.IsTrue()) { 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(nameof(Game));\n" " }\n" "}\n"), Encoding::UTF8); 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(nameof(Game));\n" " }\n" "}\n"), Encoding::UTF8); 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::UTF8); if (failed) return 12; } // Get the last opened project path String localCachePath; FileSystem::GetSpecialFolderPath(SpecialFolder::AppData, localCachePath); String editorConfigPath = localCachePath / TEXT("Flax"); String lastProjectSettingPath = editorConfigPath / TEXT("LastProject.txt"); if (!FileSystem::DirectoryExists(editorConfigPath)) FileSystem::CreateDirectory(editorConfigPath); String lastProjectPath; if (FileSystem::FileExists(lastProjectSettingPath)) File::ReadAllText(lastProjectSettingPath, lastProjectPath); // Try to open the last project when requested if (projectPath.IsEmpty() && CommandLine::Options.LastProject.IsTrue()) { if (!lastProjectPath.IsEmpty() && FileSystem::DirectoryExists(lastProjectPath)) projectPath = lastProjectPath; } // Missing project case if (projectPath.IsEmpty()) { #if PLATFORM_HAS_HEADLESS_MODE if (CommandLine::Options.Headless.IsTrue()) { Platform::Fatal(TEXT("Missing project path.")); return -1; } #endif // Ask user to pick a project to open Array files; if (FileSystem::ShowOpenFileDialog( nullptr, lastProjectPath, 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 open 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 (const 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; } } // Update the last opened project path if (lastProjectPath.Compare(Project->ProjectFolderPath) != 0) File::WriteAllText(lastProjectSettingPath, Project->ProjectFolderPath, Encoding::UTF8); return 0; } Window* Editor::CreateMainWindow() { PROFILE_MEM(Editor); 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.IsTrue()) { 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(); PROFILE_MEM(Editor); // 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 if (!CommandLine::Options.Headless.IsTrue()) { 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() { PROFILE_MEM(Editor); Managed->BeforeRun(); } void Editor::BeforeExit() { PROFILE_MEM(Editor); CloseSplashScreen(); Managed->Exit(); SAFE_DELETE(Managed); Project = nullptr; ProjectInfo::ProjectsCache.ClearDelete(); } void EditorImpl::OnUpdate() { PROFILE_MEM(Editor); // 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