From df67f32399fc84027e59084dcaaf7cf8d0cdd27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Sun, 30 Jul 2023 13:23:34 +0200 Subject: [PATCH] Start of project system + added Directory.IsEmpty --- .../src/EditWindows/EntityHierarchyWindow.bf | 6 + GlitchyEditor/src/EditorContentManager.bf | 9 +- GlitchyEditor/src/EditorLayer.bf | 194 +++++++++++++++++- GlitchyEditor/src/Project.bf | 84 ++++++++ .../src/Extension/System/IO/Directory.bf | 13 ++ GlitchyEngine/src/Scripting/ScriptEngine.bf | 87 +++++--- 6 files changed, 356 insertions(+), 37 deletions(-) create mode 100644 GlitchyEditor/src/Project.bf create mode 100644 GlitchyEngine/src/Extension/System/IO/Directory.bf diff --git a/GlitchyEditor/src/EditWindows/EntityHierarchyWindow.bf b/GlitchyEditor/src/EditWindows/EntityHierarchyWindow.bf index be2e65a..f9a0004 100644 --- a/GlitchyEditor/src/EditWindows/EntityHierarchyWindow.bf +++ b/GlitchyEditor/src/EditWindows/EntityHierarchyWindow.bf @@ -470,6 +470,12 @@ namespace GlitchyEditor.EditWindows { StringView searchString = StringView(&_entitySearchChars); + if (_scene == null) + { + ImGui.TextUnformatted(""); + return; + } + if(searchString.IsWhiteSpace) { // Show entity hierarchy as tree diff --git a/GlitchyEditor/src/EditorContentManager.bf b/GlitchyEditor/src/EditorContentManager.bf index 8ca2fcc..b48d517 100644 --- a/GlitchyEditor/src/EditorContentManager.bf +++ b/GlitchyEditor/src/EditorContentManager.bf @@ -361,8 +361,13 @@ class EditorContentManager : IContentManager IAssetLoader assetLoader = GetAssetLoader(file); - // TODO: what are we supposed to do if we don't find a loader? Sure not crash... - Log.EngineLogger.AssertDebug(assetLoader != null); + // TODO: what are we supposed to do if we don't find a loader? Surely not crash... + //Log.EngineLogger.AssertDebug(assetLoader != null); + if (assetLoader == null) + { + Log.EngineLogger.Error($"No asset loader registered for asset {identifier}."); + return .Invalid; + } Asset loadedAsset; diff --git a/GlitchyEditor/src/EditorLayer.bf b/GlitchyEditor/src/EditorLayer.bf index 27aedc3..3da1d32 100644 --- a/GlitchyEditor/src/EditorLayer.bf +++ b/GlitchyEditor/src/EditorLayer.bf @@ -97,6 +97,12 @@ namespace GlitchyEditor } } + private Project _currentProject ~ delete _; + + public Project CurrentProject => _currentProject; + + public bool IsProjectLoaded => _currentProject != null; + [AllowAppend] public this(String[] args, EditorContentManager contentManager) : base("Editor") { @@ -115,9 +121,18 @@ namespace GlitchyEditor InitEditor(); if (args.Count >= 1) - LoadSceneFile(args[0]); + { + Result result = OpenProject(args[0]); - // Create a new scene, if LoadSceneFile failed + if (result == .Err) + Log.EngineLogger.Error($"Failed to open project \"{args[0]}\"."); + } + + /*if (args.Count >= 1) + LoadSceneFile(args[0]); + */ + + // Create a new scene if no scene is loaded if (_activeScene == null) NewScene(); } @@ -188,14 +203,25 @@ namespace GlitchyEditor _editor.EditorSceneRenderer = _editorSceneRenderer; _editor.RequestOpenScene.Add(new (s, fileName) => { - LoadSceneFile(fileName); + String fullName = scope .(); + Path.Combine(fullName, _currentProject.AssetsFolder, fileName); + + LoadSceneFile(fullName); }); } - + + private bool _showCreateNewProject = false; + public override void Update(GameTime gameTime) { Debug.Profiler.ProfileFunction!(); + if (IsProjectLoaded) + UpdateScene(gameTime); + } + + private void UpdateScene(GameTime gameTime) + { _editor.CurrentScene = _activeScene; Scene.UpdateMode updateMode; @@ -244,7 +270,7 @@ namespace GlitchyEditor // Clear the swapchain-buffer RenderCommand.Clear(null, .Color | .Depth, .(0.2f, 0.2f, 0.2f), 1.0f, 0); - + RenderCommand.SetBlendState(_alphaBlendState); RenderCommand.SetDepthStencilState(_depthStencilState); @@ -261,7 +287,7 @@ namespace GlitchyEditor RenderCommand.SetBlendState(_alphaBlendState); RenderCommand.SetDepthStencilState(_depthStencilState); - + if (_editor.SceneViewportWindow.Visible) { _camera.Update(gameTime); @@ -383,6 +409,7 @@ namespace GlitchyEditor _editor.Update(); _settingsWindow.Show(); + ShowCreateNewProjectModal(); UI_Toolbar(); @@ -619,6 +646,154 @@ namespace GlitchyEditor } } + private Result CreateNewProject(StringView directory, StringView projectName) + { + var createDirectoryResult = Directory.CreateDirectory(directory); + + if (createDirectoryResult case .Err(let error)) + { + Log.ClientLogger.Error($"Failed to create directory \"{directory}\"."); + return .Err; + } + + if (!Directory.IsEmpty(directory)) + { + Log.ClientLogger.Error($"Cannot create project in non-empty directory."); + return .Err; + } + + Project newProject = Project.CreateNew(directory, projectName); + + var createAssetDirResult = Directory.CreateDirectory(newProject.AssetsFolder); + + if (createAssetDirResult case .Err(let error)) + { + Log.ClientLogger.Error($"Failed to create asset directory \"{newProject.AssetsFolder}\"."); + return .Err; + } + + // Copy .gitignore + String gitignoreTarget = scope .(); + newProject.PathInProject(gitignoreTarget, ".gitignore"); + var copyGitignore = File.Copy("resources/gitignore.txt", gitignoreTarget); + + if (copyGitignore case .Err(let error)) + { + Log.ClientLogger.Error($"Failed to copy .gitignore (\"{error}\")."); + } + + // TODO: VS Project + + return .Ok; + } + + private bool _openCreateProjectModal; + + private void ShowCreateNewProjectModal() + { + static char8[128] projectNameBuffer = .(); + static char8[256] projectDirectoryBuffer = .(); + + if (_openCreateProjectModal) + { + ImGui.OpenPopup("Create new Project"); + _openCreateProjectModal = false; + + projectNameBuffer = .(); + } + + // Always center this window when appearing + var center = ImGui.GetMainViewport().GetCenter(); + ImGui.SetNextWindowPos(center, .Appearing, .(0.5f, 0.5f)); + + if (ImGui.BeginPopupModal("Create new Project", null, .AlwaysAutoResize)) + { + ImGui.TextUnformatted("Project Name:"); + ImGui.InputText("##projectName", &projectNameBuffer, projectNameBuffer.Count - 1); + + ImGui.NewLine(); + + ImGui.TextUnformatted("Directory:"); + ImGui.InputText("##directory", &projectDirectoryBuffer, projectDirectoryBuffer.Count - 1); + ImGui.SameLine(); + + if (ImGui.Button("...")) + { + FolderBrowserDialog folderDialog = scope FolderBrowserDialog(); + Result result = folderDialog.ShowDialog(); + + if (result case .Ok(let dialogResult) && dialogResult case .OK) + { + folderDialog.SelectedPath.CopyTo(projectDirectoryBuffer); + } + } + + StringView projectName = StringView(&projectNameBuffer); + StringView directory = StringView(&projectDirectoryBuffer); + + String target = scope String(); + Path.Combine(target, directory, projectName); + + ImGui.NewLine(); + + ImGui.Text($"The Project will be in:\n{target}"); + + ImGui.NewLine(); + + ImGui.BeginDisabled(directory.IsWhiteSpace || projectName.IsWhiteSpace); + + if (ImGui.Button("Create")) + { + Result result = CreateNewProject(target, projectName); + + if (result case .Ok) + ImGui.CloseCurrentPopup(); + } + + ImGui.EndDisabled(); + + ImGui.SameLine(); + + if (ImGui.Button("Cancel")) + { + ImGui.CloseCurrentPopup(); + } + + ImGui.EndPopup(); + } + } + + private void CloseCurrentProject() + { + OnSceneStop(); + + SetReference!(_editorScene, null); + SetReference!(_activeScene, null); + _editor.CurrentScene = null; + + // TODO: Do actual work here! + delete _currentProject; + _currentProject = null; + } + + private Result OpenProject(StringView workspacePath) + { + CloseCurrentProject(); + + _currentProject = Project.Load(workspacePath); + + String appAssemblyPath = scope String(); + // TODO: obviously change dll name, configurable? + Path.Combine(appAssemblyPath, _currentProject.AssetsFolder, "Scripts/bin/Sandbox.dll"); + + ScriptEngine.SetAppAssemblyPath(appAssemblyPath); + + // TODO: Load last opened scene + NewScene(); + + return .Ok; + } + /// Creates a new scene. private void NewScene() { @@ -753,6 +928,9 @@ namespace GlitchyEditor if(ImGui.BeginMenu("File", true)) { + if (ImGui.MenuItem("Create new Project...", "Ctrl+N")) + _openCreateProjectModal = true; + if (ImGui.MenuItem("New", "Ctrl+N")) NewScene(); @@ -844,7 +1022,7 @@ namespace GlitchyEditor _camera.OnViewportResize(sizeX, sizeY); - _activeScene.SetViewportSize(sizeX, sizeY); + _activeScene?.SetViewportSize(sizeX, sizeY); } private void GameViewportSizeChanged(Object sender, float2 viewportSize) @@ -857,7 +1035,7 @@ namespace GlitchyEditor _gameViewportTarget.Resize(sizeX, sizeY); - _activeScene.SetViewportSize(sizeX, sizeY); + _activeScene?.SetViewportSize(sizeX, sizeY); } private bool OnKeyPressed(KeyPressedEvent e) diff --git a/GlitchyEditor/src/Project.bf b/GlitchyEditor/src/Project.bf new file mode 100644 index 0000000..fe9e67c --- /dev/null +++ b/GlitchyEditor/src/Project.bf @@ -0,0 +1,84 @@ +using System; +using System.IO; +using Bon; +using GlitchyEngine; + +namespace GlitchyEditor; + +[BonTarget] +class Project +{ + [BonInclude] + private String _projectName ~ delete _; + + [BonIgnore] + private String _workspacePath ~ delete _; + + [BonIgnore] + private String _assetsFolder ~ delete _; + + [BonIgnore] + private String _scriptFolder ~ delete _; + + public StringView ProjectName => _projectName; + public StringView WorkspacePath => _workspacePath; + + public StringView AssetsFolder => _assetsFolder; + + [AllowAppend] + private this(StringView workspacePath) + { + _workspacePath = new String(workspacePath); + + _assetsFolder = new String(); + PathInProject(_assetsFolder, "Assets"); + } + + public void PathInProject(String target, StringView relativePath) + { + Path.Combine(target, WorkspacePath, relativePath); + } + + public void GetRelativePath() + { + + } + + public static Project CreateNew(StringView projectDirectory, StringView projectName) + { + Project project = new Project(projectDirectory); + project._projectName = new String(projectName); + + String settingsFile = scope .(); + project.PathInProject(settingsFile, "project.gep"); + + Result result = Bon.SerializeIntoFile(project, settingsFile); + + if (result case .Err) + Log.EngineLogger.Warning($"Failed to save project file \"{settingsFile}\"."); + + return project; + } + + public static Result Load(StringView projectDirectory) + { + Project project = new Project(projectDirectory); + + String settingsFile = scope .(); + project.PathInProject(settingsFile, "project.gep"); + + if (File.Exists(settingsFile)) + { + Result result = Bon.DeserializeFromFile(ref project, settingsFile); + + if (result case .Err) + Log.EngineLogger.Warning($"Failed to deserialize project file \"{settingsFile}\"."); + } + else + { + Log.EngineLogger.Warning($"Project file \"{settingsFile}\" doesn't exist."); + } + + return project; + } +} \ No newline at end of file diff --git a/GlitchyEngine/src/Extension/System/IO/Directory.bf b/GlitchyEngine/src/Extension/System/IO/Directory.bf new file mode 100644 index 0000000..2f712ca --- /dev/null +++ b/GlitchyEngine/src/Extension/System/IO/Directory.bf @@ -0,0 +1,13 @@ +namespace System.IO; + +extension Directory +{ + [Import("Shlwapi.lib"), CLink, CallingConvention(.Stdcall)] + private static extern System.Windows.IntBool PathIsDirectoryEmptyW(char16* pszPath); + + public static bool IsEmpty(StringView fileName) + { + // TODO: This is obviously windows only + return PathIsDirectoryEmptyW(fileName.ToScopedNativeWChar!()); + } +} \ No newline at end of file diff --git a/GlitchyEngine/src/Scripting/ScriptEngine.bf b/GlitchyEngine/src/Scripting/ScriptEngine.bf index 22c545a..4c15a52 100644 --- a/GlitchyEngine/src/Scripting/ScriptEngine.bf +++ b/GlitchyEngine/src/Scripting/ScriptEngine.bf @@ -95,6 +95,8 @@ static class ScriptEngine // TODO: This should be a global setting somewhere private static bool _debuggingEnabled = true; + private static String _appAssemblyPath = new .() ~ delete _; + internal static class Attributes { internal static MonoClass* s_ShowInEditorAttribute; @@ -106,8 +108,14 @@ static class ScriptEngine ScriptGlue.Init(); LoadScriptAssemblies(); + } - InitAssemblyWatcher(); + /// Changes the path to the current app assembly. + public static void SetAppAssemblyPath(StringView appAssemblyPath) + { + _appAssemblyPath.Set(appAssemblyPath); + + ReloadAssemblies(); } static void InitMono() @@ -138,33 +146,46 @@ static class ScriptEngine static void InitAssemblyWatcher() { - if (_userAssemblyWatcher == null) + // We don't need a file system watcher, if we have nothing to watch... + if (!File.Exists(_appAssemblyPath)) + return; + + String directory = scope .(); + Path.GetDirectoryPath(_appAssemblyPath, directory); + + if (_userAssemblyWatcher != null && _userAssemblyWatcher.Directory != directory) { - // TODO: Obviously don't hardcode path - _userAssemblyWatcher = new FileSystemWatcher("SandboxProject/Assets/Scripts/bin/", "*/Sandbox.dll"); - _userAssemblyWatcher.OnChanged.Add(new (fileName) => - { - // TODO: Temporary, we want to be able to reload while in play-mode. (+ Editor Scripts will be a thing some day) - if (_entityScriptInstances.Count > 0) - { - Log.EngineLogger.Warning("There are script instances. Skipping assembly reload."); - return; - } - - Log.EngineLogger.Info("Script reload requested."); - _userAssemblyWatcher.StopRaisingEvents(); - - Application.Instance.InvokeOnMainThread(new () => - { - Log.EngineLogger.Info("Reloading scripts..."); - ReloadAssemblies(); - Log.EngineLogger.Info("Scripts reloaded!"); - - _userAssemblyWatcher.StartRaisingEvents(); - }); - - }); + delete _userAssemblyWatcher; } + + String fileName = scope .("*/"); + Path.GetFileName(_appAssemblyPath, fileName); + + // TODO: Obviously don't hardcode path + _userAssemblyWatcher = new FileSystemWatcher(directory, fileName); + _userAssemblyWatcher.OnChanged.Add(new (fileName) => + { + // TODO: Temporary, we want to be able to reload while in play-mode. (+ Editor Scripts will be a thing some day) + if (_entityScriptInstances.Count > 0) + { + Log.EngineLogger.Warning("There are script instances. Skipping assembly reload."); + return; + } + + Log.EngineLogger.Info("Script reload requested."); + _userAssemblyWatcher.StopRaisingEvents(); + + Application.Instance.InvokeOnMainThread(new () => + { + Log.EngineLogger.Info("Reloading scripts..."); + ReloadAssemblies(); + Log.EngineLogger.Info("Scripts reloaded!"); + + _userAssemblyWatcher.StartRaisingEvents(); + }); + + }); + _userAssemblyWatcher.StartRaisingEvents(); } @@ -172,7 +193,14 @@ static class ScriptEngine { CreateAppDomain("GlitchyEngineScriptRuntime"); (s_CoreAssembly, s_CoreAssemblyImage) = LoadAssembly("resources/scripts/ScriptCore.dll", _debuggingEnabled); - (s_AppAssembly, s_AppAssemblyImage) = LoadAssembly("SandboxProject/Assets/Scripts/bin/Sandbox.dll", _debuggingEnabled); + + if (File.Exists(_appAssemblyPath)) + (s_AppAssembly, s_AppAssemblyImage) = LoadAssembly(_appAssemblyPath, _debuggingEnabled); + else + { + s_AppAssembly = null; + s_AppAssemblyImage = null; + } ClearDictionaryAndReleaseValues!(_sharpClasses); @@ -184,6 +212,8 @@ static class ScriptEngine GetEntitiesFromAssemblies(); ScriptGlue.RegisterManagedComponents(); + + InitAssemblyWatcher(); } public static void SetContext(Scene scene) @@ -355,6 +385,9 @@ static class ScriptEngine { ClearDictionaryAndReleaseValues!(_entityScripts); + if (s_AppAssemblyImage == null) + return; + MonoTableInfo* typeDefinitionsTable = Mono.mono_image_get_table_info(s_AppAssemblyImage, .MONO_TABLE_TYPEDEF); int32 numTypes = Mono.mono_table_info_get_rows(typeDefinitionsTable);