diff --git a/TODO.md b/TODO.md index 38d6a20..cf2eeb1 100644 --- a/TODO.md +++ b/TODO.md @@ -14,7 +14,8 @@ - ~~Reimplement unloading.~~ - Finalize ResourceManager and ResourceLoader APIs for 1.0. - Add async API for ResourceManager. -- Virtual file system. +- ~~Virtual file system.~~ +- Custom PAK format using the VFS implementation. - (stretch goal) Streamed resource loading. ## Serialization diff --git a/TestGame/Resources/fire_effect.toml b/TestGame/Resources/fire_effect.toml index 17ad69a..df3c818 100644 --- a/TestGame/Resources/fire_effect.toml +++ b/TestGame/Resources/fire_effect.toml @@ -17,4 +17,4 @@ LinearVelocityRandom = 0.5 ScaleBegin = 0.1 ScaleEnd = 5.0 ColorBegin = [255, 162, 0] -ColorEnd = [64, 64, 64, 0] +ColorEnd = [0, 0, 0, 0] diff --git a/Voile.sln b/Voile.sln index 755126d..fbf3adc 100644 --- a/Voile.sln +++ b/Voile.sln @@ -18,14 +18,7 @@ Global Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {393AA04F-A0DE-42F2-AAEC-6B2DCFB7A852}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {393AA04F-A0DE-42F2-AAEC-6B2DCFB7A852}.Debug|Any CPU.Build.0 = Debug|Any CPU - {393AA04F-A0DE-42F2-AAEC-6B2DCFB7A852}.Release|Any CPU.ActiveCfg = Release|Any CPU - {393AA04F-A0DE-42F2-AAEC-6B2DCFB7A852}.Release|Any CPU.Build.0 = Release|Any CPU {DA4FDEDC-AA81-4336-844F-562F9E763974}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DA4FDEDC-AA81-4336-844F-562F9E763974}.Debug|Any CPU.Build.0 = Debug|Any CPU {DA4FDEDC-AA81-4336-844F-562F9E763974}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -35,4 +28,7 @@ Global {DBA85D7B-0A91-405B-9078-5463F49AE47E}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBA85D7B-0A91-405B-9078-5463F49AE47E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection EndGlobal diff --git a/Voile/Source/Game.cs b/Voile/Source/Game.cs index 00949b3..b4b4102 100644 --- a/Voile/Source/Game.cs +++ b/Voile/Source/Game.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Voile.Input; using Voile.Rendering; using Voile.Resources; +using Voile.VFS; namespace Voile { @@ -88,6 +89,7 @@ namespace Voile throw new NullReferenceException("No ResourceManager provided."); } + Mount(); LoadResources(); Ready(); Run(); @@ -239,6 +241,12 @@ namespace Voile Renderer?.Start(renderSettings); } + private void Mount() + { + var resourceRootMount = new FileSystemMountPoint(ResourceRoot); + VirtualFileSystem.Mount(resourceRootMount); + } + private List _startableSystems = new(); private List _updatableSystems = new(); private List _renderableSystems = new(); diff --git a/Voile/Source/Resources/DataReaders/TomlDataReader.cs b/Voile/Source/Resources/DataReaders/TomlDataReader.cs index da18592..078689d 100644 --- a/Voile/Source/Resources/DataReaders/TomlDataReader.cs +++ b/Voile/Source/Resources/DataReaders/TomlDataReader.cs @@ -7,7 +7,7 @@ namespace Voile.Resources.DataReaders; /// /// Reads key/value data from a TOML file. /// -public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValueGetter, IDisposable +public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValueGetter { public string ExpectedHeader { get; private set; } = string.Empty; public TomlDataReader(string expectedHeader) @@ -17,14 +17,7 @@ public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValue public void Read(Stream data) { - if (data is not FileStream fs) - { - throw new ArgumentException("Toml data reader only supports file streams."); - } - - _fs = fs; - - using (var reader = new StreamReader(_fs)) + using (var reader = new StreamReader(data)) { _table = TOML.Parse(reader); _valid = _table.HasKey(ExpectedHeader); @@ -231,12 +224,6 @@ public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValue public bool Valid() => _valid; - public void Dispose() - { - _fs?.Dispose(); - } - private TomlTable? _table; - private FileStream? _fs; private bool _valid; } \ No newline at end of file diff --git a/Voile/Source/Resources/Loaders/FontLoader.cs b/Voile/Source/Resources/Loaders/FontLoader.cs index 9723e2f..5ea6854 100644 --- a/Voile/Source/Resources/Loaders/FontLoader.cs +++ b/Voile/Source/Resources/Loaders/FontLoader.cs @@ -1,4 +1,6 @@ +using Voile.VFS; + namespace Voile.Resources; public class FontLoader : ResourceLoader @@ -11,9 +13,14 @@ public class FontLoader : ResourceLoader protected override Font LoadResource(string path) { - byte[] fileBuffer = File.ReadAllBytes(path); + using Stream stream = VirtualFileSystem.Read(path); + + byte[] fileBuffer = new byte[stream.Length]; + int bytesRead = stream.Read(fileBuffer, 0, fileBuffer.Length); var result = new Font(path, fileBuffer); - result.BufferSize = fileBuffer.Length; + + result.BufferSize = bytesRead; + return result; } } \ No newline at end of file diff --git a/Voile/Source/Resources/Loaders/ResourceLoader.cs b/Voile/Source/Resources/Loaders/ResourceLoader.cs index 0fbd237..833300f 100644 --- a/Voile/Source/Resources/Loaders/ResourceLoader.cs +++ b/Voile/Source/Resources/Loaders/ResourceLoader.cs @@ -24,7 +24,6 @@ namespace Voile.Resources var resource = LoadResource(path); var guid = Guid.NewGuid(); - var loadedResources = ResourceManager.LoadedResources; var oldResourceGuid = loadedResources.FirstOrDefault(loadedResource => loadedResource.Value.Path == path).Key; @@ -46,6 +45,7 @@ namespace Voile.Resources { foreach (var loadedResource in ResourceManager.LoadedResources) { + if (loadedResource.Value is not T) continue; Load(loadedResource.Value.Path); } } diff --git a/Voile/Source/Resources/Loaders/Texture2dLoader.cs b/Voile/Source/Resources/Loaders/Texture2dLoader.cs index 0f6afd5..cf21175 100644 --- a/Voile/Source/Resources/Loaders/Texture2dLoader.cs +++ b/Voile/Source/Resources/Loaders/Texture2dLoader.cs @@ -1,5 +1,6 @@ using Voile.Resources; using StbImageSharp; +using Voile.VFS; namespace Voile { @@ -18,7 +19,7 @@ namespace Voile protected override Texture2d LoadResource(string path) { ImageResult image; - using (var stream = File.OpenRead(path)) + using (var stream = VirtualFileSystem.Read(path)) { image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); } diff --git a/Voile/Source/Resources/ResourceManager.cs b/Voile/Source/Resources/ResourceManager.cs index 2e62cb4..36d5f3d 100644 --- a/Voile/Source/Resources/ResourceManager.cs +++ b/Voile/Source/Resources/ResourceManager.cs @@ -1,8 +1,8 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; using Voile.Utils; +using Voile.VFS; namespace Voile.Resources { @@ -92,10 +92,7 @@ namespace Voile.Resources T? resource = default; result = null; - var fullPath = Path.Combine(ResourceRoot, path); - - // TODO: don't check if file doesn't exist in the file system, make it more generic but for now it's fine - if (!File.Exists(fullPath)) + if (!VirtualFileSystem.FileExists(path)) { _logger.Error($"File at \"{path}\" doesn't exist!"); return false; @@ -108,7 +105,7 @@ namespace Voile.Resources return false; } - var extension = Path.GetExtension(fullPath); + var extension = Path.GetExtension(path); var hasExtension = loader.SupportedExtensions.Any(ext => ext == extension); if (!hasExtension) @@ -116,7 +113,7 @@ namespace Voile.Resources _logger.Error($"Extension {extension} is not supported!"); } - var resourceGuid = loader.Load(fullPath); + var resourceGuid = loader.Load(path); if (!GetResource(resourceGuid, out T? loadedResource)) { diff --git a/Voile/Source/Systems/ParticleSystem.cs b/Voile/Source/Systems/ParticleSystem.cs index 9e030b3..3b9d240 100644 --- a/Voile/Source/Systems/ParticleSystem.cs +++ b/Voile/Source/Systems/ParticleSystem.cs @@ -1,7 +1,7 @@ using System.Numerics; using Voile.Resources; using Voile.Resources.DataReaders; -using Voile.Utils; +using Voile.VFS; namespace Voile.Systems.Particles; @@ -62,28 +62,26 @@ public class ParticleEmitterSettingsResourceLoader : ResourceLoader +/// A file in the OS file system. +/// +public class FileSystemFile : VirtualFile +{ + public FileSystemFile(string path) + { + _fsPath = path; + } + + public override Stream GetStream() + { + return new FileStream(_fsPath, FileMode.Open, FileAccess.Read); + } + + private string _fsPath; +} + +/// +/// A implementation for an OS file system. +/// +public class FileSystemMountPoint : IVirtualMountPoint +{ + public int Order => int.MaxValue; + + public FileSystemMountPoint(string path) + { + _fsPath = path; + } + + public void Mount() + { + _logger.Info($"Mounting from file system path \"{_fsPath}\"."); + + int rootLength = _fsPath.Length; + var files = + Directory.GetFiles(_fsPath, "*", SearchOption.AllDirectories) + .Select(p => p.Remove(0, rootLength)) + .ToList(); + + foreach (string file in files) + { + var relativePath = NormalizePath(file); + var fullPath = NormalizePath(Path.Combine(_fsPath, file)); + _files[relativePath] = new FileSystemFile(fullPath); + } + } + + public VirtualFile GetFile(string path) => _files[path]; + + public IDictionary GetFiles() => _files; + + public bool HasFile(string path) => _files.ContainsKey(path); + + private string NormalizePath(string path) + { + return path + .Replace(@"\\", @"\") + .Replace(@"\", @"/"); + } + + private Logger _logger = new(nameof(FileSystemMountPoint)); + private string _fsPath; + private Dictionary _files = new(); +} \ No newline at end of file diff --git a/Voile/Source/VFS/IVirtualFile.cs b/Voile/Source/VFS/IVirtualFile.cs new file mode 100644 index 0000000..947aa27 --- /dev/null +++ b/Voile/Source/VFS/IVirtualFile.cs @@ -0,0 +1,6 @@ +namespace Voile.VFS; + +public abstract class VirtualFile +{ + public abstract Stream GetStream(); +} \ No newline at end of file diff --git a/Voile/Source/VFS/IVirtualMountPoint.cs b/Voile/Source/VFS/IVirtualMountPoint.cs new file mode 100644 index 0000000..69efd47 --- /dev/null +++ b/Voile/Source/VFS/IVirtualMountPoint.cs @@ -0,0 +1,33 @@ +namespace Voile.VFS; + +/// +/// A virtual mounting point. +/// +public interface IVirtualMountPoint +{ + /// + /// Order of mounting for this mount point. Lower values indicate higher priority for lookup. + /// + int Order { get; } + /// + /// Mounts this . + /// + void Mount(); + /// + /// Gets a file. + /// + /// Relative path to the file. + /// An instance of if the file exists; otherwise, an exception is thrown. + VirtualFile GetFile(string path); + /// + /// Gets all files available at this . + /// + /// A dictionary mapping a relative path to an instance of a . + IDictionary GetFiles(); + /// + /// Determines whether a file exists at the given relative path within this mount point. + /// + /// The relative path of the file to check. + /// true if the file exists; otherwise, false. + bool HasFile(string path); +} \ No newline at end of file diff --git a/Voile/Source/VFS/VirtualFileSystem.cs b/Voile/Source/VFS/VirtualFileSystem.cs new file mode 100644 index 0000000..8c59ec5 --- /dev/null +++ b/Voile/Source/VFS/VirtualFileSystem.cs @@ -0,0 +1,59 @@ +namespace Voile.VFS; + +/// +/// A virtual file system that provides an abstract interface for manipulating files from various sources. +/// +public static class VirtualFileSystem +{ + /// + /// Mounts a . + /// This will make files available for this mount point accessible. + /// + /// + public static void Mount(IVirtualMountPoint mountPoint) + { + mountPoint.Mount(); + _mounts.Add(mountPoint); + + _mounts = _mounts.OrderBy(mount => mount.Order).ToList(); + } + + /// + /// Check if file exists or not. + /// + /// Relative path to the file. + /// + public static bool FileExists(string path) + { + return _mounts.Any(mount => mount.HasFile(path)); + } + + /// + /// Gets a from path. + /// + /// Relative path to the file. + /// A virtual file at the path. + public static VirtualFile GetFile(string path) + { + var mount = _mounts.FirstOrDefault(mount => mount.HasFile(path)); + if (mount == null) + { + throw new FileNotFoundException($"File \"{path}\" was not found in any mounted point."); + } + + return mount.GetFile(path); + } + + /// + /// Reads a file. + /// + /// Relative path to the file. + /// A readable stream. + public static Stream Read(string path) + { + var file = GetFile(path); + return file.GetStream(); + } + + private static List _mounts = new(); +} \ No newline at end of file