diff --git a/BeefSpace.toml b/BeefSpace.toml index ae0a15c..1d9711d 100644 --- a/BeefSpace.toml +++ b/BeefSpace.toml @@ -4,3 +4,6 @@ WorkspaceFolders = {GlitchyEngine = ["GlitchyEngine", "GlitchLog", "GlitchyEngin [Workspace] StartupProject = "GlitchyEditor" + +[Configs.Debug.Win64] +AllocStackTraceDepth = 12 diff --git a/GlitchyEditor/content/Textures/TestMaterial.mat b/GlitchyEditor/content/Textures/TestMaterial.mat index 92d0d96..f877d23 100644 --- a/GlitchyEditor/content/Textures/TestMaterial.mat +++ b/GlitchyEditor/content/Textures/TestMaterial.mat @@ -1,7 +1,28 @@ { - Effect = "content/Shaders/myEffect.hlsl", - Textures = - [ - "AlbedoTexture": "Textures/TestMat/rustediron2_albedo.png" - ] + Effect = "content/Shaders/myEffect.hlsl", + Textures = [ + "AlbedoTexture": "Textures/TestMat/rustediron2_albedo.png" + ], + Variables = [ + "AlbedoColor": .ColorRGBA{ + Value = { + R = 1, + G = 1, + B = 1, + A = 1 + } + }, + "NormalScaling": .Float2{ + Value = { + X = 1, + Y = 1 + } + }, + "MetallicFactor": .Float{ + Value = 1 + }, + "RoughnessFactor": .Float{ + Value = 1 + } + ] } \ No newline at end of file diff --git a/GlitchyEditor/src/Assets/AssetHierarchy.bf b/GlitchyEditor/src/Assets/AssetHierarchy.bf new file mode 100644 index 0000000..a49c616 --- /dev/null +++ b/GlitchyEditor/src/Assets/AssetHierarchy.bf @@ -0,0 +1,343 @@ +using GlitchyEngine; +using GlitchyEngine.Collections; +using GlitchyEngine.Renderer; +using System; +using System.Collections; +using System.IO; +using System.Linq; + +namespace GlitchyEditor.Assets; + +public class AssetNode +{ + public String Name ~ delete _; + public String Path ~ delete _; + + public bool IsDirectory; + + public AssetFile AssetFile ~ delete _; + + public List SubAssets ~ { + SubAssets?.ClearAndDeleteItems(); + delete SubAssets; + } + + public Texture2D PreviewImage ~ _?.ReleaseRef(); +} + +public class SubAsset +{ + public AssetNode Asset; + public String Name ~ delete _; + //public String AssetInternalPath ~ delete _; + + public Texture2D PreviewImage ~ _?.ReleaseRef(); +} + +class AssetHierarchy +{ + FileSystemWatcher fsw ~ { + _.StopRaisingEvents(); + delete _; + }; + + bool _fileSystemDirty = false; + + internal TreeNode _assetHierarchy = null ~ DeleteTreeAndChildren!(_); + private append Dictionary> _pathToAssetNode = .(); + + private append String _contentDirectory = .(); + + private EditorContentManager _contentManager; + + public StringView ContentDirectory + { + get => _contentDirectory; + private set + { + _contentDirectory.Clear(); + _contentDirectory.Append(value); + Path.Fixup(_contentDirectory); + } + } + + public this(EditorContentManager contentManager) + { + _contentManager = contentManager; + } + + public void SetContentDirectory(StringView contentDirectory) + { + ContentDirectory = contentDirectory; + + _fileSystemDirty = true; + + SetupFileSystemWatcher(); + + Update(); + } + + /// Initializes the FSW for the current ContentDirectory and registers the events. + private void SetupFileSystemWatcher() + { + delete fsw; + fsw = new FileSystemWatcher(_contentDirectory); + fsw.IncludeSubdirectories = true; + + fsw.OnChanged.Add(new (filename) => { + // Note: Gets fired for a directory if a file inside it is created/removed + + Log.EngineLogger.Trace($"File content changed (\"{filename}\")"); + //_fileSystemDirty = true; + + FileContentChanged(filename); + }); + + fsw.OnCreated.Add(new (filename) => { + Log.EngineLogger.Trace($"File created (\"{filename}\")"); + + _fileSystemDirty = true; + }); + + fsw.OnDeleted.Add(new (filename) => { + Log.EngineLogger.Trace($"File deleted (\"{filename}\")"); + + _fileSystemDirty = true; + }); + + fsw.OnRenamed.Add(new (oldName, newName) => { + Log.EngineLogger.Trace($"File renamed (From \"{oldName}\" to \"{newName}\")"); + + _fileSystemDirty = true; + /*String contentFilePath = scope String(); + + Path.InternalCombine(contentFilePath, ContentDirectory, oldName); + + //_fileSystemDirty = true; + TreeNode fileNode = GetNodeFromPath(contentFilePath); + fileNode->*/ + }); + + fsw.StartRaisingEvents(); + } + + /// Gets the tree node for the given filePath or null, if the file/directory doesn't exist. + /// @param filePath the path for which to return the tree node. + /// @remarks Do not hold a reference to the TreeNode because it can become invalid when the file hierarchy changes. + public Result> GetNodeFromPath(StringView filePath) + { + if (_pathToAssetNode.TryGetValue(filePath, let treeNode)) + { + return treeNode; + } + + return .Err; + } + + public bool FileExists(StringView filePath) + { + return _pathToAssetNode.ContainsKey(filePath); + } + + public void Update() + { + // TODO: do we really need to do this in the update loop? + if (_fileSystemDirty) + { + UpdateFiles(); + } + } + + + /// Rebuilds the asset file hierarchy. + private void UpdateFiles() + { + Log.EngineLogger.Trace($"Updating asset hierarchy"); + + if (_assetHierarchy == null) + { + // TODO: move to init? + + _assetHierarchy = new TreeNode(new AssetNode()); + _assetHierarchy->Path = new String(ContentDirectory); + _assetHierarchy->Name = new String("Content"); + + Log.EngineLogger.Trace($"Created directory node for: \"{_assetHierarchy->Path}\""); + + _pathToAssetNode.Add(ContentDirectory, _assetHierarchy); + } + + void HandleFile(AssetNode node) + { + String identifier = scope .(node.Path.Length); + Path.GetRelativePath(node.Path, _contentDirectory, identifier); + + node.AssetFile = new AssetFile(_contentManager, identifier, node.Path, node.IsDirectory); + } + + /// Determines the files that belong to the given directory and adds them to the tree. + void AddFilesOfDirectory(TreeNode directory) + { + // Filter that accepts all files. + String filter = scope $"{directory->Path}/*"; + + // Buffer used to hold the path of the files iterated below. + String filepathBuffer = scope String(256); + // Buffer used to hold the file extension of the files iterated below. + String extensionBuffer = scope String(16); + + for (var entry in Directory.Enumerate(filter, .Files)) + { + entry.GetFilePath(filepathBuffer..Clear()); + + Path.GetExtension(filepathBuffer, .. extensionBuffer..Clear()); + + // Ignore meta files. + if (extensionBuffer.Equals(AssetFile.ConfigFileExtension, .OrdinalIgnoreCase)) + continue; + + TreeNode treeNode = directory.Children.Where(scope (node) => node.Value.Path == filepathBuffer).FirstOrDefault(); + + if (treeNode == null) + { + AssetNode assetNode = new AssetNode(); + assetNode.Name = new String(); + Path.GetFileName(filepathBuffer, assetNode.Name); + + filepathBuffer.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + + assetNode.Path = new String(filepathBuffer); + assetNode.IsDirectory = false; + + treeNode = directory.AddChild(assetNode); + _pathToAssetNode.Add(assetNode.Path, treeNode); + + //GrabSubAssets(node); + HandleFile(treeNode.Value); + + Log.EngineLogger.Trace($"Created file node for: \"{assetNode.Path}\""); + } + } + } + + void RemoveOrphanedEntries(TreeNode node) + { + /// Removes the node and its children from _pathToAssetNode + void RemoveSubtree(TreeNode tree) + { + _pathToAssetNode.Remove(tree->Path); + + for (var child in tree.Children) + { + RemoveSubtree(child); + } + } + + for (TreeNode child in node.Children) + { + if (!Directory.Exists(child->Path) && !File.Exists(child->Path)) + { + Log.EngineLogger.Trace($"Removed orphaned node for: \"{child->Path}\""); + + @child.Remove(); + + RemoveSubtree(child); + + DeleteTreeAndChildren!(child); + } + } + } + + /// Adds the given directory to the specified tree. + /// Recursively adds all Files and Subdirectories. + void AddDirectoryToTree(String path, TreeNode parentNode) + { + path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + + // Try to find the node for the specified path in the given parent + TreeNode treeNode = parentNode.Children.Where(scope (node) => node.Value.Path == path).FirstOrDefault(); + + // Create new Node for the Directory, if no TreeNode exists. + if (treeNode == null) + { + AssetNode assetNode = new AssetNode(); + assetNode.Path = new String(path); + assetNode.Name = new String(); + assetNode.IsDirectory = true; + Path.GetFileName(assetNode.Path, assetNode.Name); + + treeNode = parentNode.AddChild(assetNode); + _pathToAssetNode.Add(assetNode.Path, treeNode); + + Log.EngineLogger.Trace($"Created directory node for: \"{assetNode.Path}\""); + } + + String directoryNameBuffer = scope String(256); + + // Filter that finds all entries of a directory. + String filter = scope $"{path}/*"; + + for (var directory in Directory.Enumerate(filter, .Directories)) + { + directory.GetFilePath(directoryNameBuffer..Clear()); + + AddDirectoryToTree(directoryNameBuffer, treeNode); + } + + AddFilesOfDirectory(treeNode); + + RemoveOrphanedEntries(treeNode); + } + + String filter = scope $"{ContentDirectory}/*"; + + String directoryNameBuffer = scope String(256); + + for (var directory in Directory.Enumerate(filter, .Directories)) + { + directory.GetFilePath(directoryNameBuffer..Clear()); + + AddDirectoryToTree(directoryNameBuffer, _assetHierarchy); + } + + RemoveOrphanedEntries(_assetHierarchy); + + _fileSystemDirty = false; + } + + private void FileContentChanged(StringView fileName) + { + var fileName; + + // Config files aren't really tracked but changing them effectively changes the corresponding file + // so we fire the event for them. + if (fileName.EndsWith(AssetFile.ConfigFileExtension)) + fileName.RemoveFromEnd(AssetFile.ConfigFileExtension.Length); + + String fileNameWithContentRoot = scope .(); + Path.InternalCombine(fileNameWithContentRoot, _contentDirectory, fileName); + + var nodeResult = GetNodeFromPath(fileNameWithContentRoot); + + TreeNode node = null; + + if (!(nodeResult case .Ok(out node))) + { + // This happens, when we create new files. + Log.EngineLogger.Trace($"Could not find node for file \"{fileNameWithContentRoot}\""); + return; + } + + // Don't fire event for directories. + if (node->IsDirectory) + return; + + OnFileContentChanged(node.Value); + + // TODO: Handle file changes (reload asset, etc...) + } + + public delegate void FileContentChangedFunc(AssetNode node); + + public Event OnFileContentChanged ~ _.Dispose(); +} diff --git a/GlitchyEditor/src/Assets/IAssetSaver.bf b/GlitchyEditor/src/Assets/IAssetSaver.bf new file mode 100644 index 0000000..b873531 --- /dev/null +++ b/GlitchyEditor/src/Assets/IAssetSaver.bf @@ -0,0 +1,10 @@ +using GlitchyEngine.Content; +using System; +using System.IO; + +namespace GlitchyEditor.Assets; + +interface IAssetSaver +{ + Result EditorSaveAsset(Stream file, Asset asset, AssetLoaderConfig config, StringView assetIdentifier, StringView? subAsset, IContentManager contentManager); +} \ No newline at end of file diff --git a/GlitchyEditor/src/Assets/MaterialAssetLoader.bf b/GlitchyEditor/src/Assets/MaterialAssetLoader.bf index 7017ccb..c2b6c11 100644 --- a/GlitchyEditor/src/Assets/MaterialAssetLoader.bf +++ b/GlitchyEditor/src/Assets/MaterialAssetLoader.bf @@ -7,11 +7,26 @@ using GlitchyEngine; using GlitchyEngine.Renderer; using ImGui; using GlitchyEngine.Math; +using System.Diagnostics; +using Bon.Integrated; namespace GlitchyEditor.Assets; class MaterialAssetPropertiesEditor : AssetPropertiesEditor { + public static bool TryGetValue(Dictionary parameters, String name, out Variant value) + { + if (parameters.TryGetValue(name, let param)) + { + value = param; + return true; + } + + value = ?; + + return false; + } + mixin DropAssetTarget() where T : Asset { Asset asset = null; @@ -83,18 +98,6 @@ class MaterialAssetPropertiesEditor : AssetPropertiesEditor private void ShowVariables(Material material, Effect effect) { - bool TryGetValue(Dictionary parameters, String name, out Variant value) - { - if (parameters.TryGetValue(name, let param)) - { - value = param; - return true; - } - - value = ?; - - return false; - } for (let (name, arguments) in effect.[Friend]_variableDescriptions) { @@ -194,16 +197,123 @@ class MaterialAssetLoaderConfig : AssetLoaderConfig } +[BonTarget] +public enum VariableValue +{ + case Float(float Value); + case Float2(Vector2 Value); + case Float3(Vector3 Value); + case Float4(Vector4 Value); + case Int(int Value); + case Int2(Int2 Value); + case Int3(Int3 Value); + case Int4(Int4 Value); + case ColorRGB(ColorRGB Value); + case ColorRGBA(ColorRGBA Value); + case None; + + /*static this() + { + gBonEnv.typeHandlers.Add(typeof(Self), + ((.)new => VariableValueSerialize, (.)new => VariableValueDeserialize)); + } + + static void VariableValueSerialize(BonWriter writer, ValueView value, BonEnvironment env) + { + Log.EngineLogger.Assert(value.type == typeof(Self)); + + let variableValue = value.Get(); + + writer.Type(variableValue) + using (writer.ObjectBlock()) + { + Serialize.Value(writer, nameof(MaterialFile.Effect), materialFile.Effect, env); + Serialize.Value(writer, nameof(MaterialFile.Textures), materialFile.Textures, env); + + + } + } + + static Result VariableValueDeserialize(BonReader reader, ValueView val, BonEnvironment env) + { + return .Ok; + }*/ +} + [BonTarget] class MaterialFile { public String Effect ~ delete _; public Dictionary Textures ~ DeleteDictionaryAndKeysAndValues!(_); - //public Dictionary Variables; + public Dictionary Variables ~ + { + if (_ != null) + { + for (var entry in _) + { + delete entry.key; + //delete entry.value; + /*if (entry.value.HasValue) + entry.value->Dispose();*/ + } + + delete _; + } + }; + + /*static this() + { + gBonEnv.typeHandlers.Add(typeof(Self), + ((.)new => MaterialSerialize, (.)new => MaterialDeserialize)); + } + + static void MaterialSerialize(BonWriter writer, ValueView value, BonEnvironment env) + { + Log.EngineLogger.Assert(value.type == typeof(Self)); + + let materialFile = value.Get(); + + using (writer.ObjectBlock()) + { + Serialize.Value(writer, nameof(MaterialFile.Effect), materialFile.Effect, env); + Serialize.Value(writer, nameof(MaterialFile.Textures), materialFile.Textures, env); + + + } + } + + private static void SerializeVariablesDictionary(BonWriter writer, MaterialFile materialFile, BonEnvironment env) + { + using (writer.ArrayBlock()) + { + for (let (name, value) in materialFile.Variables) + { + let keyVal = ValueView(typeof(String), name); + Serialize.Value(writer, keyVal, env); + writer.Pair(); + + ValueView valueVal;// = ValueView(, entriesPtr + (currentIndex * entryStride) + entryValueOffset); + switch(value.GetType()) + { + case typeof(ColorRGBA): + writer.Identifier("ColorRGBA"); + default: + + } + + Serialize.Value(writer, valueVal, env); + } + } + } + + static Result MaterialDeserialize(BonReader reader, ValueView val, BonEnvironment env) + { + return .Ok; + }*/ } -class MaterialAssetLoader : IAssetLoader //, IReloadingAssetLoader +class MaterialAssetLoader : IAssetLoader, IAssetSaver //, IReloadingAssetLoader { private static readonly List _fileExtensions = new .(){".mat"} ~ delete _; @@ -225,10 +335,11 @@ class MaterialAssetLoader : IAssetLoader //, IReloadingAssetLoader MaterialFile materialFile = scope .(); var result = Bon.Deserialize(ref materialFile, text); - + if (result case .Err) { Log.EngineLogger.Error("Failed to load material."); + Debug.SafeBreak(); return null; // TODO: return error material } @@ -243,7 +354,7 @@ class MaterialAssetLoader : IAssetLoader //, IReloadingAssetLoader { if (texture == null) { - Log.EngineLogger.Error("Failed to load texture."); + Log.EngineLogger.Error($"Failed to load texture \"{textureIdentifier}\"."); // TODO: LoadAsset should return an error texture. } @@ -253,17 +364,167 @@ class MaterialAssetLoader : IAssetLoader //, IReloadingAssetLoader fx.ReleaseRef(); - /*for (let (slotName, textureIdentifier) in materialFile.Variables) + for (let (slotName, variableValue) in materialFile.Variables) { - if (texture == null) + switch (variableValue) { - Log.EngineLogger.Error("Failed to load texture."); - // TODO: LoadAsset should return an error texture. + case .ColorRGBA(let value): + material.SetVariable(slotName, value); + case .ColorRGB(let value): + material.SetVariable(slotName, value); + case .Float(let value): + material.SetVariable(slotName, value); + case .Float2(let value): + material.SetVariable(slotName, value); + case .Float3(let value): + material.SetVariable(slotName, value); + case .Float4(let value): + material.SetVariable(slotName, value); + case .None: + default: + Log.EngineLogger.Error("Errorre"); } - material.SetVariable(slotName, ); - }*/ + + } - return material; //ModelLoader.LoadMesh(file, subAsset.Value, 0); + return material; + } + + public Result EditorSaveAsset(Stream file, Asset asset, AssetLoaderConfig config, StringView assetIdentifier, StringView? subAsset, IContentManager contentManager) + { + Material material = asset as Material; + + if (material == null) + { + Log.EngineLogger.Error("Asset must be a Material!"); + return .Err; + } + + MaterialFile materialFile = scope .(); + + materialFile.Effect = new String(material.Effect.Identifier); + materialFile.Textures = new .(); + materialFile.Variables = new .(); + + //material.SetTexture(); + + for (let (slotName, textureViewBinding) in material.[Friend]_textures) + { + //materialFile.Textures.Add(slotName, textureViewBinding.) + + } + + Effect effect = material.Effect; + + if (effect == null) + return .Ok; + + for (let (name, arguments) in effect.[Friend]_variableDescriptions) + { + VariableValue variableValue = .None; + //Object variantValue = null; + + let variable = effect.Variables[name]; + + bool hasPreviewType = MaterialAssetPropertiesEditor.TryGetValue(arguments, "Type", var previewType); + + if (hasPreviewType && previewType.Get() == "Color") + { + Log.EngineLogger.AssertDebug(variable.Type == .Float && variable.Rows == 1); + + if (variable.Columns == 3) + { + material.GetVariable(variable.Name, var value); + + value = ColorRGB.LinearToSRGB((ColorRGB)value); + + //variantValue = new box value; + variableValue = .ColorRGB(value); + } + else if (variable.Columns == 4) + { + material.GetVariable(variable.Name, var value); + + value = ColorRGBA.LinearToSRGB((ColorRGBA)value); + + variableValue = .ColorRGBA(value); + //variantValue = new box value; + } + } + else if (variable.Type == .Float && variable.Rows == 1) + { + switch (variable.Columns) + { + case 1: + material.GetVariable(variable.Name, let value); + variableValue = .Float(value); + case 2: + material.GetVariable(variable.Name, let value); + variableValue = .Float2(value); + case 3: + material.GetVariable(variable.Name, let value); + variableValue = .Float3(value); + case 4: + material.GetVariable(variable.Name, let value); + variableValue = .Float4(value); + } + + /*bool hasMin = TryGetValue(arguments, "Min", var min); + bool hasMax = TryGetValue(arguments, "Max", var max); + + for (int r < variable.Rows) + { + switch (variable.Columns) + { + case 1: + material.GetVariable(variable.Name, var value); + + float[1] minV = hasMin ? min.Get() : .(float.MinValue); + float[1] maxV = hasMax ? max.Get() : .(float.MaxValue); + + if (ImGui.EditVector<1>(displayName, ref *(float[1]*)&value, .(), 0.1f, 100.0f, minV, maxV)) + material.SetVariable(variable.Name, value); + case 2: + material.GetVariable(variable.Name, var value); + + Vector2 minV = hasMin ? min.Get() : .(float.MinValue); + Vector2 maxV = hasMax ? max.Get() : .(float.MaxValue); + + if (ImGui.EditVector2(displayName, ref value, .Zero, 0.1f, 100.0f, minV, maxV)) + material.SetVariable(variable.Name, value); + case 3: + material.GetVariable(variable.Name, var value); + + Vector3 minV = hasMin ? min.Get() : .(float.MinValue); + Vector3 maxV = hasMax ? max.Get() : .(float.MaxValue); + + if (ImGui.EditVector3(displayName, ref value, .Zero, 0.1f, 100.0f, minV, maxV)) + material.SetVariable(variable.Name, value); + case 4: + material.GetVariable(variable.Name, var value); + + Vector4 minV = hasMin ? min.Get() : .(float.MinValue); + Vector4 maxV = hasMax ? max.Get() : .(float.MaxValue); + + if (ImGui.EditVector4(displayName, ref value, .Zero, 0.1f, 100.0f, minV, maxV)) + material.SetVariable(variable.Name, value); + } + }*/ + } + + materialFile.Variables.Add(name, variableValue); + } + + String text = scope .(); + + gBonEnv.serializeFlags |= .IncludeDefault | .Verbose; + + Bon.Serialize(materialFile, text); + + StreamWriter writer = scope .(file, .UTF8, 1024); + writer.Write(text); + + return .Ok; } } \ No newline at end of file diff --git a/GlitchyEditor/src/Assets/SerializeExtension.bf b/GlitchyEditor/src/Assets/SerializeExtension.bf new file mode 100644 index 0000000..8e4323f --- /dev/null +++ b/GlitchyEditor/src/Assets/SerializeExtension.bf @@ -0,0 +1,6 @@ +using System; +namespace Bon.Integrated; + +extension Serialize +{ +} \ No newline at end of file diff --git a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf index 1df6c6a..43a69ef 100644 --- a/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf +++ b/GlitchyEditor/src/EditWindows/ContentBrowserWindow.bf @@ -6,6 +6,7 @@ using System.Collections; using GlitchyEngine.Renderer; using GlitchyEngine.Math; using GlitchyEngine; +using GlitchyEditor.Assets; namespace GlitchyEditor.EditWindows { diff --git a/GlitchyEditor/src/EditWindows/PropertiesWindow.bf b/GlitchyEditor/src/EditWindows/PropertiesWindow.bf index c060a1c..ced7721 100644 --- a/GlitchyEditor/src/EditWindows/PropertiesWindow.bf +++ b/GlitchyEditor/src/EditWindows/PropertiesWindow.bf @@ -18,7 +18,7 @@ class PropertiesWindow : EditorWindow private append String _selectedFileName = .(); - private Asset _currentAsset ~ _.ReleaseRef(); + private Asset _currentAsset ~ _?.ReleaseRef(); public this(Editor editor) { @@ -103,6 +103,11 @@ class PropertiesWindow : EditorWindow return; _currentPropertiesEditor.ShowEditor(); + + if (ImGui.Button("Save Asset")) + { + _editor.ContentManager.SaveAsset(_currentAsset); + } if (!assetFile.AssetConfig.Config.Changed) { diff --git a/GlitchyEditor/src/EditorContentManager.bf b/GlitchyEditor/src/EditorContentManager.bf index 87a2dc9..7e583b7 100644 --- a/GlitchyEditor/src/EditorContentManager.bf +++ b/GlitchyEditor/src/EditorContentManager.bf @@ -73,340 +73,6 @@ namespace GlitchyEditor; } }*/ -public class AssetNode -{ - public String Name ~ delete _; - public String Path ~ delete _; - - public bool IsDirectory; - - public AssetFile AssetFile ~ delete _; - - public List SubAssets ~ { - SubAssets?.ClearAndDeleteItems(); - delete SubAssets; - } - - public Texture2D PreviewImage ~ _?.ReleaseRef(); -} - -public class SubAsset -{ - public AssetNode Asset; - public String Name ~ delete _; - //public String AssetInternalPath ~ delete _; - - public Texture2D PreviewImage ~ _?.ReleaseRef(); -} - -class AssetHierarchy -{ - FileSystemWatcher fsw ~ { - _.StopRaisingEvents(); - delete _; - }; - - bool _fileSystemDirty = false; - - internal TreeNode _assetHierarchy = null ~ DeleteTreeAndChildren!(_); - private append Dictionary> _pathToAssetNode = .(); - - private append String _contentDirectory = .(); - - private EditorContentManager _contentManager; - - public StringView ContentDirectory - { - get => _contentDirectory; - private set - { - _contentDirectory.Clear(); - _contentDirectory.Append(value); - Path.Fixup(_contentDirectory); - } - } - - public this(EditorContentManager contentManager) - { - _contentManager = contentManager; - } - - public void SetContentDirectory(StringView contentDirectory) - { - ContentDirectory = contentDirectory; - - _fileSystemDirty = true; - - SetupFileSystemWatcher(); - - Update(); - } - - /// Initializes the FSW for the current ContentDirectory and registers the events. - private void SetupFileSystemWatcher() - { - delete fsw; - fsw = new FileSystemWatcher(_contentDirectory); - fsw.IncludeSubdirectories = true; - - fsw.OnChanged.Add(new (filename) => { - // Note: Gets fired for a directory if a file inside it is created/removed - - Log.EngineLogger.Trace($"File content changed (\"{filename}\")"); - //_fileSystemDirty = true; - - FileContentChanged(filename); - }); - - fsw.OnCreated.Add(new (filename) => { - Log.EngineLogger.Trace($"File created (\"{filename}\")"); - - _fileSystemDirty = true; - }); - - fsw.OnDeleted.Add(new (filename) => { - Log.EngineLogger.Trace($"File deleted (\"{filename}\")"); - - _fileSystemDirty = true; - }); - - fsw.OnRenamed.Add(new (oldName, newName) => { - Log.EngineLogger.Trace($"File renamed (From \"{oldName}\" to \"{newName}\")"); - - _fileSystemDirty = true; - /*String contentFilePath = scope String(); - - Path.InternalCombine(contentFilePath, ContentDirectory, oldName); - - //_fileSystemDirty = true; - TreeNode fileNode = GetNodeFromPath(contentFilePath); - fileNode->*/ - }); - - fsw.StartRaisingEvents(); - } - - /// Gets the tree node for the given filePath or null, if the file/directory doesn't exist. - /// @param filePath the path for which to return the tree node. - /// @remarks Do not hold a reference to the TreeNode because it can become invalid when the file hierarchy changes. - public Result> GetNodeFromPath(StringView filePath) - { - if (_pathToAssetNode.TryGetValue(filePath, let treeNode)) - { - return treeNode; - } - - return .Err; - } - - public bool FileExists(StringView filePath) - { - return _pathToAssetNode.ContainsKey(filePath); - } - - public void Update() - { - // TODO: do we really need to do this in the update loop? - if (_fileSystemDirty) - { - UpdateFiles(); - } - } - - - /// Rebuilds the asset file hierarchy. - private void UpdateFiles() - { - Log.EngineLogger.Trace($"Updating asset hierarchy"); - - if (_assetHierarchy == null) - { - // TODO: move to init? - - _assetHierarchy = new TreeNode(new AssetNode()); - _assetHierarchy->Path = new String(ContentDirectory); - _assetHierarchy->Name = new String("Content"); - - Log.EngineLogger.Trace($"Created directory node for: \"{_assetHierarchy->Path}\""); - - _pathToAssetNode.Add(ContentDirectory, _assetHierarchy); - } - - void HandleFile(AssetNode node) - { - String identifier = scope .(node.Path.Length); - Path.GetRelativePath(node.Path, _contentDirectory, identifier); - - node.AssetFile = new AssetFile(_contentManager, identifier, node.Path, node.IsDirectory); - } - - /// Determines the files that belong to the given directory and adds them to the tree. - void AddFilesOfDirectory(TreeNode directory) - { - // Filter that accepts all files. - String filter = scope $"{directory->Path}/*"; - - // Buffer used to hold the path of the files iterated below. - String filepathBuffer = scope String(256); - // Buffer used to hold the file extension of the files iterated below. - String extensionBuffer = scope String(16); - - for (var entry in Directory.Enumerate(filter, .Files)) - { - entry.GetFilePath(filepathBuffer..Clear()); - - Path.GetExtension(filepathBuffer, .. extensionBuffer..Clear()); - - // Ignore meta files. - if (extensionBuffer.Equals(AssetFile.ConfigFileExtension, .OrdinalIgnoreCase)) - continue; - - TreeNode treeNode = directory.Children.Where(scope (node) => node.Value.Path == filepathBuffer).FirstOrDefault(); - - if (treeNode == null) - { - AssetNode assetNode = new AssetNode(); - assetNode.Name = new String(); - Path.GetFileName(filepathBuffer, assetNode.Name); - - filepathBuffer.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - - assetNode.Path = new String(filepathBuffer); - assetNode.IsDirectory = false; - - treeNode = directory.AddChild(assetNode); - _pathToAssetNode.Add(assetNode.Path, treeNode); - - //GrabSubAssets(node); - HandleFile(treeNode.Value); - - Log.EngineLogger.Trace($"Created file node for: \"{assetNode.Path}\""); - } - } - } - - void RemoveOrphanedEntries(TreeNode node) - { - /// Removes the node and its children from _pathToAssetNode - void RemoveSubtree(TreeNode tree) - { - _pathToAssetNode.Remove(tree->Path); - - for (var child in tree.Children) - { - RemoveSubtree(child); - } - } - - for (TreeNode child in node.Children) - { - if (!Directory.Exists(child->Path) && !File.Exists(child->Path)) - { - Log.EngineLogger.Trace($"Removed orphaned node for: \"{child->Path}\""); - - @child.Remove(); - - RemoveSubtree(child); - - DeleteTreeAndChildren!(child); - } - } - } - - /// Adds the given directory to the specified tree. - /// Recursively adds all Files and Subdirectories. - void AddDirectoryToTree(String path, TreeNode parentNode) - { - path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - - // Try to find the node for the specified path in the given parent - TreeNode treeNode = parentNode.Children.Where(scope (node) => node.Value.Path == path).FirstOrDefault(); - - // Create new Node for the Directory, if no TreeNode exists. - if (treeNode == null) - { - AssetNode assetNode = new AssetNode(); - assetNode.Path = new String(path); - assetNode.Name = new String(); - assetNode.IsDirectory = true; - Path.GetFileName(assetNode.Path, assetNode.Name); - - treeNode = parentNode.AddChild(assetNode); - _pathToAssetNode.Add(assetNode.Path, treeNode); - - Log.EngineLogger.Trace($"Created directory node for: \"{assetNode.Path}\""); - } - - String directoryNameBuffer = scope String(256); - - // Filter that finds all entries of a directory. - String filter = scope $"{path}/*"; - - for (var directory in Directory.Enumerate(filter, .Directories)) - { - directory.GetFilePath(directoryNameBuffer..Clear()); - - AddDirectoryToTree(directoryNameBuffer, treeNode); - } - - AddFilesOfDirectory(treeNode); - - RemoveOrphanedEntries(treeNode); - } - - String filter = scope $"{ContentDirectory}/*"; - - String directoryNameBuffer = scope String(256); - - for (var directory in Directory.Enumerate(filter, .Directories)) - { - directory.GetFilePath(directoryNameBuffer..Clear()); - - AddDirectoryToTree(directoryNameBuffer, _assetHierarchy); - } - - RemoveOrphanedEntries(_assetHierarchy); - - _fileSystemDirty = false; - } - - private void FileContentChanged(StringView fileName) - { - var fileName; - - // Config files aren't really tracked but changing them effectively changes the corresponding file - // so we fire the event for them. - if (fileName.EndsWith(AssetFile.ConfigFileExtension)) - fileName.RemoveFromEnd(AssetFile.ConfigFileExtension.Length); - - String fileNameWithContentRoot = scope .(); - Path.InternalCombine(fileNameWithContentRoot, _contentDirectory, fileName); - - var nodeResult = GetNodeFromPath(fileNameWithContentRoot); - - TreeNode node = null; - - if (!(nodeResult case .Ok(out node))) - { - // This happens, when we create new files. - Log.EngineLogger.Trace($"Could not find node for file \"{fileNameWithContentRoot}\""); - return; - } - - // Don't fire event for directories. - if (node->IsDirectory) - return; - - OnFileContentChanged(node.Value); - - // TODO: Handle file changes (reload asset, etc...) - } - - public delegate void FileContentChangedFunc(AssetNode node); - - public Event OnFileContentChanged ~ _.Dispose(); -} - class EditorContentManager : IContentManager { private append String _contentDirectory = .(); @@ -602,7 +268,6 @@ class EditorContentManager : IContentManager return null; } - //AssetFile file = scope .(this, filePath, false); AssetFile file = resultNode->Value.AssetFile; IAssetLoader assetLoader = null; @@ -628,6 +293,9 @@ class EditorContentManager : IContentManager delete stream; + if (loadedAsset == null) + return null; + //String identifierString = new .(identifier); //_identifiers.Add(identifierString); @@ -642,10 +310,91 @@ class EditorContentManager : IContentManager return loadedAsset; } + /// Saves the asset. + public Result SaveAsset(Asset asset) + { + // Find subasset name + int poundIndex = asset.Identifier.IndexOf('#'); + + StringView resourceName = poundIndex == -1 ? asset.Identifier : asset.Identifier.Substring(0, poundIndex); + StringView? subassetName = asset.Identifier.Substring(poundIndex + 1); + + String filePath = scope String(resourceName.Length + _contentDirectory.Length + 2); + Path.Combine(filePath, _contentDirectory, resourceName); + + Path.Fixup(filePath); + + //filePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + + TreeNode assetNode = Try!(AssetHierarchy.GetNodeFromPath(filePath)); + + AssetFile file = assetNode->AssetFile; + + IAssetLoader assetLoader = null; + + String loaderTypeName = scope .(128); + + for (IAssetLoader loader in _assetLoaders) + { + loader.GetType().GetName(loaderTypeName..Clear()); + + if (loaderTypeName == file.AssetConfig.AssetLoader) + { + assetLoader = loader; + break; + } + } + + IAssetSaver assetSaver = assetLoader as IAssetSaver; + + if (assetSaver == null) + { + Log.EngineLogger.Error("The asset loader can't save!"); + return .Err; + } + + Stream stream = OpenStream(filePath, false, true); + + assetSaver.EditorSaveAsset(stream, asset, file.AssetConfig.Config, resourceName, subassetName, this); + + delete stream; + + return .Ok; + } + + private Stream OpenStream(StringView assetIdentifier, bool openOnly, bool truncate = false) + { + var assetIdentifier; + + if (!assetIdentifier.StartsWith(_contentDirectory)) + { + String filePath = scope:: String(assetIdentifier.Length + _contentDirectory.Length + 2); + Path.Combine(filePath, _contentDirectory, assetIdentifier); + + assetIdentifier = filePath; + } + + FileStream fs = new FileStream(); + + FileMode fileMode = openOnly ? FileMode.Open : FileMode.OpenOrCreate; + + if (truncate) + fileMode |= .Truncate; + + var result = fs.Open(assetIdentifier, fileMode, openOnly ? .Read : .ReadWrite, .ReadWrite); + + if (result case .Err) + return null; + + return fs; + } + // TODO: probably not needed public Stream GetStream(StringView assetIdentifier) { - var assetIdentifier; + return OpenStream(assetIdentifier, true); + + /*var assetIdentifier; if (!assetIdentifier.StartsWith(_contentDirectory)) { @@ -661,7 +410,7 @@ class EditorContentManager : IContentManager if (result case .Err) return null; - return fs; + return fs;*/ } public void ManageAsset(Asset asset) diff --git a/GlitchyEngine/src/Content/Asset.bf b/GlitchyEngine/src/Content/Asset.bf index 38a35fc..da33c2f 100644 --- a/GlitchyEngine/src/Content/Asset.bf +++ b/GlitchyEngine/src/Content/Asset.bf @@ -46,7 +46,7 @@ class Asset : RefCounter _contentManager?.UnmanageAsset(this); } - static void AssetSerialize(BonWriter writer, ValueView value, BonEnvironment environment) + static void AssetSerialize(BonWriter writer, ValueView value, BonEnvironment environment, SerializeValueState state) { Log.EngineLogger.Assert(value.type == typeof(Asset)); @@ -54,7 +54,7 @@ class Asset : RefCounter writer.String(identifier); } - static Result AssetDeserialize(BonReader reader, ValueView value, BonEnvironment environment)//, DeserializeFieldState state) + static Result AssetDeserialize(BonReader reader, ValueView value, BonEnvironment environment, DeserializeValueState state) { Log.EngineLogger.Assert(value.type == typeof(Asset)); diff --git a/GlitchyEngine/src/Core/UUID.bf b/GlitchyEngine/src/Core/UUID.bf index d3a31d3..d7bc046 100644 --- a/GlitchyEngine/src/Core/UUID.bf +++ b/GlitchyEngine/src/Core/UUID.bf @@ -36,14 +36,14 @@ namespace GlitchyEngine.Core return (int)_uuid; } - static void Serialize(BonWriter writer, ValueView val, BonEnvironment env) + static void Serialize(BonWriter writer, ValueView val, BonEnvironment env, SerializeValueState state) { UUID uuid = *(UUID*)val.dataPtr; Bon.Integrated.Serialize.[Friend]Integer(typeof(uint64), writer, ValueView(typeof(uint64), &uuid._uuid)); } - public static Result Deserialize(BonReader reader, ValueView val, BonEnvironment env) + public static Result Deserialize(BonReader reader, ValueView val, BonEnvironment env, DeserializeValueState state) { Bon.Integrated.Deserialize.[Friend]Integer!(typeof(uint64), reader, val); diff --git a/GlitchyEngine/src/World/SceneSerializer.bf b/GlitchyEngine/src/World/SceneSerializer.bf index 70e75d6..1faa9ad 100644 --- a/GlitchyEngine/src/World/SceneSerializer.bf +++ b/GlitchyEngine/src/World/SceneSerializer.bf @@ -33,7 +33,7 @@ namespace GlitchyEngine.World String buffer = scope String(); let writer = scope BonWriter(buffer, true); - Serialize.Start(writer); + var length = Serialize.Start(writer); gBonEnv.serializeFlags |= .IncludeDefault | .Verbose; @@ -61,7 +61,7 @@ namespace GlitchyEngine.World writer.EntryEnd(); } - Serialize.End(writer); + Serialize.End(writer, length); String targetDirectory = Path.GetDirectoryPath(filePath, .. scope String()); Directory.CreateDirectory(targetDirectory); diff --git a/GlitchyEngine/vendor/bon b/GlitchyEngine/vendor/bon index 928cfa1..0c80e36 160000 --- a/GlitchyEngine/vendor/bon +++ b/GlitchyEngine/vendor/bon @@ -1 +1 @@ -Subproject commit 928cfa14ea96ce5a978bf22c4ecb1750c57403b5 +Subproject commit 0c80e365cd20d8d56649e01632a317bcbad6f61c