Initial virtual file system implementation.

This commit is contained in:
2024-10-21 01:26:22 +02:00
parent bb6900a60a
commit fdcf29d6e0
14 changed files with 219 additions and 57 deletions

View File

@@ -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<IStartableSystem> _startableSystems = new();
private List<IUpdatableSystem> _updatableSystems = new();
private List<IUpdatableSystem> _renderableSystems = new();

View File

@@ -7,7 +7,7 @@ namespace Voile.Resources.DataReaders;
/// <summary>
/// Reads key/value data from a TOML file.
/// </summary>
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;
}

View File

@@ -1,4 +1,6 @@
using Voile.VFS;
namespace Voile.Resources;
public class FontLoader : ResourceLoader<Font>
@@ -11,9 +13,14 @@ public class FontLoader : ResourceLoader<Font>
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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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))
{

View File

@@ -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<ParticleEmit
{
var settings = new ParticleEmitterSettings();
using (var reader = new TomlDataReader("ParticleEmitterSettings"))
{
reader.Read(File.Open(path, FileMode.Open));
var reader = new TomlDataReader("ParticleEmitterSettings");
reader.Read(VirtualFileSystem.Read(path));
settings.Local = reader.GetBool("Local", true);
settings.MaxParticles = reader.GetInt("MaxParticles");
settings.EmitRadius = reader.GetInt("EmitRadius");
settings.LifeTime = reader.GetFloat("LifeTime", 1.0f);
settings.Explosiveness = reader.GetFloat("Explosiveness");
settings.Direction = reader.GetVector2("Direction", Vector2.Zero);
settings.LinearVelocity = reader.GetFloat("LinearVelocity", 980f);
settings.AngularVelocity = reader.GetFloat("AngularVelocity");
settings.AngularVelocityRandom = reader.GetFloat("AngularVelocityRandom", 1.0f);
settings.LinearVelocityRandom = reader.GetFloat("LinearVelocityRandom", 1.0f);
settings.Gravity = reader.GetVector2("Gravity", Vector2.UnitY * 980f);
settings.ScaleBegin = reader.GetFloat("ScaleBegin");
settings.ScaleEnd = reader.GetFloat("ScaleEnd");
settings.ColorBegin = reader.GetColor("ColorBegin", Color.White);
settings.ColorEnd = reader.GetColor("ColorEnd", Color.Black);
settings.LinearVelocityDamping = reader.GetFloat("LinearVelocityDamping", 1.0f);
settings.AngularVelocityDamping = reader.GetFloat("AngularVelocityDamping", 1.0f);
}
settings.Local = reader.GetBool("Local", true);
settings.MaxParticles = reader.GetInt("MaxParticles");
settings.EmitRadius = reader.GetInt("EmitRadius");
settings.LifeTime = reader.GetFloat("LifeTime", 1.0f);
settings.Explosiveness = reader.GetFloat("Explosiveness");
settings.Direction = reader.GetVector2("Direction", Vector2.Zero);
settings.LinearVelocity = reader.GetFloat("LinearVelocity", 980f);
settings.AngularVelocity = reader.GetFloat("AngularVelocity");
settings.AngularVelocityRandom = reader.GetFloat("AngularVelocityRandom", 1.0f);
settings.LinearVelocityRandom = reader.GetFloat("LinearVelocityRandom", 1.0f);
settings.Gravity = reader.GetVector2("Gravity", Vector2.UnitY * 980f);
settings.ScaleBegin = reader.GetFloat("ScaleBegin");
settings.ScaleEnd = reader.GetFloat("ScaleEnd");
settings.ColorBegin = reader.GetColor("ColorBegin", Color.White);
settings.ColorEnd = reader.GetColor("ColorEnd", Color.Black);
settings.LinearVelocityDamping = reader.GetFloat("LinearVelocityDamping", 1.0f);
settings.AngularVelocityDamping = reader.GetFloat("AngularVelocityDamping", 1.0f);
return new ParticleEmitterSettingsResource(path, settings);
}

View File

@@ -0,0 +1,69 @@
using Voile.Utils;
namespace Voile.VFS;
/// <summary>
/// A file in the OS file system.
/// </summary>
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;
}
/// <summary>
/// A <see cref="IVirtualMountPoint"/> implementation for an OS file system.
/// </summary>
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<string, VirtualFile> 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<string, VirtualFile> _files = new();
}

View File

@@ -0,0 +1,6 @@
namespace Voile.VFS;
public abstract class VirtualFile
{
public abstract Stream GetStream();
}

View File

@@ -0,0 +1,33 @@
namespace Voile.VFS;
/// <summary>
/// A virtual mounting point.
/// </summary>
public interface IVirtualMountPoint
{
/// <summary>
/// Order of mounting for this mount point. Lower values indicate higher priority for lookup.
/// </summary>
int Order { get; }
/// <summary>
/// Mounts this <see cref="IVirtualMountPoint"/>.
/// </summary>
void Mount();
/// <summary>
/// Gets a file.
/// </summary>
/// <param name="path">Relative path to the file.</param>
/// <returns>An instance of <see cref="VirtualFile"/> if the file exists; otherwise, an exception is thrown.</returns>
VirtualFile GetFile(string path);
/// <summary>
/// Gets all files available at this <see cref="IVirtualMountPoint"/>.
/// </summary>
/// <returns>A dictionary mapping a relative path to an instance of a <see cref="VirtualFile"/>.</returns>
IDictionary<string, VirtualFile> GetFiles();
/// <summary>
/// Determines whether a file exists at the given relative path within this mount point.
/// </summary>
/// <param name="path">The relative path of the file to check.</param>
/// <returns><c>true</c> if the file exists; otherwise, <c>false</c>.</returns>
bool HasFile(string path);
}

View File

@@ -0,0 +1,59 @@
namespace Voile.VFS;
/// <summary>
/// A virtual file system that provides an abstract interface for manipulating files from various sources.
/// </summary>
public static class VirtualFileSystem
{
/// <summary>
/// Mounts a <see cref="IVirtualMountPoint"/>.
/// This will make files available for this mount point accessible.
/// </summary>
/// <param name="mountPoint"></param>
public static void Mount(IVirtualMountPoint mountPoint)
{
mountPoint.Mount();
_mounts.Add(mountPoint);
_mounts = _mounts.OrderBy(mount => mount.Order).ToList();
}
/// <summary>
/// Check if file exists or not.
/// </summary>
/// <param name="path">Relative path to the file.</param>
/// <returns></returns>
public static bool FileExists(string path)
{
return _mounts.Any(mount => mount.HasFile(path));
}
/// <summary>
/// Gets a <see cref="VirtualFile"/> from path.
/// </summary>
/// <param name="path">Relative path to the file.</param>
/// <returns>A virtual file at the path.</returns>
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);
}
/// <summary>
/// Reads a file.
/// </summary>
/// <param name="path">Relative path to the file.</param>
/// <returns>A readable stream.</returns>
public static Stream Read(string path)
{
var file = GetFile(path);
return file.GetStream();
}
private static List<IVirtualMountPoint> _mounts = new();
}