From acc023745caf0d656997eeecd2134b5b999ff37d Mon Sep 17 00:00:00 2001 From: Christer C Date: Thu, 12 Oct 2023 21:25:06 +0200 Subject: [PATCH] Feature/gamepad c64 (#89) * Native Silk.Net Gamepad to control C64 Joystick. * Web WASM Gamepad to control C64 Joystick. --- .github/workflows/dotnet-push-package.yml | 2 +- .github/workflows/sonarscan-dotnet.yml | 2 +- .../ConfigUI/SilkNetImGuiC64Config.cs | 28 ++-- .../SilkNetImGuiLogsPanel.cs | 2 +- .../SilkNetImGuiMenu.cs | 1 + .../Highbyte.DotNet6502.App.SkiaWASM.csproj | 1 + .../Pages/Commodore64/C64ConfigUI.razor | 56 +++++--- .../Pages/Commodore64/C64Menu.razor | 20 +-- .../Pages/Index.razor.cs | 6 +- .../Program.cs | 2 + .../Skia/WasmHost.cs | 5 +- .../AspNetInputHandlerContext.cs | 56 +++++++- .../Commodore64/Input/C64AspNetGamepad.cs | 15 +++ .../Input/C64AspNetInputHandler.cs | 46 ++++++- .../Highbyte.DotNet6502.Impl.AspNet.csproj | 1 + .../Audio/C64NAudioAudioHandler.cs | 3 +- .../Commodore64/Input/C64SilkNetGamepad.cs | 16 +++ .../Input/C64SilkNetInputHandler.cs | 54 +++++++- .../SilkNetInputHandlerContext.cs | 125 ++++++++++++++++-- .../Config/C64KeyboardJoystickMap.cs | 15 +-- .../TimerAndPeripheral/C64Joystick.cs | 36 +++-- .../TimerAndPeripheral/C64Keyboard.cs | 64 ++++----- .../Commodore64/TimerAndPeripheral/Cia.cs | 2 +- 23 files changed, 435 insertions(+), 123 deletions(-) create mode 100644 src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetGamepad.cs create mode 100644 src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetGamepad.cs diff --git a/.github/workflows/dotnet-push-package.yml b/.github/workflows/dotnet-push-package.yml index ede7607d..39f2b553 100644 --- a/.github/workflows/dotnet-push-package.yml +++ b/.github/workflows/dotnet-push-package.yml @@ -16,7 +16,7 @@ env: # PROJECT_FILE6: "src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Highbyte.DotNet6502.Impl.SilkNet.csproj" # PROJECT_FILE7: "src/libraries/Highbyte.DotNet6502.Impl.Skia/Highbyte.DotNet6502.Impl.Skia.csproj" CONFIGURATION: "Release" - VERSION: "0.12.0" #Major.Minor[.Rev] + VERSION: "0.12.1" #Major.Minor[.Rev] VERSION_SUFFIX: "-alpha" PACKAGE_REPO: "https://nuget.pkg.github.com/highbyte/index.json" diff --git a/.github/workflows/sonarscan-dotnet.yml b/.github/workflows/sonarscan-dotnet.yml index dfe346d1..5773af77 100644 --- a/.github/workflows/sonarscan-dotnet.yml +++ b/.github/workflows/sonarscan-dotnet.yml @@ -32,7 +32,7 @@ jobs: # Runs a single command using the runners shell - name: SonarScanner for .NET 7 with pull request decoration support - uses: highbyte/sonarscan-dotnet@v2.2.4 + uses: highbyte/sonarscan-dotnet@v2.2.6 with: # The key of the SonarQube project sonarProjectKey: highbyte_dotnet-6502 diff --git a/src/apps/Highbyte.DotNet6502.App.SkiaNative/ConfigUI/SilkNetImGuiC64Config.cs b/src/apps/Highbyte.DotNet6502.App.SkiaNative/ConfigUI/SilkNetImGuiC64Config.cs index 2a7e875b..4dbc83c4 100644 --- a/src/apps/Highbyte.DotNet6502.App.SkiaNative/ConfigUI/SilkNetImGuiC64Config.cs +++ b/src/apps/Highbyte.DotNet6502.App.SkiaNative/ConfigUI/SilkNetImGuiC64Config.cs @@ -1,7 +1,7 @@ -using System.Linq; using System.Numerics; +using Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Input; using Highbyte.DotNet6502.Systems.Commodore64.Config; -using static Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral.C64Joystick; +using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; namespace Highbyte.DotNet6502.App.SkiaNative.ConfigUI; @@ -20,13 +20,14 @@ public class SilkNetImGuiC64Config public bool Cancel { get; set; } private bool _isValidConfig = true; + public bool IsValidConfig => _isValidConfig; private List? _validationErrors; private const int POS_X = 50; private const int POS_Y = 50; private const int WIDTH = 400; - private const int HEIGHT = 380; + private const int HEIGHT = 550; //private static Vector4 s_informationColor = new Vector4(1.0f, 1.0f, 1.0f, 1.0f); private static Vector4 s_errorColor = new Vector4(1.0f, 0.0f, 0.0f, 1.0f); //private static Vector4 s_warningColor = new Vector4(0.5f, 0.8f, 0.8f, 1); @@ -91,15 +92,22 @@ public void PostOnRender() _config!.SetROM(C64Config.CHARGEN_ROM_NAME, _chargenRomFile); } - ImGui.Text("Keyboard joystick 2"); - var keyToJoystickMap = _config!.KeyboardJoystickMap; int joystick = 2; + ImGui.Text($"Joystick {joystick}"); + ImGui.BeginDisabled(disabled: true); + foreach (var mapKey in C64SilkNetGamepad.SilkNetGamePadToC64JoystickMap) + { + ImGui.LabelText($"{string.Join(",", mapKey.Key)}", $"{string.Join(",", mapKey.Value)}"); + } + ImGui.EndDisabled(); + + ImGui.Text($"Keyboard Joystick {joystick}"); + var keyToJoystickMap = _config!.KeyboardJoystickMap; ImGui.BeginDisabled(disabled: true); - ImGui.LabelText("Up", $"{string.Join(",", keyToJoystickMap.GetMappedKeysForJoystickAction(joystick, C64JoystickAction.Up))}"); - ImGui.LabelText("Down", $"{string.Join(",", keyToJoystickMap.GetMappedKeysForJoystickAction(joystick, C64JoystickAction.Down))}"); - ImGui.LabelText("Left", $"{string.Join(",", keyToJoystickMap.GetMappedKeysForJoystickAction(joystick, C64JoystickAction.Left))}"); - ImGui.LabelText("Right", $"{string.Join(",", keyToJoystickMap.GetMappedKeysForJoystickAction(joystick, C64JoystickAction.Right))}"); - ImGui.LabelText("Fire", $"{string.Join(",", keyToJoystickMap.GetMappedKeysForJoystickAction(joystick, C64JoystickAction.Fire)).Replace(" ", "SPACE")}"); + foreach (var mapKey in keyToJoystickMap.GetMap(joystick)) + { + ImGui.LabelText($"{string.Join(",", mapKey.Key)}", $"{string.Join(",", mapKey.Value)}"); + } ImGui.EndDisabled(); if (_config!.IsDirty) diff --git a/src/apps/Highbyte.DotNet6502.App.SkiaNative/SilkNetImGuiLogsPanel.cs b/src/apps/Highbyte.DotNet6502.App.SkiaNative/SilkNetImGuiLogsPanel.cs index 0c7ed6e5..8f48c24f 100644 --- a/src/apps/Highbyte.DotNet6502.App.SkiaNative/SilkNetImGuiLogsPanel.cs +++ b/src/apps/Highbyte.DotNet6502.App.SkiaNative/SilkNetImGuiLogsPanel.cs @@ -14,7 +14,7 @@ public class SilkNetImGuiLogsPanel : ISilkNetImGuiWindow private const int POS_X = 300; private const int POS_Y = 2; - private const int WIDTH = 800; + private const int WIDTH = 950; private const int HEIGHT = 600; private static Vector4 s_labelColor = new Vector4(0.7f, 0.7f, 0.7f, 1.0f); private static Vector4 s_informationColor = new Vector4(1.0f, 1.0f, 1.0f, 1.0f); diff --git a/src/apps/Highbyte.DotNet6502.App.SkiaNative/SilkNetImGuiMenu.cs b/src/apps/Highbyte.DotNet6502.App.SkiaNative/SilkNetImGuiMenu.cs index f912a6d1..6ab80888 100644 --- a/src/apps/Highbyte.DotNet6502.App.SkiaNative/SilkNetImGuiMenu.cs +++ b/src/apps/Highbyte.DotNet6502.App.SkiaNative/SilkNetImGuiMenu.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Numerics; using Highbyte.DotNet6502.App.SkiaNative.ConfigUI; +using Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Input; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems.Commodore64.Config; diff --git a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Highbyte.DotNet6502.App.SkiaWASM.csproj b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Highbyte.DotNet6502.App.SkiaWASM.csproj index 17b93801..989669e9 100644 --- a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Highbyte.DotNet6502.App.SkiaWASM.csproj +++ b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Highbyte.DotNet6502.App.SkiaWASM.csproj @@ -37,6 +37,7 @@ + diff --git a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Commodore64/C64ConfigUI.razor b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Commodore64/C64ConfigUI.razor index f37f5b59..ff49a2e2 100644 --- a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Commodore64/C64ConfigUI.razor +++ b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Commodore64/C64ConfigUI.razor @@ -1,6 +1,8 @@ @using Highbyte.DotNet6502.App.SkiaWASM.Skia +@using Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input; @using Highbyte.DotNet6502.Systems; @using Highbyte.DotNet6502.Systems.Commodore64.Config +@using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; @using static Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral.C64Joystick; @using static Highbyte.DotNet6502.App.SkiaWASM.Pages.Index @@ -65,10 +67,31 @@ else -

Joystick #2 keyboard map

+ +

Joystick

+@{ + var gampadToJoystickMap = C64AspNetGamepad.AspNetGamePadToC64JoystickMap; + int joystick = 2; +} +
+
+
Joystick @joystick action
+
Gampad button
+
+ @{ + foreach (var mapKey in gampadToJoystickMap) + { +
+
@string.Join(",", mapKey.Value)
+
@string.Join(",", mapKey.Key)
+
+ } + } +
+ +

Joystick keyboard

@{ var keyToJoystickMap = C64Config.KeyboardJoystickMap; - var joystick = 2; }
@@ -76,25 +99,18 @@ else
-
Up:
-
@string.Join(",", keyToJoystickMap.GetMappedKeysForJoystickAction(joystick, C64JoystickAction.Up))
-
-
-
Down:
-
@string.Join(",", keyToJoystickMap.GetMappedKeysForJoystickAction(joystick, C64JoystickAction.Down))
-
-
-
Left:
-
@string.Join(",", keyToJoystickMap.GetMappedKeysForJoystickAction(joystick, C64JoystickAction.Left))
-
-
-
Right:
-
@string.Join(",", keyToJoystickMap.GetMappedKeysForJoystickAction(joystick, C64JoystickAction.Right))
-
-
-
Fire:
-
@string.Join(",", keyToJoystickMap.GetMappedKeysForJoystickAction(joystick, C64JoystickAction.Fire)).Replace(" ", "SPACE")
+
Joystick @joystick action
+
Key
+ @{ + foreach (var mapKey in keyToJoystickMap.GetMap(joystick)) + { +
+
@string.Join(",", mapKey.Value)
+
@string.Join(",", mapKey.Key)
+
+ } + }

diff --git a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Commodore64/C64Menu.razor b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Commodore64/C64Menu.razor index 3dd601ed..f7c2259b 100644 --- a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Commodore64/C64Menu.razor +++ b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Commodore64/C64Menu.razor @@ -65,16 +65,21 @@ private string SYSTEM_NAME = C64.SystemName; // Note: The current config object (reference) is stored in this variable so that the UI can bind it's properties (not possible to use async call to _systemList.GetSystemConfig() in property ) - private C64Config _c64Config => (C64Config)Parent.SystemConfig; + private C64Config? _c64Config => Parent.SystemConfig as C64Config; + private bool JoystickKeyboardEnabled { get { + if (_c64Config == null) + return false; return _c64Config?.KeyboardJoystickEnabled ?? false; } set { + if (_c64Config == null) + return; _c64Config.KeyboardJoystickEnabled = value; if (Parent.CurrentEmulatorState != EmulatorState.Uninitialized) { @@ -316,13 +321,12 @@ // Send "list" + Enter to the keyboard buffer to immediately list the loaded program var c64Keyboard = c64.Cia.Keyboard; - // TODO after implementing C64 keyboard matrix scanning - // TODO: Bypass keyboard matrix scanning and send directly to keyboard buffer? - // c64Keyboard.KeyPressed(Petscii.CharToPetscii['l']); - // c64Keyboard.KeyPressed(Petscii.CharToPetscii['i']); - // c64Keyboard.KeyPressed(Petscii.CharToPetscii['s']); - // c64Keyboard.KeyPressed(Petscii.CharToPetscii['t']); - // c64Keyboard.KeyPressed(Petscii.CharToPetscii[(char)13]); + // Bypass keyboard matrix scanning and send directly to keyboard buffer? + c64Keyboard.InsertPetsciiCharIntoBuffer(Petscii.CharToPetscii['l']); + c64Keyboard.InsertPetsciiCharIntoBuffer(Petscii.CharToPetscii['i']); + c64Keyboard.InsertPetsciiCharIntoBuffer(Petscii.CharToPetscii['s']); + c64Keyboard.InsertPetsciiCharIntoBuffer(Petscii.CharToPetscii['t']); + c64Keyboard.InsertPetsciiCharIntoBuffer(Petscii.CharToPetscii[(char)13]); await Parent.OnStart(new()); } diff --git a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Index.razor.cs b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Index.razor.cs index 1c560f5b..d98c37b0 100644 --- a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Index.razor.cs +++ b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Pages/Index.razor.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Components.Forms; using Blazored.LocalStorage; using Highbyte.DotNet6502.Logging.Console; +using Toolbelt.Blazor.Gamepad; namespace Highbyte.DotNet6502.App.SkiaWASM.Pages; @@ -121,6 +122,9 @@ private double Scale [Inject] public DotNet6502ConsoleLoggerConfiguration LoggerConfiguration { get; set; } + [Inject] + public GamepadList GamepadList { get; set; } + private ILogger _logger; protected override async Task OnInitializedAsync() @@ -268,7 +272,7 @@ protected async void OnPaintSurface(SKPaintGLSurfaceEventArgs e) if (!_wasmHost.Initialized) { - await _wasmHost.Init(e.Surface.Canvas, grContext, _audioContext, Js!); + await _wasmHost.Init(e.Surface.Canvas, grContext, _audioContext, GamepadList, Js!); } //_emulatorRenderer.SetSize(e.Info.Width, e.Info.Height); diff --git a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Program.cs b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Program.cs index c2f3b4bf..8430beab 100644 --- a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Program.cs @@ -3,6 +3,7 @@ using Highbyte.DotNet6502.App.SkiaWASM; using Highbyte.DotNet6502.Logging.Console; using Blazored.LocalStorage; +using Toolbelt.Blazor.Extensions.DependencyInjection; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); @@ -10,6 +11,7 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddBlazoredModal(); builder.Services.AddBlazoredLocalStorage(); +builder.Services.AddGamepadList(); builder.Logging.ClearProviders(); builder.Logging.AddDotNet6502Console(); diff --git a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Skia/WasmHost.cs b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Skia/WasmHost.cs index def0ad97..162bc342 100644 --- a/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Skia/WasmHost.cs +++ b/src/apps/Highbyte.DotNet6502.App.SkiaWASM/Skia/WasmHost.cs @@ -4,6 +4,7 @@ using Highbyte.DotNet6502.Impl.Skia; using Highbyte.DotNet6502.Monitor; using Highbyte.DotNet6502.Systems; +using Toolbelt.Blazor.Gamepad; namespace Highbyte.DotNet6502.App.SkiaWASM.Skia; @@ -97,13 +98,13 @@ public WasmHost( Initialized = false; } - public async Task Init(SKCanvas canvas, GRContext grContext, AudioContextSync audioContext, IJSRuntime jsRuntime) + public async Task Init(SKCanvas canvas, GRContext grContext, AudioContextSync audioContext, GamepadList gamepadList, IJSRuntime jsRuntime) { _skCanvas = canvas; _grContext = grContext; _skiaRenderContext = new SkiaRenderContext(GetCanvas, GetGRContext); - InputHandlerContext = new AspNetInputHandlerContext(_loggerFactory); + InputHandlerContext = new AspNetInputHandlerContext(_loggerFactory, gamepadList); AudioHandlerContext = new WASMAudioHandlerContext(audioContext, jsRuntime, _initialMasterVolume); _systemList.InitContext(() => _skiaRenderContext, () => InputHandlerContext, () => AudioHandlerContext); diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/AspNetInputHandlerContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/AspNetInputHandlerContext.cs index 494e7ce3..9596690c 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/AspNetInputHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/AspNetInputHandlerContext.cs @@ -1,5 +1,7 @@ +using Highbyte.DotNet6502.Instructions; using Highbyte.DotNet6502.Systems; using Microsoft.Extensions.Logging; +using Toolbelt.Blazor.Gamepad; namespace Highbyte.DotNet6502.Impl.AspNet; @@ -7,18 +9,63 @@ public class AspNetInputHandlerContext : IInputHandlerContext { private readonly ILogger _logger; + // Keyboard public HashSet KeysDown = new(); - private bool _capsLockKeyDownCaptured; private bool _capsLockOn; - public AspNetInputHandlerContext(ILoggerFactory loggerFactory) + // Gamepad + private readonly GamepadList _gamepadList; + private readonly System.Timers.Timer _gamepadUpdateTimer = new System.Timers.Timer(50) { Enabled = true }; + private Gamepad? _currentGamepad; + public HashSet GamepadButtonsDown = new(); + + public AspNetInputHandlerContext(ILoggerFactory loggerFactory, GamepadList gamepadList) { _logger = loggerFactory.CreateLogger(); + _gamepadList = gamepadList; } public void Init() { + _gamepadUpdateTimer.Elapsed += GamepadUpdateTimer_Elapsed; + } + + private async void GamepadUpdateTimer_Elapsed(object sender, EventArgs args) + { + try + { + var gamepads = await _gamepadList.GetGamepadsAsync(); + + var gamePad = gamepads.LastOrDefault(); + if (gamePad != _currentGamepad) + { + _currentGamepad = gamePad; + if (_currentGamepad != null && _currentGamepad.Connected) + _logger.LogInformation($"Current gamepad changed to: {_currentGamepad.Id} ({_currentGamepad.Index})"); + else + _logger.LogInformation($"Gamepad disconnected"); + } + + GamepadButtonsDown.Clear(); + + if (_currentGamepad != null && _currentGamepad.Connected) + { + for (int buttonIndex = 0; buttonIndex < _currentGamepad.Buttons.Count; buttonIndex++) + { + var button = _currentGamepad.Buttons[buttonIndex]; + if (!button.Pressed) + continue; + GamepadButtonsDown.Add(buttonIndex); + _logger.LogInformation($"Gamepad button pressed: {buttonIndex} ({button.Pressed})"); + } + } + } + catch (Exception e) + { + System.Diagnostics.Debug.WriteLine(e.ToString()); + throw; + } } public void KeyUp(KeyboardEventArgs e) @@ -66,4 +113,9 @@ public bool GetCapsLockState() public void Cleanup() { } + + public void Dispose() + { + _gamepadUpdateTimer.Dispose(); + } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetGamepad.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetGamepad.cs new file mode 100644 index 00000000..9892d7d3 --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetGamepad.cs @@ -0,0 +1,15 @@ +using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; + +namespace Highbyte.DotNet6502.Impl.AspNet.Commodore64.Input; + +public static class C64AspNetGamepad +{ + public static Dictionary AspNetGamePadToC64JoystickMap = new() + { + { new[] { 0 }, new[] { C64JoystickAction.Fire } }, + { new[] { 12 }, new[] { C64JoystickAction.Up} }, + { new[] { 13 }, new[] { C64JoystickAction.Down } }, + { new[] { 14 }, new[] { C64JoystickAction.Left } }, + { new[] { 15 }, new[] { C64JoystickAction.Right } }, + }; +} diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputHandler.cs index fac57b85..094b327e 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Commodore64/Input/C64AspNetInputHandler.cs @@ -102,11 +102,51 @@ private List GetC64KeysFromAspNetKeys(HashSet keysDown, out bool private void CaptureJoystick(C64 c64) { - //var joystick = c64.Cia.Joystick; - // TODO: Capture joystick input via Javascript (connected USB controller?) - // For now there is option to control C64 joystick via keyboard (see C64Keyboard class) + var c64JoystickActions = GetC64JoystickActionsFromAspNetGamepad(_inputHandlerContext!.GamepadButtonsDown); + c64.Cia.Joystick.SetJoystick2Actions(c64JoystickActions); } + private HashSet GetC64JoystickActionsFromAspNetGamepad(HashSet gamepadButtonsDown) + { + var c64JoystickActions = new HashSet(); + var foundMappings = new List(); + var map = C64AspNetGamepad.AspNetGamePadToC64JoystickMap; + foreach (var mapKeys in map.Keys) + { + int matchCount = 0; + foreach (var mapKeysKey in mapKeys) + { + if (gamepadButtonsDown.Contains(mapKeysKey)) + matchCount++; + } + if (matchCount == mapKeys.Length) + { + // Remove any other mappings found that contains any of the Gamepad buttons in this mapping. + for (int i = foundMappings.Count - 1; i >= 0; i--) + { + var currentlyFoundMapKeys = foundMappings[i]; + if (currentlyFoundMapKeys.Any(x => mapKeys.Contains(x))) + { + foundMappings.RemoveAt(i); + } + } + foundMappings.Add(mapKeys); + } + } + + foreach (var mapKeys in foundMappings) + { + var c64Keys = map[mapKeys]; + foreach (var c64Key in c64Keys) + { + if (!c64JoystickActions.Contains(c64Key)) + c64JoystickActions.Add(c64Key); + } + } + return c64JoystickActions; + } + + public List GetStats() { List list = new(); diff --git a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Highbyte.DotNet6502.Impl.AspNet.csproj b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Highbyte.DotNet6502.Impl.AspNet.csproj index 9698a99a..6674a8ec 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Highbyte.DotNet6502.Impl.AspNet.csproj +++ b/src/libraries/Highbyte.DotNet6502.Impl.AspNet/Highbyte.DotNet6502.Impl.AspNet.csproj @@ -30,6 +30,7 @@ + diff --git a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioAudioHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioAudioHandler.cs index 8347941d..1256a42b 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioAudioHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.NAudio/Commodore64/Audio/C64NAudioAudioHandler.cs @@ -220,6 +220,7 @@ private void AddDebugMessage(string msg, int? voice = null, SidVoiceWaveForm? si formattedMsg = $"{msg}"; } - _logger.LogDebug(formattedMsg); + //_logger.LogDebug(formattedMsg); + _logger.LogTrace(formattedMsg); } } diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetGamepad.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetGamepad.cs new file mode 100644 index 00000000..034ae921 --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetGamepad.cs @@ -0,0 +1,16 @@ +using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; + +namespace Highbyte.DotNet6502.Impl.SilkNet.Commodore64.Input; + +public static class C64SilkNetGamepad +{ + + public static Dictionary SilkNetGamePadToC64JoystickMap = new() + { + { new[] { ButtonName.A }, new[] { C64JoystickAction.Fire } }, + { new[] { ButtonName.DPadUp }, new[] { C64JoystickAction.Up} }, + { new[] { ButtonName.DPadDown }, new[] { C64JoystickAction.Down } }, + { new[] { ButtonName.DPadLeft }, new[] { C64JoystickAction.Left } }, + { new[] { ButtonName.DPadRight }, new[] { C64JoystickAction.Right } }, + }; +} diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputHandler.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputHandler.cs index b686240c..eec557e9 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputHandler.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/Commodore64/Input/C64SilkNetInputHandler.cs @@ -10,6 +10,8 @@ public class C64SilkNetInputHandler : IInputHandler _stats = new(); private readonly C64SilkNetKeyboard _c64SilkNetKeyboard; + //private readonly C64SilkNetGamepad _c64SilkNetGamepad; + private readonly ILogger _logger; public C64SilkNetInputHandler(ILoggerFactory loggerFactory) @@ -26,6 +28,8 @@ public C64SilkNetInputHandler(ILoggerFactory loggerFactory) _logger.LogInformation($"KbLanguage: {languageName}"); _c64SilkNetKeyboard = new C64SilkNetKeyboard(languageName); + + //_c64SilkNetGamepad = new C64SilkNetGamepad(); } public void Init(C64 system, SilkNetInputHandlerContext inputHandlerContext) @@ -61,10 +65,10 @@ private List GetC64KeysFromSilkNetKeys(HashSet keysDown, out bool r { restoreKeyPressed = keysDown.Contains(Key.PageUp) ? true : false; capsLockOn = _inputHandlerContext!.GetCapsLockState(); - var c64KeysDown = new List(); var foundMappings = new List(); - foreach (var mapKeys in _c64SilkNetKeyboard.SilkNetToC64KeyMap.Keys) + var map = _c64SilkNetKeyboard.SilkNetToC64KeyMap; + foreach (var mapKeys in map.Keys) { int matchCount = 0; foreach (var mapKeysKey in mapKeys) @@ -89,7 +93,7 @@ private List GetC64KeysFromSilkNetKeys(HashSet keysDown, out bool r foreach (var mapKeys in foundMappings) { - var c64Keys = _c64SilkNetKeyboard.SilkNetToC64KeyMap[mapKeys]; + var c64Keys = map[mapKeys]; foreach (var c64Key in c64Keys) { if (!c64KeysDown.Contains(c64Key)) @@ -101,10 +105,48 @@ private List GetC64KeysFromSilkNetKeys(HashSet keysDown, out bool r private void CaptureJoystick(C64 c64) { - //var joystick = c64.Cia.Joystick; - // TODO: Capture joystick input via Silk.NET xbox controller? - // For now there is option to control C64 joystick via keyboard (see C64Keyboard class) + var c64JoystickActions = GetC64JoystickActionsFromSilkNetGamepad(_inputHandlerContext!.GamepadButtonsDown); + c64.Cia.Joystick.SetJoystick2Actions(c64JoystickActions); + } + private HashSet GetC64JoystickActionsFromSilkNetGamepad(HashSet gamepadButtonsDown) + { + var c64JoystickActions = new HashSet(); + var foundMappings = new List(); + var map = C64SilkNetGamepad.SilkNetGamePadToC64JoystickMap; + foreach (var mapKeys in map.Keys) + { + int matchCount = 0; + foreach (var mapKeysKey in mapKeys) + { + if (gamepadButtonsDown.Contains(mapKeysKey)) + matchCount++; + } + if (matchCount == mapKeys.Length) + { + // Remove any other mappings found that contains any of the Gamepad buttons in this mapping. + for (int i = foundMappings.Count - 1; i >= 0; i--) + { + var currentlyFoundMapKeys = foundMappings[i]; + if (currentlyFoundMapKeys.Any(x => mapKeys.Contains(x))) + { + foundMappings.RemoveAt(i); + } + } + foundMappings.Add(mapKeys); + } + } + + foreach (var mapKeys in foundMappings) + { + var c64Keys = map[mapKeys]; + foreach (var c64Key in c64Keys) + { + if (!c64JoystickActions.Contains(c64Key)) + c64JoystickActions.Add(c64Key); + } + } + return c64JoystickActions; } public List GetStats() diff --git a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetInputHandlerContext.cs b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetInputHandlerContext.cs index d5644036..8477d84a 100644 --- a/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetInputHandlerContext.cs +++ b/src/libraries/Highbyte.DotNet6502.Impl.SilkNet/SilkNetInputHandlerContext.cs @@ -1,6 +1,8 @@ +using System.Linq; using System.Runtime.InteropServices; using Highbyte.DotNet6502.Systems; using Microsoft.Extensions.Logging; +using Silk.NET.Input; using Silk.NET.SDL; namespace Highbyte.DotNet6502.Impl.SilkNet; @@ -11,14 +13,19 @@ public class SilkNetInputHandlerContext : IInputHandlerContext private readonly ILogger _logger; private static IInputContext s_inputcontext; public IInputContext InputContext => s_inputcontext; + + // Keyboard private IKeyboard _primaryKeyboard; public IKeyboard PrimaryKeyboard => _primaryKeyboard; - public HashSet KeysDown = new(); - private bool _capsLockKeyDownCaptured; private bool _capsLockOn; + // Gamepad + private IGamepad? _currentGamePad; + public IGamepad? CurrentGamePad => _currentGamePad; + public HashSet GamepadButtonsDown = new(); + public bool Quit { get; private set; } public bool IsKeyPressed(Key key) => _primaryKeyboard.IsKeyPressed(key); @@ -35,37 +42,127 @@ public void Init() s_inputcontext = _silkNetWindow.CreateInput(); - // Silk.NET Input: Keyboard if (s_inputcontext == null) throw new Exception("Silk.NET Input Context not found."); + + s_inputcontext.ConnectionChanged += ConnectionChanged; + + // Silk.NET Input: Keyboard if (s_inputcontext.Keyboards != null && s_inputcontext.Keyboards.Count != 0) _primaryKeyboard = s_inputcontext.Keyboards[0]; if (_primaryKeyboard == null) throw new Exception("Keyboard not found"); + ListenForKeyboardInput(); - ListenForKeyboardInput(enabled: true); + // Silk.NET Input: Gamepad + if (s_inputcontext.Gamepads != null && s_inputcontext.Gamepads.Count != 0) + { + _currentGamePad = s_inputcontext.Gamepads[0]; + ListenForGampadInput(); + } + else + { + _logger.LogInformation("No gamepads found."); + } } - public void ListenForKeyboardInput(bool enabled) + private void ConnectionChanged(IInputDevice device, bool isConnected) { - if (enabled) + _logger.LogInformation($"Input Connection Changed: {device.Name} {device.Index} isConnected: {isConnected}"); + if (device is IGamepad gamepad) { - // Unregister any existing handlers to avoid duplicates - _primaryKeyboard.KeyUp -= KeyUp; - _primaryKeyboard.KeyDown -= KeyDown; + if (isConnected) + { + _currentGamePad = gamepad; + ListenForGampadInput(); + _logger.LogInformation($"Current Gamepad is now: {device.Name} {device.Index}"); + } + else + { + _currentGamePad = null; + } + } + //else if (device is IKeyboard keyboard) + //{ + // if (isConnected) + // { + // _primaryKeyboard = keyboard; + // ListenForKeyboardInput(); + // } + // else + // { + // _primaryKeyboard = null; + // } + //} + //else + //{ + // _logger.LogWarning($"Unknown device type: {device.GetType().Name}"); + //} + + } + + private void ListenForGampadInput() + { + if (_currentGamePad == null) + return; - _primaryKeyboard.KeyUp += KeyUp; - _primaryKeyboard.KeyDown += KeyDown; + _currentGamePad.Deadzone = new Deadzone(0.05f, DeadzoneMethod.Traditional); + // Unregister any existing handlers to avoid duplicates + _currentGamePad.ButtonDown -= GamepadButtonDown; + _currentGamePad.ButtonUp -= GamepadButtonUp; + //_currentGamePad.ThumbstickMoved -= GamepadThumbstickMoved; + //_currentGamePad.TriggerMoved -= GamepadTriggerMoved; + + _currentGamePad.ButtonDown += GamepadButtonDown; + _currentGamePad.ButtonUp += GamepadButtonUp; + //_currentGamePad.ThumbstickMoved += GamepadThumbstickMoved; + //_currentGamePad.TriggerMoved += GamepadTriggerMoved; + } + + private void GamepadTriggerMoved(IGamepad gamepad, Trigger trigger) + { + _logger.LogDebug($"GamepadTriggerMoved: {trigger.Index}"); + } + + private void GamepadThumbstickMoved(IGamepad gamepad, Thumbstick thumbstick) + { + _logger.LogDebug($"GamepadThumbstickMoved: {thumbstick.Index} {thumbstick.X},{thumbstick.Y}"); + } + + private void GamepadButtonUp(IGamepad gamepad, Button button) + { + var buttonId = button.Name; + if (GamepadButtonsDown.Contains(buttonId)) + { + _logger.LogDebug($"GamepadButtonUp: {button.Name} {button.Index} {button.Pressed}"); + GamepadButtonsDown.Remove(buttonId); } - else + } + + private void GamepadButtonDown(IGamepad gamepad, Button button) + { + var buttonId = button.Name; + if (!GamepadButtonsDown.Contains(buttonId)) { - _primaryKeyboard.KeyUp -= KeyUp; - _primaryKeyboard.KeyDown -= KeyDown; + _logger.LogDebug($"GamepadButtonDown: {button.Name} {button.Index} {button.Pressed}"); + GamepadButtonsDown.Add(buttonId); } } + public void ListenForKeyboardInput(bool enabled = true) + { + // Unregister any existing handlers to avoid duplicates + _primaryKeyboard.KeyUp -= KeyUp; + _primaryKeyboard.KeyDown -= KeyDown; + + if (!enabled) + return; + _primaryKeyboard.KeyUp += KeyUp; + _primaryKeyboard.KeyDown += KeyDown; + } + private void KeyUp(IKeyboard keyboard, Key key, int scanCode) { if (KeysDown.Contains(key)) diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Config/C64KeyboardJoystickMap.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Config/C64KeyboardJoystickMap.cs index 71cdfe4f..2559f7df 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Config/C64KeyboardJoystickMap.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/Config/C64KeyboardJoystickMap.cs @@ -1,27 +1,26 @@ using Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; -using static Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral.C64Joystick; namespace Highbyte.DotNet6502.Systems.Commodore64.Config; public class C64KeyboardJoystickMap { - public Dictionary KeyToJoystick1Map = new() + private Dictionary KeyToJoystick1Map = new() { }; - public Dictionary KeyToJoystick2Map = new() + + private Dictionary KeyToJoystick2Map = new() { + {C64Key.Space, C64JoystickAction.Fire}, {C64Key.W, C64JoystickAction.Up}, {C64Key.S, C64JoystickAction.Down}, {C64Key.A, C64JoystickAction.Left}, - {C64Key.D, C64JoystickAction.Right}, - {C64Key.Space, C64JoystickAction.Fire}, + {C64Key.D, C64JoystickAction.Right} }; - public List GetMappedKeysForJoystickAction(int joystick, C64JoystickAction action) + public Dictionary GetMap(int joystick) { if (joystick != 1 && joystick != 2) throw new ArgumentException($"Invalid joystick number: {joystick}"); - var map = joystick == 1 ? KeyToJoystick1Map : KeyToJoystick2Map; - return map.Where(x => x.Value == action).Select(x => x.Key).ToList(); + return joystick == 1 ? KeyToJoystick1Map : KeyToJoystick2Map; } } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/C64Joystick.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/C64Joystick.cs index b00e8340..d224da0a 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/C64Joystick.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/C64Joystick.cs @@ -1,16 +1,20 @@ using Highbyte.DotNet6502.Systems.Commodore64.Config; +using Microsoft.Extensions.Logging; namespace Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; public class C64Joystick { + private readonly ILogger _logger; + public bool KeyboardJoystickEnabled { get; set; } public C64KeyboardJoystickMap KeyboardJoystickMap { get; private set; } public HashSet CurrentJoystick1Actions { get; private set; } = new(); public HashSet CurrentJoystick2Actions { get; private set; } = new(); - public C64Joystick(C64Config c64Config) + public C64Joystick(C64Config c64Config, ILoggerFactory loggerFactory) { + _logger = loggerFactory.CreateLogger(); KeyboardJoystickEnabled = c64Config.KeyboardJoystickEnabled; KeyboardJoystickMap = c64Config.KeyboardJoystickMap; } @@ -18,23 +22,27 @@ public C64Joystick(C64Config c64Config) public void SetJoystick1Actions(HashSet joystickActions) { CurrentJoystick1Actions = joystickActions; + if (joystickActions.Count > 0) + _logger.LogTrace($"C64 joystick 1 pressed: {string.Join(",", joystickActions)}"); + } public void SetJoystick2Actions(HashSet joystickActions) { CurrentJoystick2Actions = joystickActions; + if (joystickActions.Count > 0) + _logger.LogTrace($"C64 joystick 2 pressed: {string.Join(",", joystickActions)}"); } - - /// - /// Possible joystick actions. More than one can be active at the same time. - /// The integer value corresponds to the bit position in the joystick register (set = not active, clear = active). - /// - public enum C64JoystickAction - { - Up = 0, - Down = 1, - Left = 2, - Right = 3, - Fire = 4 - } +} +/// +/// Possible joystick actions. More than one can be active at the same time. +/// The integer value corresponds to the bit position in the joystick register (set = not active, clear = active). +/// +public enum C64JoystickAction +{ + Up = 0, + Down = 1, + Left = 2, + Right = 3, + Fire = 4 } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/C64Keyboard.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/C64Keyboard.cs index 7e9a695a..fcb64941 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/C64Keyboard.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/C64Keyboard.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; -using static Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral.C64Joystick; namespace Highbyte.DotNet6502.Systems.Commodore64.TimerAndPeripheral; @@ -55,7 +54,7 @@ public void SetKeysPressed(List keys, bool restorePressed, bool capsLock if (restorePressed) { SetRestoreKeyPressed(); - _logger.LogDebug($"C64 restore key pressed, NMI is invokded."); + _logger.LogTrace($"C64 restore key pressed, NMI is invokded."); } // Set pressed keys @@ -63,12 +62,12 @@ public void SetKeysPressed(List keys, bool restorePressed, bool capsLock foreach (var key in keys) _pressedKeys.Add(key); if (keys.Count > 0) - _logger.LogDebug($"C64 keys pressed: {string.Join(",", keys)}"); + _logger.LogTrace($"C64 keys pressed: {string.Join(",", keys)}"); // Check for special key: Caps lock // Not connected to the C64 keyboard matrix, it's connected to the left shift key (keeping it pressed) if (capsLockOn != _capsLockOn) - _logger.LogDebug($"C64 caps lock changed to: {capsLockOn}"); + _logger.LogTrace($"C64 caps lock changed to: {capsLockOn}"); _capsLockOn = capsLockOn; if (capsLockOn) _pressedKeys.Add(C64Key.LShift); @@ -145,42 +144,47 @@ public byte GetPressedKeysForSelectedMatrixRow() return pressedKeys; } + /// + /// Inserts a PETSCII character directly into the keyboard buffer, bypassing keyboard matrix. + /// Can be useful when wanting to type Basic commands on behalf of the user. + /// + /// + public void InsertPetsciiCharIntoBuffer(byte petsciiChar) + { + // Address: 0x00c6: Keyboard buffer index + // Address: 0x0277 - 0x0280: Keyboard buffer + var bufferIndex = _c64.Mem[0x00c6]; + if (bufferIndex >= 10) + return; + _c64.Mem[0x00c6]++; + _c64.Mem[(ushort)(0x0277 + bufferIndex)] = petsciiChar; + } + /// /// Move joystick based on keyboard input (if configured). /// private void HandleJoystickKeyboard() { - var joystick = _c64.Cia.Joystick; - if (joystick.KeyboardJoystickEnabled) + if (_c64.Cia.Joystick.KeyboardJoystickEnabled) { - // Joystick 1 - var joystick1KeyboardMap = joystick.KeyboardJoystickMap.KeyToJoystick1Map; - var joystick1Actions = new HashSet(); - foreach (var c64Key in joystick1KeyboardMap.Keys) - { - if (_pressedKeys.Contains(c64Key)) - { - joystick1Actions.Add(joystick1KeyboardMap[c64Key]); - _pressedKeys.Remove(c64Key); // Remove key from pressed keys to avoid duplicate actions - } - } - joystick.SetJoystick1Actions(joystick1Actions); + HandleJoystickKeyboard(1); + HandleJoystickKeyboard(2); + } + } - // Joystick 2 - var joystick2KeyboardMap = joystick.KeyboardJoystickMap.KeyToJoystick2Map; - var joystick2Actions = new HashSet(); - foreach (var c64Key in joystick2KeyboardMap.Keys) + private void HandleJoystickKeyboard(int joystick) + { + var joystickKeyboardMap = _c64.Cia.Joystick.KeyboardJoystickMap.GetMap(joystick); + var joystickActions = new HashSet(); + foreach (var c64Key in joystickKeyboardMap.Keys) + { + if (_pressedKeys.Contains(c64Key)) { - if (_pressedKeys.Contains(c64Key)) - { - joystick2Actions.Add(joystick2KeyboardMap[c64Key]); - _pressedKeys.Remove(c64Key); // Remove key from pressed keys to avoid duplicate actions - } + joystickActions.Add(joystickKeyboardMap[c64Key]); + _pressedKeys.Remove(c64Key); // Remove key from pressed keys to avoid duplicate actions } - joystick.SetJoystick2Actions(joystick2Actions); - - } + _c64.Cia.Joystick.SetJoystick1Actions(joystickActions); } } diff --git a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/Cia.cs b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/Cia.cs index 74e00d66..514de5d6 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/Cia.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems/Commodore64/TimerAndPeripheral/Cia.cs @@ -20,7 +20,7 @@ public Cia(C64 c64, Config.C64Config c64Config, ILoggerFactory loggerFactory) _c64 = c64; CiaIRQ = new CiaIRQ(); Keyboard = new C64Keyboard(c64, loggerFactory); - Joystick = new C64Joystick(c64Config); + Joystick = new C64Joystick(c64Config, loggerFactory); CiaTimers = new(); CiaTimers.Add(CiaTimerType.Cia1_A, new CiaTimer(CiaTimerType.Cia1_A, IRQSource.TimerA, _c64));