diff --git a/CoreBoy.MonoGame/Config.cs b/CoreBoy.MonoGame/Config.cs new file mode 100644 index 0000000..93bc426 --- /dev/null +++ b/CoreBoy.MonoGame/Config.cs @@ -0,0 +1,84 @@ +using CoreBoy.controller; +using Microsoft.Xna.Framework.Input; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; + +namespace CoreBoy.MonoGame +{ + internal struct ConfigWindow + { + private const int GB_WIDTH = 160; + private const int GB_HEIGHT = 144; + + public int Width { get; set; } + public int Height { get; set; } + public double Scale { get; set; } + + public int WindowWidth => (int)Math.Clamp(Width * Scale, GB_WIDTH, 3840); + public int WindowHeight => (int)Math.Clamp(Height * Scale, GB_HEIGHT, 2160); + } + + internal struct ConfigKeymap + { + public string A { get; set; } + public string B { get; set; } + public string Left { get; set; } + public string Right { get; set; } + public string Up { get; set; } + public string Down { get; set; } + public string Start { get; set; } + public string Select { get; set; } + + private static (Button button, Keys key) GetButtonKeyPair(Button button, string keyStr, Keys fallbackKey) + { + var canParse = Enum.TryParse(keyStr, out var key); + if (!canParse) + { + key = fallbackKey; + } + + return (button, key); + } + + internal IReadOnlyDictionary GetButtonKeyMap() + { + return new[] + { + GetButtonKeyPair(Button.A, A, Keys.Z), + GetButtonKeyPair(Button.B, B, Keys.X), + GetButtonKeyPair(Button.Left, Left, Keys.Left), + GetButtonKeyPair(Button.Right, Right, Keys.Right), + GetButtonKeyPair(Button.Down, Down, Keys.Down), + GetButtonKeyPair(Button.Up, Up, Keys.Up), + GetButtonKeyPair(Button.Start, Start, Keys.Enter), + GetButtonKeyPair(Button.Select, Select, Keys.Space), + }.ToDictionary(k => k.button, v => v.key); + } + } + + internal sealed class Config + { + public string Game { get; set; } + public ConfigWindow Window { get; set; } + public ConfigKeymap Keymap { get; set; } + + internal static Config Load() + { + var filePath = Path.Combine(Environment.CurrentDirectory, "Config.json"); + var json = File.ReadAllText(filePath); + + var options = new JsonSerializerOptions + { + AllowTrailingCommas = true, + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip + }; + + var config = JsonSerializer.Deserialize(json, options); + return config; + } + } +} diff --git a/CoreBoy.MonoGame/Config.json b/CoreBoy.MonoGame/Config.json new file mode 100644 index 0000000..109b894 --- /dev/null +++ b/CoreBoy.MonoGame/Config.json @@ -0,0 +1,21 @@ +{ + // Game file located in ./Games/ + "game": "game.gb", + // Window size at startup + "window": { + "width": 160, + "height": 144, + "scale": 4 + }, + // Game Boy buttons mapped to keyboard buttons + "keymap": { + "a": "Z", + "b": "X", + "left": "Left", + "right": "Right", + "up": "Up", + "down": "Down", + "start": "Enter", + "select": "Space" + } +} \ No newline at end of file diff --git a/CoreBoy.MonoGame/CoreBoy.MonoGame.csproj b/CoreBoy.MonoGame/CoreBoy.MonoGame.csproj new file mode 100644 index 0000000..09f597f --- /dev/null +++ b/CoreBoy.MonoGame/CoreBoy.MonoGame.csproj @@ -0,0 +1,31 @@ + + + + WinExe + netcoreapp3.1 + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + diff --git a/CoreBoy.MonoGame/MonoGameEmulatorSurface.cs b/CoreBoy.MonoGame/MonoGameEmulatorSurface.cs new file mode 100644 index 0000000..3b131d7 --- /dev/null +++ b/CoreBoy.MonoGame/MonoGameEmulatorSurface.cs @@ -0,0 +1,141 @@ +using CoreBoy.controller; +using CoreBoy.gui; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace CoreBoy.MonoGame +{ + internal class MonoGameEmulatorSurface : Game, IController + { + private readonly Config _config; + private readonly Emulator _emulator; + private readonly GameboyOptions _gameboyOptions; + private readonly List _downKeys = new List(); + private readonly IReadOnlyDictionary _buttonKeyMap; + private readonly object _updateLock = new object(); + private readonly GraphicsDeviceManager _graphics; + private readonly CancellationTokenSource _cancellation; + + private Texture2D _currentFrame; + private SpriteBatch _spriteBatch; + private IButtonListener _listener; + + internal MonoGameEmulatorSurface() + { + Content.RootDirectory = "Content"; + _graphics = new GraphicsDeviceManager(this); + IsMouseVisible = true; + Window.AllowUserResizing = true; + + _config = Config.Load(); + _buttonKeyMap = _config.Keymap.GetButtonKeyMap(); + + _graphics.PreferredBackBufferWidth = _config.Window.WindowWidth; + _graphics.PreferredBackBufferHeight = _config.Window.WindowHeight; + _graphics.ApplyChanges(); + + _cancellation = new CancellationTokenSource(); + _gameboyOptions = new GameboyOptions(); + _emulator = new Emulator(_gameboyOptions); + + Exiting += Game_Exiting; + } + + protected override void Initialize() + { + _emulator.Controller = this; + _emulator.Display.OnFrameProduced += UpdateDisplay; + + try + { + _gameboyOptions.Rom = Path.Combine(Environment.CurrentDirectory, "Games", _config.Game); + _emulator.Run(_cancellation.Token); + } + catch (Exception ex) + { + Console.WriteLine("Failed to start emulator. " + ex.Message); + Exit(); + } + + base.Initialize(); + } + + protected override void LoadContent() + { + _spriteBatch = new SpriteBatch(GraphicsDevice); + } + + protected override void Update(GameTime gameTime) + { + var kState = Keyboard.GetState(); + + if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || kState.IsKeyDown(Keys.Escape)) + Exit(); + + foreach (var buttonKey in _buttonKeyMap) + { + var button = buttonKey.Key; + var key = buttonKey.Value; + if (!_downKeys.Contains(key) && kState.IsKeyDown(key)) + { + _downKeys.Add(key); + _listener.OnButtonPress(button); + } + else if (_downKeys.Contains(key) && kState.IsKeyUp(key)) + { + _downKeys.Remove(key); + _listener.OnButtonRelease(button); + } + } + + + base.Update(gameTime); + } + + protected override void Draw(GameTime gameTime) + { + if (_currentFrame != null) + { + GraphicsDevice.Clear(Color.Black); + + _spriteBatch.Begin(samplerState: SamplerState.PointClamp); + _spriteBatch.Draw(_currentFrame, new Rectangle(0, 0, Window.ClientBounds.Width, Window.ClientBounds.Height), Color.White); + _spriteBatch.End(); + } + + base.Draw(gameTime); + } + + private void Game_Exiting(object sender, EventArgs e) + { + _emulator.Stop(_cancellation); + _cancellation.Cancel(); + } + + private void UpdateDisplay(object _, byte[] frame) + { + if (!Monitor.TryEnter(_updateLock)) return; + + try + { + using var memoryStream = new MemoryStream(frame); + _currentFrame = Texture2D.FromStream(GraphicsDevice, memoryStream); + } + catch (Exception ex) + { + Console.WriteLine("Received error reading frame " + ex.Message); + } + finally + { + Monitor.Exit(_updateLock); + } + } + + public void SetButtonListener(IButtonListener listener) => _listener = listener; + } +} diff --git a/CoreBoy.MonoGame/Program.cs b/CoreBoy.MonoGame/Program.cs new file mode 100644 index 0000000..e270c41 --- /dev/null +++ b/CoreBoy.MonoGame/Program.cs @@ -0,0 +1,14 @@ +using System; + +namespace CoreBoy.MonoGame +{ + public static class Program + { + [STAThread] + static void Main() + { + using var game = new MonoGameEmulatorSurface(); + game.Run(); + } + } +} diff --git a/CoreBoy.sln b/CoreBoy.sln index 44191b6..bd24ab2 100644 --- a/CoreBoy.sln +++ b/CoreBoy.sln @@ -9,9 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreBoy.Test.Unit", "CoreBo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreBoy.Test.Integration", "CoreBoy.Test.Integration\CoreBoy.Test.Integration.csproj", "{EAB44CCA-A65A-48CC-8ABB-096205788883}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreBoy.Windows", "CoreBoy.Windows\CoreBoy.Windows.csproj", "{66AA473C-2F18-4A72-8F96-5F5A3E05F706}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreBoy.Windows", "CoreBoy.Windows\CoreBoy.Windows.csproj", "{66AA473C-2F18-4A72-8F96-5F5A3E05F706}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreBoy.Cli", "CoreBoy.Cli\CoreBoy.Cli.csproj", "{FB968AD4-425E-4BE5-8467-0DAD35591C62}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreBoy.Cli", "CoreBoy.Cli\CoreBoy.Cli.csproj", "{FB968AD4-425E-4BE5-8467-0DAD35591C62}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreBoy.MonoGame", "CoreBoy.MonoGame\CoreBoy.MonoGame.csproj", "{BCF1BF6D-E340-400A-B29C-1FFDC14E7D6B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,6 +41,10 @@ Global {FB968AD4-425E-4BE5-8467-0DAD35591C62}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB968AD4-425E-4BE5-8467-0DAD35591C62}.Release|Any CPU.ActiveCfg = Release|Any CPU {FB968AD4-425E-4BE5-8467-0DAD35591C62}.Release|Any CPU.Build.0 = Release|Any CPU + {BCF1BF6D-E340-400A-B29C-1FFDC14E7D6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BCF1BF6D-E340-400A-B29C-1FFDC14E7D6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BCF1BF6D-E340-400A-B29C-1FFDC14E7D6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BCF1BF6D-E340-400A-B29C-1FFDC14E7D6B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CoreBoy/CoreBoy.csproj b/CoreBoy/CoreBoy.csproj index 41b3030..ad2b2dd 100644 --- a/CoreBoy/CoreBoy.csproj +++ b/CoreBoy/CoreBoy.csproj @@ -1,4 +1,4 @@ - + Library @@ -7,7 +7,6 @@ CoreBoy CoreBoy - true diff --git a/CoreBoy/gui/WinSound.cs b/CoreBoy/gui/WinSound.cs index 1f5254f..79376e7 100644 --- a/CoreBoy/gui/WinSound.cs +++ b/CoreBoy/gui/WinSound.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using CoreBoy.sound; using NAudio.Wave; using NAudio.Wave.SampleProviders; @@ -23,7 +24,11 @@ public WinSound() public void Start() { - _engine = new AudioPlaybackEngine(SampleRate, 2); + // This is Windows-only for now. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _engine = new AudioPlaybackEngine(SampleRate, 2); + } } public void Stop() @@ -39,7 +44,7 @@ public void Play(int left, int right) return; } - //Beep((uint)left, 5);*/ + //Beep((uint)left, 5);*/ } }