Compare commits

..

10 Commits

32 changed files with 464 additions and 373 deletions

2
.gitignore vendored
View File

@@ -50,3 +50,5 @@ Voile.Fmod/runtimes/**/*.so*
# DocFX # DocFX
.cache .cache
/**/_site/ /**/_site/
*.dmp

View File

@@ -7,6 +7,7 @@
- ~~TextDataResource providing a convenient wrapper around TOML data files.~~ - ~~TextDataResource providing a convenient wrapper around TOML data files.~~
- ~~Add documentation for common classes.~~ - ~~Add documentation for common classes.~~
- ~~Hot reloading of resources.~~ - ~~Hot reloading of resources.~~
- Separate render and update thread.
## I/O ## I/O
@@ -14,7 +15,8 @@
- ~~Reimplement unloading.~~ - ~~Reimplement unloading.~~
- Finalize ResourceManager and ResourceLoader APIs for 1.0. - Finalize ResourceManager and ResourceLoader APIs for 1.0.
- Add async API for ResourceManager. - Add async API for ResourceManager.
- Virtual file system. - ~~Virtual file system.~~
- Custom PAK format using the VFS implementation.
- (stretch goal) Streamed resource loading. - (stretch goal) Streamed resource loading.
## Serialization ## Serialization
@@ -29,7 +31,7 @@
- ~~API for drawing textured quads~~ - ~~API for drawing textured quads~~
- ~~Camera API~~ - ~~Camera API~~
- Arbitrary mesh rendering API. - Arbitrary mesh rendering API.
- Create WebGPU renderer (StandardRenderer) - Create WebGPU renderer (StandardRenderSystem)
## Audio ## Audio

View File

@@ -17,4 +17,4 @@ LinearVelocityRandom = 0.5
ScaleBegin = 0.1 ScaleBegin = 0.1
ScaleEnd = 5.0 ScaleEnd = 5.0
ColorBegin = [255, 162, 0] ColorBegin = [255, 162, 0]
ColorEnd = [64, 64, 64, 0] ColorEnd = [0, 0, 0, 0]

Binary file not shown.

View File

@@ -5,17 +5,21 @@ using Voile.Systems.Particles;
using System.Numerics; using System.Numerics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Voile.Rendering; using Voile.Rendering;
using Voile.OpenAL;
public class TestGame : Game public class TestGame : Game
{ {
public override string Name => "Jump Adventures 2"; public override string Name => "Test Game";
public override string ResourceRoot => "Resources/"; public override string ResourceRoot => "Resources/";
public override void Initialize() public override void Initialize()
{ {
InitializeSystemsDefault(); InitializeSystemsDefault();
_audioSystem = new OpenALSystem();
_particleSystem = new ParticleSystem(); _particleSystem = new ParticleSystem();
AddSystemToUpdate(_audioSystem);
AddSystemToUpdate(_particleSystem); AddSystemToUpdate(_particleSystem);
} }
@@ -34,6 +38,7 @@ public class TestGame : Game
} }
ResourceManager.TryLoad("icon.png", out _icon); ResourceManager.TryLoad("icon.png", out _icon);
ResourceManager.TryLoad("sounds/test_sound_mono.ogg", out _sound);
if (!ResourceManager.TryLoad("fire_effect.toml", out _fireEffect)) if (!ResourceManager.TryLoad("fire_effect.toml", out _fireEffect))
{ {
@@ -43,22 +48,18 @@ public class TestGame : Game
protected override void Ready() protected override void Ready()
{ {
Input.AddInputMapping("reload", new InputAction[] { new KeyInputAction(KeyboardKey.R) }); Input.AddInputMapping("reload", new IInputAction[] { new KeyInputAction(KeyboardKey.R) });
_emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _fireEffect); _emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _fireEffect);
} }
protected override void Update(double deltaTime) protected override void Update(double deltaTime)
{
}
protected override void Render(double deltaTime)
{ {
if (Input.IsActionPressed("reload")) if (Input.IsActionPressed("reload"))
{ {
ResourceManager.Reload(); // ResourceManager.Reload();
_particleSystem!.RestartEmitter(_emitterId); // _particleSystem!.RestartEmitter(_emitterId);
_audioSystem.PlaySound(_sound.Value, 1.0f);
} }
if (Input.KeyboardKeyJustPressed(KeyboardKey.One)) if (Input.KeyboardKeyJustPressed(KeyboardKey.One))
@@ -70,7 +71,10 @@ public class TestGame : Game
{ {
_particleSystem.SetEmitterPosition(_emitterId, Input.GetMousePosition()); _particleSystem.SetEmitterPosition(_emitterId, Input.GetMousePosition());
} }
}
protected override void Render(double deltaTime)
{
Renderer.ClearBackground(Color.Black); Renderer.ClearBackground(Color.Black);
foreach (var emitter in _particleSystem!.Emitters) foreach (var emitter in _particleSystem!.Emitters)
{ {
@@ -105,8 +109,10 @@ public class TestGame : Game
} }
[NotNull] private ParticleSystem _particleSystem; [NotNull] private ParticleSystem _particleSystem;
private OpenALSystem _audioSystem;
private int _emitterId; private int _emitterId;
private ResourceRef<ParticleEmitterSettingsResource> _fireEffect; private ResourceRef<ParticleEmitterSettingsResource> _fireEffect;
private ResourceRef<Font> _font; private ResourceRef<Font> _font;
private ResourceRef<Sound> _sound;
private ResourceRef<Texture2d> _icon; private ResourceRef<Texture2d> _icon;
} }

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable> <Nullable>disable</Nullable>
@@ -11,6 +11,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="../Voile/Voile.csproj" /> <ProjectReference Include="../Voile/Voile.csproj" />
<ProjectReference Include="../Voile.OpenAL/Voile.OpenAL.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,59 @@
using Voile.Audio;
using Silk.NET.OpenAL;
namespace Voile.OpenAL;
public class OpenALSystem : AudioSystem
{
public OpenALSystem()
{
_al = AL.GetApi();
_alc = ALContext.GetApi();
Init();
}
public override void PlaySound(Sound sound, float volume)
{
var buffer = CreateAlBuffer(sound.Buffer, sound.SampleRate, sound.Channel);
var source = CreateAlSource(buffer);
_al.SourcePlay(source);
}
public override void Update(double deltaTime)
{
throw new NotImplementedException();
}
private unsafe void Init()
{
_device = _alc.OpenDevice("");
_context = _alc.CreateContext(_device, null);
_alc.MakeContextCurrent(_context);
}
private uint CreateAlBuffer(ReadOnlyMemory<short> data, int sampleRate, SoundChannel channels)
{
var buffer = _al.GenBuffer();
var format = channels == SoundChannel.Mono ? BufferFormat.Mono16 : BufferFormat.Stereo16;
unsafe
{
_al.BufferData(buffer, format, data.Pin().Pointer, data.Length, sampleRate);
}
return buffer;
}
private uint CreateAlSource(uint buffer)
{
var source = _al.GenSource();
_al.SetSourceProperty(source, SourceInteger.Buffer, buffer);
return source;
}
private unsafe Device* _device;
private ALContext _alc;
private unsafe Context* _context;
private AL _al;
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Voile/Voile.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Silk.NET.OpenAL" Version="2.21.0" />
</ItemGroup>
</Project>

View File

@@ -13,19 +13,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Folder", "Solution
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestGame", "TestGame\TestGame.csproj", "{DBA85D7B-0A91-405B-9078-5463F49AE47E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestGame", "TestGame\TestGame.csproj", "{DBA85D7B-0A91-405B-9078-5463F49AE47E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Voile.OpenAL", "Voile.OpenAL\Voile.OpenAL.csproj", "{3ABB7D30-4B64-43AD-A14F-E532B12AFC60}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution 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.ActiveCfg = Debug|Any CPU
{DA4FDEDC-AA81-4336-844F-562F9E763974}.Debug|Any CPU.Build.0 = 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 {DA4FDEDC-AA81-4336-844F-562F9E763974}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -34,5 +29,12 @@ Global
{DBA85D7B-0A91-405B-9078-5463F49AE47E}.Debug|Any CPU.Build.0 = Debug|Any CPU {DBA85D7B-0A91-405B-9078-5463F49AE47E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DBA85D7B-0A91-405B-9078-5463F49AE47E}.Release|Any CPU.ActiveCfg = Release|Any CPU {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 {DBA85D7B-0A91-405B-9078-5463F49AE47E}.Release|Any CPU.Build.0 = Release|Any CPU
{3ABB7D30-4B64-43AD-A14F-E532B12AFC60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3ABB7D30-4B64-43AD-A14F-E532B12AFC60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3ABB7D30-4B64-43AD-A14F-E532B12AFC60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3ABB7D30-4B64-43AD-A14F-E532B12AFC60}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@@ -1,7 +0,0 @@
namespace Voile
{
public class AudioBus
{
}
}

View File

@@ -1,9 +0,0 @@
namespace Voile
{
public class AudioEffect { }
public class AudioEffectReverb : AudioEffect
{
}
}

View File

@@ -1,40 +1,7 @@
using Voile;
namespace Voile.Audio; namespace Voile.Audio;
public abstract class AudioSystem : IStartableSystem, IUpdatableSystem, IDisposable public abstract class AudioSystem : IUpdatableSystem
{ {
public void Start() => Initialize(); public abstract void Update(double deltaTime);
public abstract void PlaySound(Sound sound, float volume);
public void Update(double deltaTime) => Update();
public void Dispose()
{
Shutdown();
GC.SuppressFinalize(this);
}
protected abstract void Initialize();
protected abstract void Update();
protected abstract void Shutdown();
// BUS
public abstract void CreateBus(string busName);
public abstract void SetBusVolume(string busName, float volume);
public abstract float GetBusVolume(string busName);
// SOUND
public abstract void PlaySound(Sound sound, float pitch, float volume, string bus = "Master");
public void PlaySound(Sound sound, string bus = "Master") => PlaySound(sound, 1.0f, 1.0f, bus);
public SoundInstance CreateInstance(Sound sound)
{
var instance = new SoundInstance(this, sound);
return instance;
}
// EFFECTS
public abstract void AddBusEffect<T>(T effect, string bus = "Master") where T : AudioEffect;
private LehmerRandom _random = new LehmerRandom();
} }

View File

@@ -1,48 +0,0 @@
namespace Voile.Audio
{
/// <summary>
/// Dummy audio system.
/// </summary>
public class DummyAudioSystem : AudioSystem
{
public override void AddBusEffect<T>(T effect, string bus = "Master")
{
return;
}
public override void CreateBus(string busName)
{
return;
}
public override float GetBusVolume(string busName)
{
return 0.0f;
}
protected override void Initialize()
{
return;
}
public override void SetBusVolume(string busName, float volume)
{
return;
}
protected override void Shutdown()
{
return;
}
protected override void Update()
{
return;
}
public override void PlaySound(Sound sound, float pitch, float volume, string bus = "Master")
{
return;
}
}
}

View File

@@ -1,42 +0,0 @@
namespace Voile.Audio
{
public class SoundInstance
{
protected virtual Sound Sound => _sound;
public SoundInstance(AudioSystem backend, Sound sound)
{
_backend = backend;
_sound = sound;
}
public SoundInstance PitchVariation(float min, float max)
{
var random = new LehmerRandom();
_pitch = (float)random.NextDouble() * (max - min) + min;
return this;
}
public SoundInstance VolumeVariation(float min, float max)
{
var random = new LehmerRandom();
_volume = (float)random.NextDouble() * (max - min) + min;
return this;
}
public SoundInstance OnBus(string bus = "Master")
{
_bus = bus;
return this;
}
public void Play()
{
_backend.PlaySound(Sound, _pitch, _volume, _bus);
}
private readonly AudioSystem _backend;
private readonly Sound _sound;
private string _bus = "Master";
private float _pitch, _volume = 1.0f;
}
}

View File

@@ -1,8 +1,10 @@
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Voile.Audio;
using Voile.Input; using Voile.Input;
using Voile.Rendering; using Voile.Rendering;
using Voile.Resources; using Voile.Resources;
using Voile.VFS;
namespace Voile namespace Voile
{ {
@@ -41,6 +43,8 @@ namespace Voile
/// </summary> /// </summary>
protected RenderSystem? Renderer { get; set; } protected RenderSystem? Renderer { get; set; }
protected AudioSystem? AudioSystem { get; set; }
/// <summary> /// <summary>
/// Name of this game. Also used as a default window title. /// Name of this game. Also used as a default window title.
/// </summary> /// </summary>
@@ -88,6 +92,7 @@ namespace Voile
throw new NullReferenceException("No ResourceManager provided."); throw new NullReferenceException("No ResourceManager provided.");
} }
Mount();
LoadResources(); LoadResources();
Ready(); Ready();
Run(); Run();
@@ -239,6 +244,12 @@ namespace Voile
Renderer?.Start(renderSettings); Renderer?.Start(renderSettings);
} }
private void Mount()
{
var resourceRootMount = new FileSystemMountPoint(ResourceRoot);
VirtualFileSystem.Mount(resourceRootMount);
}
private List<IStartableSystem> _startableSystems = new(); private List<IStartableSystem> _startableSystems = new();
private List<IUpdatableSystem> _updatableSystems = new(); private List<IUpdatableSystem> _updatableSystems = new();
private List<IUpdatableSystem> _renderableSystems = new(); private List<IUpdatableSystem> _renderableSystems = new();

View File

@@ -1,13 +1,14 @@
namespace Voile.Input namespace Voile.Input
{ {
public abstract class InputAction
public interface IInputAction
{ {
public abstract bool IsDown(InputSystem inputHandler); bool IsDown(InputSystem inputSystem);
public abstract bool IsPressed(InputSystem inputHandler); bool IsPressed(InputSystem inputSystem);
public abstract bool IsReleased(InputSystem inputHandler); bool IsReleased(InputSystem inputSystem);
} }
public class KeyInputAction : InputAction public struct KeyInputAction : IInputAction
{ {
public KeyboardKey Key => _keyboardKey; public KeyboardKey Key => _keyboardKey;
@@ -15,19 +16,19 @@ namespace Voile.Input
{ {
_keyboardKey = keyboardKey; _keyboardKey = keyboardKey;
} }
public override bool IsDown(InputSystem inputHandler) public bool IsDown(InputSystem inputSystem)
{ {
return inputHandler.IsKeyboardKeyDown(_keyboardKey); return inputSystem.IsKeyboardKeyDown(_keyboardKey);
} }
public override bool IsPressed(InputSystem inputHandler) public bool IsPressed(InputSystem inputSystem)
{ {
return inputHandler.KeyboardKeyJustPressed(_keyboardKey); return inputSystem.KeyboardKeyJustPressed(_keyboardKey);
} }
public override bool IsReleased(InputSystem inputHandler) public bool IsReleased(InputSystem inputSystem)
{ {
return inputHandler.KeyboardKeyJustReleased(_keyboardKey); return inputSystem.KeyboardKeyJustReleased(_keyboardKey);
} }

View File

@@ -12,11 +12,11 @@ namespace Voile.Input
/// <summary> /// <summary>
/// The list of all available input mappings, custom and built-in. /// The list of all available input mappings, custom and built-in.
/// </summary> /// </summary>
public static IReadOnlyDictionary<string, List<InputAction>> InputMappings => inputMappings; public static IReadOnlyDictionary<string, List<IInputAction>> InputMappings => inputMappings;
public void Start() public void Start()
{ {
inputMappings = new Dictionary<string, List<InputAction>>(); inputMappings = new Dictionary<string, List<IInputAction>>();
CreateDefaultMappings(); CreateDefaultMappings();
} }
@@ -25,17 +25,82 @@ namespace Voile.Input
public void Dispose() => GC.SuppressFinalize(this); public void Dispose() => GC.SuppressFinalize(this);
public bool Handled { get => _handled; set => _handled = value; } public bool Handled { get => _handled; set => _handled = value; }
public bool IsActionDown(string action)
{
List<IInputAction>? mappings;
if (TryGetInputMappings(action, out mappings))
{
foreach (IInputAction inputAction in mappings)
if (inputAction.IsDown(this)) return true;
}
return false;
}
public bool IsActionPressed(string action)
{
List<IInputAction>? mappings;
if (TryGetInputMappings(action, out mappings))
{
foreach (IInputAction inputAction in mappings)
if (inputAction.IsPressed(this)) return true;
}
return false;
}
public bool IsActionReleased(string action)
{
List<IInputAction>? mappings;
if (TryGetInputMappings(action, out mappings))
{
foreach (IInputAction inputAction in mappings)
if (inputAction.IsReleased(this)) return true;
}
return false;
}
public Vector2 GetInputDirection(KeyboardKey leftKey, KeyboardKey rightKey, KeyboardKey upKey, KeyboardKey downKey)
{
Vector2 dir = Vector2.Zero;
if (IsKeyboardKeyDown(leftKey))
dir.X -= 1;
if (IsKeyboardKeyDown(rightKey))
dir.X += 1;
if (IsKeyboardKeyDown(upKey))
dir.Y -= 1;
if (IsKeyboardKeyDown(downKey))
dir.Y += 1;
return dir == Vector2.Zero ? Vector2.Zero : Vector2.Normalize(dir);
}
public Vector2 GetInputDirection(string leftAction, string rightAction, string upAction, string downAction)
{
Vector2 dir = Vector2.Zero;
if (IsActionDown(leftAction))
dir.X -= 1;
if (IsActionDown(rightAction))
dir.X += 1;
if (IsActionDown(upAction))
dir.Y -= 1;
if (IsActionDown(downAction))
dir.Y += 1;
return dir == Vector2.Zero ? Vector2.Zero : Vector2.Normalize(dir);
}
public abstract bool IsKeyboardKeyDown(KeyboardKey key); public abstract bool IsKeyboardKeyDown(KeyboardKey key);
public abstract bool KeyboardKeyJustPressed(KeyboardKey key); public abstract bool KeyboardKeyJustPressed(KeyboardKey key);
public abstract bool KeyboardKeyJustReleased(KeyboardKey key); public abstract bool KeyboardKeyJustReleased(KeyboardKey key);
public abstract Vector2 GetInputDirection(KeyboardKey leftKey, KeyboardKey rightKey, KeyboardKey upKey, KeyboardKey downKey);
public abstract Vector2 GetInputDirection(string leftAction, string rightAction, string upAction, string downAction);
public abstract int GetCharPressed(); public abstract int GetCharPressed();
public abstract bool IsActionDown(string action);
public abstract bool IsActionPressed(string action);
public abstract bool IsActionReleased(string action);
public abstract bool IsMouseButtonDown(MouseButton button); public abstract bool IsMouseButtonDown(MouseButton button);
public abstract float GetMouseWheelMovement(); public abstract float GetMouseWheelMovement();
public abstract void SetMousePosition(Vector2 position); public abstract void SetMousePosition(Vector2 position);
@@ -46,48 +111,47 @@ namespace Voile.Input
public void SetAsHandled() => _handled = true; public void SetAsHandled() => _handled = true;
public void AddInputMapping(string actionName, IEnumerable<InputAction> inputActions) public void AddInputMapping(string actionName, IEnumerable<IInputAction> inputActions)
{ {
inputMappings.Add(actionName, inputActions.ToList()); inputMappings.Add(actionName, inputActions.ToList());
} }
private void CreateDefaultMappings() private void CreateDefaultMappings()
{ {
AddInputMapping("up", new InputAction[] { AddInputMapping("up", [
new KeyInputAction(KeyboardKey.W), new KeyInputAction(KeyboardKey.W),
new KeyInputAction(KeyboardKey.Up), new KeyInputAction(KeyboardKey.Up),
}); ]);
AddInputMapping("down", new InputAction[] { AddInputMapping("down", [
new KeyInputAction(KeyboardKey.S), new KeyInputAction(KeyboardKey.S),
new KeyInputAction(KeyboardKey.Down), new KeyInputAction(KeyboardKey.Down),
}); ]);
AddInputMapping("left", new InputAction[] { AddInputMapping("left", [
new KeyInputAction(KeyboardKey.A), new KeyInputAction(KeyboardKey.A),
new KeyInputAction(KeyboardKey.Left), new KeyInputAction(KeyboardKey.Left),
}); ]);
AddInputMapping("right", new InputAction[] { AddInputMapping("right", [
new KeyInputAction(KeyboardKey.D), new KeyInputAction(KeyboardKey.D),
new KeyInputAction(KeyboardKey.Right), new KeyInputAction(KeyboardKey.Right),
}); ]);
} }
protected bool TryGetInputMappings(string forAction, [NotNullWhen(true)] out IEnumerable<InputAction>? inputActions) protected bool TryGetInputMappings(string forAction, [NotNullWhen(true)] out List<IInputAction>? inputActions)
{ {
var contains = inputMappings.ContainsKey(forAction);
inputActions = null; inputActions = null;
if (!contains) if (inputMappings.TryGetValue(forAction, out var actions))
{ {
_logger.Error($"The action \"{forAction}\" is not present in the input mappings!"); inputActions = actions;
return false; return true;
} }
inputActions = inputMappings[forAction]; _logger.Error($"The action \"{forAction}\" is not present in the input mappings!");
return true; return false;
} }
protected static Dictionary<string, List<InputAction>> inputMappings = new(); protected static Dictionary<string, List<IInputAction>> inputMappings = new();
private bool _handled; private bool _handled;
private Logger _logger = new(nameof(InputSystem)); private Logger _logger = new(nameof(InputSystem));
} }

View File

@@ -13,39 +13,6 @@ namespace Voile.Input
return Raylib.GetCharPressed(); return Raylib.GetCharPressed();
} }
public override Vector2 GetInputDirection(KeyboardKey leftKey, KeyboardKey rightKey, KeyboardKey upKey, KeyboardKey downKey)
{
Vector2 dir = Vector2.Zero;
if (IsKeyboardKeyDown(leftKey))
dir += new Vector2(-1, 0);
if (IsKeyboardKeyDown(rightKey))
dir += new Vector2(1, 0);
if (IsKeyboardKeyDown(upKey))
dir += new Vector2(0, -1);
if (IsKeyboardKeyDown(downKey))
dir += new Vector2(0, 1);
return dir == Vector2.Zero ? Vector2.Zero : Vector2.Normalize(dir);
}
public override Vector2 GetInputDirection(string leftAction, string rightAction, string upAction, string downAction)
{
Vector2 dir = Vector2.Zero;
if (IsActionDown(leftAction))
dir += new Vector2(-1, 0);
if (IsActionDown(rightAction))
dir += new Vector2(1, 0);
if (IsActionDown(upAction))
dir += new Vector2(0, -1);
if (IsActionDown(downAction))
dir += new Vector2(0, 1);
return dir == Vector2.Zero ? Vector2.Zero : Vector2.Normalize(dir);
}
public override Vector2 GetMousePosition() public override Vector2 GetMousePosition()
{ {
return Raylib.GetMousePosition(); return Raylib.GetMousePosition();
@@ -61,45 +28,6 @@ namespace Voile.Input
Raylib.HideCursor(); Raylib.HideCursor();
} }
public override bool IsActionDown(string action)
{
IEnumerable<InputAction>? mappings;
if (TryGetInputMappings(action, out mappings))
{
foreach (InputAction inputAction in mappings)
if (inputAction.IsDown(this)) return true;
}
return false;
}
public override bool IsActionPressed(string action)
{
IEnumerable<InputAction>? mappings;
if (TryGetInputMappings(action, out mappings))
{
foreach (InputAction inputAction in mappings)
if (inputAction.IsPressed(this)) return true;
}
return false;
}
public override bool IsActionReleased(string action)
{
IEnumerable<InputAction>? mappings;
if (TryGetInputMappings(action, out mappings))
{
foreach (InputAction inputAction in mappings)
if (inputAction.IsReleased(this)) return true;
}
return false;
}
public override bool IsKeyboardKeyDown(KeyboardKey key) public override bool IsKeyboardKeyDown(KeyboardKey key)
{ {
Raylib_cs.KeyboardKey rayKey = (Raylib_cs.KeyboardKey)key; Raylib_cs.KeyboardKey rayKey = (Raylib_cs.KeyboardKey)key;

View File

@@ -39,10 +39,10 @@ namespace Voile.Rendering
ConfigFlags flags = 0; ConfigFlags flags = 0;
// MSAA // MSAA
flags |= settings.Msaa == Msaa.Msaa4x ? ConfigFlags.FLAG_MSAA_4X_HINT : 0; flags |= settings.Msaa == Msaa.Msaa4x ? ConfigFlags.Msaa4xHint : 0;
// VSync // VSync
flags |= settings.UseVSync ? ConfigFlags.FLAG_VSYNC_HINT : 0; flags |= settings.UseVSync ? ConfigFlags.VSyncHint : 0;
_fullscreen = settings.Fullscreen; _fullscreen = settings.Fullscreen;
@@ -59,13 +59,13 @@ namespace Voile.Rendering
public override void CreateWindow(WindowSettings windowSettings) public override void CreateWindow(WindowSettings windowSettings)
{ {
Raylib.SetTraceLogLevel(TraceLogLevel.LOG_NONE); Raylib.SetTraceLogLevel(TraceLogLevel.None);
_defaultWindowSettings = windowSettings; _defaultWindowSettings = windowSettings;
_windowSize = windowSettings.Size; _windowSize = windowSettings.Size;
ConfigFlags windowFlags = 0; ConfigFlags windowFlags = 0;
windowFlags |= windowSettings.Resizable ? ConfigFlags.FLAG_WINDOW_RESIZABLE : 0; windowFlags |= windowSettings.Resizable ? ConfigFlags.ResizableWindow : 0;
_windowTitle = windowSettings.Title; _windowTitle = windowSettings.Title;
@@ -180,10 +180,10 @@ namespace Voile.Rendering
{ {
Raylib.DrawRectanglePro(new Rectangle() Raylib.DrawRectanglePro(new Rectangle()
{ {
x = transformPosition.X, X = transformPosition.X,
y = transformPosition.Y, Y = transformPosition.Y,
width = size.X, Width = size.X,
height = size.Y Height = size.Y
}, transformPivot, transformRotation, VoileColorToRaylibColor(color)); }, transformPivot, transformRotation, VoileColorToRaylibColor(color));
} }
@@ -252,28 +252,17 @@ namespace Voile.Rendering
return Raylib.GetCurrentMonitor(); return Raylib.GetCurrentMonitor();
} }
private unsafe void LoadFont(Font font) private void LoadFont(Font font)
{ {
Raylib_cs.Font fontRay; Raylib_cs.Font fontRay;
ReadOnlySpan<char> ext = ".ttf"; // TODO: don't use a hardcoded extension. string ext = ".ttf"; // TODO: don't use a hardcoded extension.
Span<byte> extBytes = new byte[Encoding.Default.GetByteCount(ext) + 1]; int fontChars = 250; // TODO: control this dynamically to not load the entire font.
Encoding.Default.GetBytes(ext, extBytes);
int fontChars = 2048; // TODO: control this dynamically to not load the entire font.
unsafe fontRay = Raylib.LoadFontFromMemory(ext, font.Buffer, font.Size, null, fontChars);
{
fixed (byte* extP = extBytes)
{
fixed (byte* bufferP = font.Buffer)
{
fontRay = Raylib.LoadFontFromMemory((sbyte*)extP, bufferP, (int)font.BufferSize, font.Size, null, fontChars);
}
}
}
Raylib.GenTextureMipmaps(ref fontRay.texture); Raylib.GenTextureMipmaps(ref fontRay.Texture);
Raylib.SetTextureFilter(fontRay.texture, TextureFilter.TEXTURE_FILTER_BILINEAR); Raylib.SetTextureFilter(fontRay.Texture, TextureFilter.Bilinear);
_fontPool.Add(fontRay); _fontPool.Add(fontRay);
@@ -288,14 +277,15 @@ namespace Voile.Rendering
{ {
fixed (void* dataPtr = texture.Buffer) fixed (void* dataPtr = texture.Buffer)
{ {
image.data = dataPtr; image.Data = dataPtr;
} }
} }
image.width = texture.Width;
image.height = texture.Height; image.Width = texture.Width;
image.mipmaps = texture.Mipmaps; image.Height = texture.Height;
image.format = (PixelFormat)texture.Format; image.Mipmaps = texture.Mipmaps;
image.Format = (PixelFormat)texture.Format;
Texture2D rayTexture; Texture2D rayTexture;

View File

@@ -7,7 +7,7 @@ namespace Voile.Resources.DataReaders;
/// <summary> /// <summary>
/// Reads key/value data from a TOML file. /// Reads key/value data from a TOML file.
/// </summary> /// </summary>
public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValueGetter, IDisposable public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValueGetter
{ {
public string ExpectedHeader { get; private set; } = string.Empty; public string ExpectedHeader { get; private set; } = string.Empty;
public TomlDataReader(string expectedHeader) public TomlDataReader(string expectedHeader)
@@ -17,14 +17,7 @@ public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValue
public void Read(Stream data) public void Read(Stream data)
{ {
if (data is not FileStream fs) using (var reader = new StreamReader(data))
{
throw new ArgumentException("Toml data reader only supports file streams.");
}
_fs = fs;
using (var reader = new StreamReader(_fs))
{ {
_table = TOML.Parse(reader); _table = TOML.Parse(reader);
_valid = _table.HasKey(ExpectedHeader); _valid = _table.HasKey(ExpectedHeader);
@@ -231,12 +224,6 @@ public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValue
public bool Valid() => _valid; public bool Valid() => _valid;
public void Dispose()
{
_fs?.Dispose();
}
private TomlTable? _table; private TomlTable? _table;
private FileStream? _fs;
private bool _valid; private bool _valid;
} }

View File

@@ -1,4 +1,6 @@
using Voile.VFS;
namespace Voile.Resources; namespace Voile.Resources;
public class FontLoader : ResourceLoader<Font> public class FontLoader : ResourceLoader<Font>
@@ -11,9 +13,14 @@ public class FontLoader : ResourceLoader<Font>
protected override Font LoadResource(string path) 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); var result = new Font(path, fileBuffer);
result.BufferSize = fileBuffer.Length;
result.BufferSize = bytesRead;
return result; return result;
} }
} }

View File

@@ -24,7 +24,6 @@ namespace Voile.Resources
var resource = LoadResource(path); var resource = LoadResource(path);
var guid = Guid.NewGuid(); var guid = Guid.NewGuid();
var loadedResources = ResourceManager.LoadedResources; var loadedResources = ResourceManager.LoadedResources;
var oldResourceGuid = loadedResources.FirstOrDefault(loadedResource => loadedResource.Value.Path == path).Key; var oldResourceGuid = loadedResources.FirstOrDefault(loadedResource => loadedResource.Value.Path == path).Key;
@@ -46,6 +45,7 @@ namespace Voile.Resources
{ {
foreach (var loadedResource in ResourceManager.LoadedResources) foreach (var loadedResource in ResourceManager.LoadedResources)
{ {
if (loadedResource.Value is not T) continue;
Load(loadedResource.Value.Path); Load(loadedResource.Value.Path);
} }
} }

View File

@@ -1,4 +1,5 @@
using StbVorbisSharp; using StbVorbisSharp;
using Voile.VFS;
namespace Voile.Resources namespace Voile.Resources
{ {
@@ -6,7 +7,7 @@ namespace Voile.Resources
{ {
public override IEnumerable<string> SupportedExtensions => new string[] public override IEnumerable<string> SupportedExtensions => new string[]
{ {
"ogg" ".ogg"
}; };
protected override Sound LoadResource(string path) protected override Sound LoadResource(string path)
@@ -14,9 +15,11 @@ namespace Voile.Resources
Vorbis vorbis; Vorbis vorbis;
Sound result; Sound result;
var fileBuffer = File.ReadAllBytes(path); using var stream = VirtualFileSystem.Read(path);
vorbis = Vorbis.FromMemory(fileBuffer); byte[] fileBuffer = new byte[stream.Length];
int bytesRead = stream.Read(fileBuffer, 0, fileBuffer.Length);
vorbis = Vorbis.FromMemory(fileBuffer);
vorbis.SubmitBuffer(); vorbis.SubmitBuffer();
if (vorbis.Decoded == 0) if (vorbis.Decoded == 0)
@@ -27,18 +30,9 @@ namespace Voile.Resources
var audioShort = vorbis.SongBuffer; var audioShort = vorbis.SongBuffer;
int length = vorbis.Decoded * vorbis.Channels; int length = vorbis.Decoded * vorbis.Channels;
byte[] audioData = new byte[length * 2];
for (int i = 0; i < length; i++) short[] audioData = new short[length];
{ Array.Copy(audioShort, audioData, length);
if (i * 2 >= audioData.Length) break;
var b1 = (byte)(audioShort[i] >> 8);
var b2 = (byte)(audioShort[i] & 256);
audioData[i * 2] = b2;
audioData[i * 2 + 1] = b1;
}
result = new Sound(path, audioData) result = new Sound(path, audioData)
{ {

View File

@@ -1,5 +1,6 @@
using Voile.Resources; using Voile.Resources;
using StbImageSharp; using StbImageSharp;
using Voile.VFS;
namespace Voile namespace Voile
{ {
@@ -18,7 +19,7 @@ namespace Voile
protected override Texture2d LoadResource(string path) protected override Texture2d LoadResource(string path)
{ {
ImageResult image; ImageResult image;
using (var stream = File.OpenRead(path)) using (var stream = VirtualFileSystem.Read(path))
{ {
image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha);
} }

View File

@@ -1,8 +1,8 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using Voile.Utils; using Voile.Utils;
using Voile.VFS;
namespace Voile.Resources namespace Voile.Resources
{ {
@@ -92,10 +92,7 @@ namespace Voile.Resources
T? resource = default; T? resource = default;
result = null; result = null;
var fullPath = Path.Combine(ResourceRoot, path); if (!VirtualFileSystem.FileExists(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))
{ {
_logger.Error($"File at \"{path}\" doesn't exist!"); _logger.Error($"File at \"{path}\" doesn't exist!");
return false; return false;
@@ -108,7 +105,7 @@ namespace Voile.Resources
return false; return false;
} }
var extension = Path.GetExtension(fullPath); var extension = Path.GetExtension(path);
var hasExtension = loader.SupportedExtensions.Any(ext => ext == extension); var hasExtension = loader.SupportedExtensions.Any(ext => ext == extension);
if (!hasExtension) if (!hasExtension)
@@ -116,7 +113,7 @@ namespace Voile.Resources
_logger.Error($"Extension {extension} is not supported!"); _logger.Error($"Extension {extension} is not supported!");
} }
var resourceGuid = loader.Load(fullPath); var resourceGuid = loader.Load(path);
if (!GetResource(resourceGuid, out T? loadedResource)) if (!GetResource(resourceGuid, out T? loadedResource))
{ {

View File

@@ -1,17 +1,17 @@
namespace Voile namespace Voile
{ {
/// <summary> /// <summary>
/// Represents raw audio samples. /// Represents raw audio samples in 16-bit PCM format.
/// </summary> /// </summary>
public class Sound : Resource public class Sound : Resource
{ {
public SoundChannel Channel { get; set; } public SoundChannel Channel { get; set; }
public int SampleRate { get; set; } public int SampleRate { get; set; }
public byte[]? Buffer { get; private set; } public short[]? Buffer { get; private set; }
public long BufferSize { get; set; } public long BufferSize { get; set; }
public Sound(string path, byte[] buffer) : base(path) public Sound(string path, short[] buffer) : base(path)
{ {
Buffer = buffer; Buffer = buffer;
} }

View File

@@ -1,7 +1,7 @@
using System.Numerics; using System.Numerics;
using Voile.Resources; using Voile.Resources;
using Voile.Resources.DataReaders; using Voile.Resources.DataReaders;
using Voile.Utils; using Voile.VFS;
namespace Voile.Systems.Particles; namespace Voile.Systems.Particles;
@@ -62,28 +62,26 @@ public class ParticleEmitterSettingsResourceLoader : ResourceLoader<ParticleEmit
{ {
var settings = new ParticleEmitterSettings(); var settings = new ParticleEmitterSettings();
using (var reader = new TomlDataReader("ParticleEmitterSettings")) var reader = new TomlDataReader("ParticleEmitterSettings");
{ reader.Read(VirtualFileSystem.Read(path));
reader.Read(File.Open(path, FileMode.Open));
settings.Local = reader.GetBool("Local", true); settings.Local = reader.GetBool("Local", true);
settings.MaxParticles = reader.GetInt("MaxParticles"); settings.MaxParticles = reader.GetInt("MaxParticles");
settings.EmitRadius = reader.GetInt("EmitRadius"); settings.EmitRadius = reader.GetInt("EmitRadius");
settings.LifeTime = reader.GetFloat("LifeTime", 1.0f); settings.LifeTime = reader.GetFloat("LifeTime", 1.0f);
settings.Explosiveness = reader.GetFloat("Explosiveness"); settings.Explosiveness = reader.GetFloat("Explosiveness");
settings.Direction = reader.GetVector2("Direction", Vector2.Zero); settings.Direction = reader.GetVector2("Direction", Vector2.Zero);
settings.LinearVelocity = reader.GetFloat("LinearVelocity", 980f); settings.LinearVelocity = reader.GetFloat("LinearVelocity", 980f);
settings.AngularVelocity = reader.GetFloat("AngularVelocity"); settings.AngularVelocity = reader.GetFloat("AngularVelocity");
settings.AngularVelocityRandom = reader.GetFloat("AngularVelocityRandom", 1.0f); settings.AngularVelocityRandom = reader.GetFloat("AngularVelocityRandom", 1.0f);
settings.LinearVelocityRandom = reader.GetFloat("LinearVelocityRandom", 1.0f); settings.LinearVelocityRandom = reader.GetFloat("LinearVelocityRandom", 1.0f);
settings.Gravity = reader.GetVector2("Gravity", Vector2.UnitY * 980f); settings.Gravity = reader.GetVector2("Gravity", Vector2.UnitY * 980f);
settings.ScaleBegin = reader.GetFloat("ScaleBegin"); settings.ScaleBegin = reader.GetFloat("ScaleBegin");
settings.ScaleEnd = reader.GetFloat("ScaleEnd"); settings.ScaleEnd = reader.GetFloat("ScaleEnd");
settings.ColorBegin = reader.GetColor("ColorBegin", Color.White); settings.ColorBegin = reader.GetColor("ColorBegin", Color.White);
settings.ColorEnd = reader.GetColor("ColorEnd", Color.Black); settings.ColorEnd = reader.GetColor("ColorEnd", Color.Black);
settings.LinearVelocityDamping = reader.GetFloat("LinearVelocityDamping", 1.0f); settings.LinearVelocityDamping = reader.GetFloat("LinearVelocityDamping", 1.0f);
settings.AngularVelocityDamping = reader.GetFloat("AngularVelocityDamping", 1.0f); settings.AngularVelocityDamping = reader.GetFloat("AngularVelocityDamping", 1.0f);
}
return new ParticleEmitterSettingsResource(path, settings); 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();
}

View File

@@ -12,18 +12,14 @@
<PackageReference Include="Silk.NET.WebGPU" Version="2.20.0" /> <PackageReference Include="Silk.NET.WebGPU" Version="2.20.0" />
<PackageReference Include="Silk.NET.WebGPU.Native.WGPU" Version="2.20.0" /> <PackageReference Include="Silk.NET.WebGPU.Native.WGPU" Version="2.20.0" />
<PackageReference Include="Silk.NET.Windowing" Version="2.20.0" /> <PackageReference Include="Silk.NET.Windowing" Version="2.20.0" />
<PackageReference Include="SoLoud.NET" Version="2020.2.7.1" />
<PackageReference Include="Tommy" Version="3.1.2" /> <PackageReference Include="Tommy" Version="3.1.2" />
<PackageReference Include="Voile.Fmod" Version="0.2.2.8" />
<PackageReference Include="ImGui.NET" Version="1.89.4" /> <PackageReference Include="ImGui.NET" Version="1.89.4" />
<PackageReference Include="Raylib-cs" Version="4.2.0.1" /> <PackageReference Include="Raylib-cs" Version="6.1.1" />
<PackageReference Include="SharpFont" Version="4.0.1" /> <PackageReference Include="SharpFont" Version="4.0.1" />
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta19" /> <PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta19" />
<PackageReference Include="StbImageSharp" Version="2.27.13" /> <PackageReference Include="StbImageSharp" Version="2.27.13" />
<PackageReference Include="StbVorbisSharp" Version="1.22.4" /> <PackageReference Include="StbVorbisSharp" Version="1.22.4" />
</ItemGroup> </ItemGroup>
<Target Name="BuildFmod" BeforeTargets="BeforeBuild">
<MSBuild Projects="../Voile.Fmod/Voile.Fmod.csproj" Targets="Restore;Build" />
</Target>
</Project> </Project>