diff --git a/BeefSpace.toml b/BeefSpace.toml index 27306f0..9024385 100644 --- a/BeefSpace.toml +++ b/BeefSpace.toml @@ -1,8 +1,10 @@ FileVersion = 1 Locked = ["ImGui"] +Unlocked = ["corlib"] [Workspace] StartupProject = "GlitchyEditor" +PreprocessorMacros = ["IMGUI"] [Projects] GlitchyEngine = {Path = "GlitchyEngine"} @@ -20,17 +22,24 @@ ImGuizmo = {Path = "GlitchyEngine/vendor/imgui/ImGuizmo"} GlitchyEngineHelper = {Path = "GlitchyEngineHelper"} bon = {Path = "GlitchyEngine/vendor/bon"} box2d-beef = {Path = "GlitchyEngine/vendor/box2D"} -"Beef.Linq" = {Path = "GlitchyEngine/vendor/Beef.Linq/src"} ScriptCore = {Path = "ScriptCore"} NetHostBeef = {Path = "GlitchyEngine/vendor/NetHostBeef/NetHostBeef"} "GlitchyEngine.Test" = {Path = "GlitchyEngine.Test"} Xml-Beef = {Path = "GlitchyEngine/vendor/Xml-Beef"} "GlitchyEngine.Benchmark" = {Path = "GlitchyEngine.Benchmark"} +"Beef.Linq" = {Path = "GlitchyEngine/vendor/Beef.Linq"} +"Oregano.git" = {Git = "https://github.com/zerkawei/Oregano.git"} [WorkspaceFolders] GlitchyEngine = ["GlitchyEngine", "GlitchLog", "GlitchyEngineHelper", "ScriptCore", "GlitchyEngine.Test", "GlitchyEngine.Benchmark"] -"GlitchyEngine/Dependencies" = ["cgltf-beef", "DirectX", "FreeType", "ImGui", "ImGuiImplDX11", "ImGuiImplWin32", "ImGuizmo", "LodePng", "msdfgen-beef", "bon", "box2d-beef", "Beef.Linq", "NetHostBeef", "Xml-Beef"] +"GlitchyEngine/Dependencies" = ["cgltf-beef", "DirectX", "FreeType", "ImGui", "ImGuiImplDX11", "ImGuiImplWin32", "ImGuizmo", "LodePng", "msdfgen-beef", "bon", "box2d-beef", "NetHostBeef", "Xml-Beef", "Beef.Linq"] Beef = ["Beefy2D", "corlib"] [Configs.Debug.Win64] AllocStackTraceDepth = 12 +ConfigSelections = {"GlitchyEngine.Test" = {Enabled = false}} + +[Configs.Release.Win64] +Toolset = "Microsoft" +LTOType = "None" +ConfigSelections = {"GlitchyEngine.Test" = {Enabled = false}} diff --git a/BeefSpace_Lock.toml b/BeefSpace_Lock.toml new file mode 100644 index 0000000..9475ca6 --- /dev/null +++ b/BeefSpace_Lock.toml @@ -0,0 +1,6 @@ +FileVersion = 1 + +[Locks."Oregano.git".Git] +URL = "https://github.com/zerkawei/Oregano.git" +Tag = "" +Hash = "0811b2dc6a878c472d2bc08b37ebc0862f853290" diff --git a/GlitchyEditor/BeefProj.toml b/GlitchyEditor/BeefProj.toml index 1a7a543..788d264 100644 --- a/GlitchyEditor/BeefProj.toml +++ b/GlitchyEditor/BeefProj.toml @@ -15,13 +15,14 @@ ImGuiImplDX11 = "*" ImGuiImplWin32 = "*" ImGuizmo = "*" "Beef.Linq" = "*" +"Oregano.git" = {Git = "https://github.com/zerkawei/Oregano.git"} [Platform.Windows] IconFile = "$(ProjectDir)/Resources/Textures/GlitchyEngineIcon.ico" [Configs.Debug.Win64] PostBuildCmds = ["CopyFilesIfNewer(\"$(WorkspaceDir)/bin/vswhere.exe\", \"$(TargetDir)\")"] -DebugCommandArguments = "\"$(ProjectDir)/../GlitchyEngine.Test/content/TestProject\"" +DebugCommandArguments = "\"D:\\Development\\Git\\SingleStateToOrbit\\\"" PreprocessorMacros = ["DEBUG", "IMGUI"] [Configs.Release.Win64] diff --git a/GlitchyEditor/src/Assets/AssetHierarchy.bf b/GlitchyEditor/src/Assets/AssetHierarchy.bf index 3feb059..1f483f6 100644 --- a/GlitchyEditor/src/Assets/AssetHierarchy.bf +++ b/GlitchyEditor/src/Assets/AssetHierarchy.bf @@ -33,6 +33,10 @@ class AssetHierarchy public TreeNode RootNode => _assetRootNode; + private append ScriptProjectDirtyManager _scriptProjectDirtyManager = .(); + + public ScriptProjectDirtyManager ScriptProjectDirtyManager => _scriptProjectDirtyManager; + public StringView ResourcesDirectory { get => _resourcesDirectory; @@ -235,6 +239,8 @@ class AssetHierarchy //SetupResourcesFileSystemWatcher(); UpdateFiles(); + + ScriptProjectDirtyManager.SetVeryDirty(); } /// Sets path to the directory that contains the game assets. @@ -322,18 +328,24 @@ class AssetHierarchy Log.EngineLogger.Trace($"File content changed (\"{filename}\")"); FileContentChanged(filename); + + ScriptProjectDirtyManager.AssetChanged(filename); }); fsw.OnCreated.Add(new (filename) => { Log.EngineLogger.Trace($"File created (\"{filename}\")"); DeferFileTreeUpdate(); + + ScriptProjectDirtyManager.AssetChanged(filename); }); fsw.OnDeleted.Add(new (filename) => { Log.EngineLogger.Trace($"File deleted (\"{filename}\")"); DeferFileTreeUpdate(); + + ScriptProjectDirtyManager.AssetChanged(filename); }); fsw.OnRenamed.Add(new (oldName, newName) => { @@ -342,6 +354,9 @@ class AssetHierarchy FileRenamed(oldName, newName); DeferFileTreeUpdate(); + + ScriptProjectDirtyManager.AssetChanged(oldName); + ScriptProjectDirtyManager.AssetChanged(newName); }); fsw.StartRaisingEvents(); diff --git a/GlitchyEditor/src/Assets/ScriptProjectDirtyManager.bf b/GlitchyEditor/src/Assets/ScriptProjectDirtyManager.bf new file mode 100644 index 0000000..2e00244 --- /dev/null +++ b/GlitchyEditor/src/Assets/ScriptProjectDirtyManager.bf @@ -0,0 +1,187 @@ +using System; +using System.IO; +using System.Collections; +using Oregano; +using System.Diagnostics; +using GlitchyEditor.Settings; + +namespace GlitchyEditor.Assets; + +class ScriptProjectDirtyManager +{ + private bool _anyAssetDirty; + private bool _projectFileDirty; + private bool _forceRebuild; + + public void AssetChanged(StringView assetPath) + { + _anyAssetDirty = true; + + _projectFileDirty |= CheckIfProjectFile(assetPath); + } + + private bool CheckIfProjectFile(StringView assetPath) + { + for (Regex regex in _patterns) + { + if (regex.IsMatch(assetPath)) + { + return true; + } + } + + return false; + } + + public void SetClean() + { + _anyAssetDirty = false; + _projectFileDirty = false; + _forceRebuild = false; + } + + public void SetVeryDirty() + { + _forceRebuild = true; + } + + public bool NeedsRebuild(RebuildOnPlay rebuildMode) + { + switch (rebuildMode) + { + case .Off: + return _forceRebuild; + case .Always: + return true; + case .WhenAnyAssetsChanged: + return _anyAssetDirty || _forceRebuild; + case .UseFastUpToDateCheck: + return _projectFileDirty || _forceRebuild; + } + } + + private static List patterns = new .(){ + "**/*.cs", // C# source files + "**/*.csproj", // C# Project file + "**/*.props", // MSBuild properties + "**/*.targets", // MSBuild targets + "**/*.resx", // Embedded .NET-Ressources + "**/*.editorconfig", // Compiler- and Analyzer-config + "**/*.globalconfig", // Global Analyzer-config + "**/global.json", + "**/NuGet.Config", + "**/packages.lock.json", + "**/runtimeconfig.template.json" + } ~ delete _; + + private List _patterns = new .() ~ DeleteContainerAndItems!(_); + // Oregano's Regex doesn't copy the pattern but keeps StringViews into it, so the strings must live as long as the regexes. + private List _patternStrings = new .() ~ DeleteContainerAndItems!(_); + + public this() + { + for (StringView pattern in patterns) + { + String regexString = new .(); + + if (CompileRegexFromGlob(pattern, regexString) case .Ok(let regex)) + { + _patternStrings.Add(regexString); + _patterns.Add(regex); + } + else + { + delete regexString; + } + } + } + + /// Converts the glob into a regex string (written to outRegex) and compiles it. + /// The returned Regex references outRegex, so outRegex must outlive it. + private static Result CompileRegexFromGlob(StringView glob, String outRegex) + { + outRegex.Append('^'); + + int inBrackets = 0; + bool isInBrackets() => inBrackets > 0; + + bool isWildcard = false; + bool wasDoubleWildcard = false; + + for (char8 c in glob) + { + if (isWildcard) + { + isWildcard = false; + if (c == '*') + { + // ** + outRegex.Append(@"(?:.*[\\/])?"); + wasDoubleWildcard = true; + continue; + } + else + { + outRegex.Append(@"[^\\/]*"); + } + } + + if (c == '[') + { + inBrackets++; + outRegex.Append(c); + continue; + } + else if (c == ']') + { + inBrackets--; + outRegex.Append(c); + continue; + } + + if (c == '.') + { + if (isInBrackets()) + outRegex.Append('.'); + else + outRegex.Append(@"\."); + + continue; + } + + if (isInBrackets()) + { + outRegex.Append(c); + continue; + } + else + { + if (c == '?') + { + outRegex.Append(@"[^\\/]"); + continue; + } + else if (c == '*') + { + isWildcard = true; + continue; + } + else if (c == '/') + { + if (!wasDoubleWildcard) + outRegex.Append(@"\/"); + continue; + } + else + { + outRegex.Append(c); + continue; + } + } + } + + outRegex.Append('$'); + + return Regex.Compile(outRegex); + } +} diff --git a/GlitchyEditor/src/Editor.bf b/GlitchyEditor/src/Editor.bf index acf6245..7c2626a 100644 --- a/GlitchyEditor/src/Editor.bf +++ b/GlitchyEditor/src/Editor.bf @@ -240,6 +240,8 @@ namespace GlitchyEditor task.ContinueWith(new (task) => { ScriptEngine.ResumeAssemblyWatcher(); + EditorApp.Instance.ContentManager.AssetHierarchy.ScriptProjectDirtyManager.SetClean(); + if (reloadAfterBuild) { EditorApp.Instance.InvokeOnMainThread(new () => { diff --git a/GlitchyEditor/src/EditorApp.bf b/GlitchyEditor/src/EditorApp.bf index 1d465c9..ab5d351 100644 --- a/GlitchyEditor/src/EditorApp.bf +++ b/GlitchyEditor/src/EditorApp.bf @@ -23,6 +23,8 @@ namespace GlitchyEditor public new static EditorApp Instance => Application.Instance as EditorApp; + public new EditorContentManager ContentManager => _contentManager; + private append BackgroundTaskManager _backgroundTaskManager = .(); public BackgroundTaskManager BackgroundTaskManager => _backgroundTaskManager; diff --git a/GlitchyEditor/src/EditorLayer.bf b/GlitchyEditor/src/EditorLayer.bf index 28a783b..aab7cfe 100644 --- a/GlitchyEditor/src/EditorLayer.bf +++ b/GlitchyEditor/src/EditorLayer.bf @@ -824,7 +824,7 @@ namespace GlitchyEditor { Debug.Profiler.ProfileFunction!(); - if (allowRebuild && EditorApp.Instance.Settings.ScriptSettings.RebuildOnPlay) + if (allowRebuild && _contentManager.AssetHierarchy.ScriptProjectDirtyManager.NeedsRebuild(EditorApp.Instance.Settings.ScriptSettings.RebuildOnPlayMode)) { // Don't immediately reload the scripts after the build, because SetActiveScene will do that anyway. var task = _editor.BuildScripts(reloadAfterBuild: false); diff --git a/GlitchyEditor/src/Settings/Settings.bf b/GlitchyEditor/src/Settings/Settings.bf index 717cf73..78259b4 100644 --- a/GlitchyEditor/src/Settings/Settings.bf +++ b/GlitchyEditor/src/Settings/Settings.bf @@ -53,6 +53,20 @@ struct SettingAttribute : Attribute } } +/// Fields with this Attribute will be exposed as settings. +[AttributeUsage(AttributeTargets.StaticField, .ReflectAttribute)] +struct SettingValueAttribute : Attribute +{ + public String Name; + public String Tooltip; + + public this(String name, String tooltip = "") + { + Name = name; + Tooltip = tooltip; + } +} + /// Fields with this Attribute will be exposed as settings. [AttributeUsage(.Method, .ReflectAttribute | .AlwaysIncludeTarget)] struct CustomSettingRendererAttribute : Attribute @@ -239,6 +253,24 @@ class IdeInstallation // This would however be quite complicated to implement well, when looking at how different Visual Studio and Rider already are. } +[Reflect] +enum RebuildOnPlay +{ + [SettingValue("Off", "The scripts will not be rebuild when entering play mode.")] + Off, + [SettingValue("Always", "The scripts will always be rebuild when entering play mode.")] + Always, + [SettingValue("When any Asset changed", """ + The scripts will automatically be rebuild when any asset in asset-directory changed, even if it is not necessary for the build process. + This can be helpful if you include files in your build-process that are not detected by the heuristic up to date check. + """)] + WhenAnyAssetsChanged, + [SettingValue("When the fancy dancy heuristic check says so.", """ + A highly sophisticated heuristic will determine if a rebuild is required. + """)] + UseFastUpToDateCheck +} + [Reflect] class ScriptSettings { @@ -269,10 +301,9 @@ class ScriptSettings private bool _lookedForVs = false; private bool _lookedForRider = false; - - [Setting("Tools", "Rebuild scripts on play", "If active, the scripts will be rebuilt every time the user presses play."), BonInclude] - public bool RebuildOnPlay = true; + [Setting("Tools", "Rebuild scripts on play mode", "Configures if and how the scripts will be rebuilt every time the play mode is entered."), BonInclude] + public RebuildOnPlay RebuildOnPlayMode = .UseFastUpToDateCheck; [Setting("Tools", "Automatically rebuild changed scripts", "If active, the scripts will be rebuilt every time the user presses play."), BonInclude] public bool AutoRebuildOnChange = true; diff --git a/GlitchyEditor/src/Settings/SettingsWindow.bf b/GlitchyEditor/src/Settings/SettingsWindow.bf index 8f1ef4f..d64d672 100644 --- a/GlitchyEditor/src/Settings/SettingsWindow.bf +++ b/GlitchyEditor/src/Settings/SettingsWindow.bf @@ -363,12 +363,23 @@ namespace GlitchyEditor // TODO: handle multiple Flags (as a separate enum type handler though!) StringView currentName = ""; + StringView currentTooltip = ""; for (let (name, value) in Enum.GetEnumerator(fieldInfo.FieldType)) { if (value == underlyingValue) { - currentName = name; + if (fieldInfo.FieldType.GetField(@name.Index) case .Ok(FieldInfo info) && + info.GetCustomAttribute() case .Ok(SettingValueAttribute settingValue)) + { + currentName = settingValue.Name; + currentTooltip = settingValue.Tooltip; + } + else + { + currentName = name; + } + break; } } @@ -379,7 +390,17 @@ namespace GlitchyEditor { bool isSelected = (name == currentName); - if (ImGui.Selectable(name.ToScopeCStr!(), isSelected)) + StringView valueName = name; + StringView valueTooltip = ""; + + if (fieldInfo.FieldType.GetField(@name.Index) case .Ok(FieldInfo info) && + info.GetCustomAttribute() case .Ok(SettingValueAttribute settingValue)) + { + valueName = settingValue.Name; + valueTooltip = settingValue.Tooltip; + } + + if (ImGui.Selectable(valueName.ToScopeCStr!(), isSelected)) { // Set value in field reference switch (fieldInfo.FieldType.Size) @@ -393,10 +414,20 @@ namespace GlitchyEditor if (!isSelected) _settingsChanged = true; } + + if (!valueTooltip.IsEmpty) + { + ImGui.AttachTooltip(valueTooltip); + } } ImGui.EndCombo(); } + + if (!currentTooltip.IsEmpty) + { + ImGui.AttachTooltip(currentTooltip); + } } if (setting.Name == _SettingToHighlight)