From 87cc62e0025875056df95201fec99f5a5edfdfe7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 5 Apr 2022 04:22:42 -0600 Subject: [PATCH 001/437] Fix ViGEmBus device leak when device creation fails --- MainWindow/MainWindow.xaml.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index a2d0130..7f31561 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using PcapDotNet.Core; +using PcapDotNet.Core; using PcapDotNet.Packets; using System; using System.Collections.Generic; @@ -300,6 +300,9 @@ static bool CreateVigemDevice(uint userIndex) } catch (Exception e) { + // Disconnect the device in case it was connected + try { vigemDevice.Disconnect(); } catch {} + // Create brief exception string // Not using Exception.Message since it doesn't contain the exception type string exceptionString = e.ToString(); From ff00fb503f8baf79e962eee322a28083f8da5d72 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 5 Apr 2022 04:25:07 -0600 Subject: [PATCH 002/437] Update ViGEm user index not found comment to reflect semi-recent developments --- MainWindow/MainWindow.xaml.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index 7f31561..70d4495 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -294,7 +294,8 @@ static bool CreateVigemDevice(uint userIndex) vigemDevice.Connect(); // Throws Xbox360UserIndexNotReportedException - // This also shouldn't happen in 99% of cases, managed to encounter the 1% with someone + // This also shouldn't happen, but it seems to be somewhat prevalent for some reason, + // and if it happens the device is not usable later on in program execution int _userIndex = vigemDevice.UserIndex; Console.WriteLine($"Created new ViGEmBus device with user index {_userIndex}"); } From 867bb72576ea055afe77a7c3fb6c6cf3187ec546 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 5 Apr 2022 04:26:33 -0600 Subject: [PATCH 003/437] Fix a couple stray un-spaced catch statements --- MainWindow/MainWindow.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index 70d4495..ac587fd 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -352,7 +352,7 @@ private void PopulateControllerDropdowns() vigemFound = true; Console.WriteLine("ViGEmBus found!"); } - catch(Nefarius.ViGEm.Client.Exceptions.VigemBusNotFoundException) + catch (Nefarius.ViGEm.Client.Exceptions.VigemBusNotFoundException) { vigemClient = null; vigemFound = false; @@ -598,7 +598,7 @@ private void PopulatePcapDropdown() { pcapDeviceList = LivePacketDevice.AllLocalMachine; } - catch(InvalidOperationException) + catch (InvalidOperationException) { Console.WriteLine("Could not retrieve list of Pcap interfaces."); return; From 8fc7b69f7073060c26ac954010e0c90c40de0662 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 5 Apr 2022 04:52:15 -0600 Subject: [PATCH 004/437] Various logging improvements TODO: Implement logging packets to a file for debugging/research Create LogUtils class with helper and extension methods for logging Add static log file instances to MainWindow Move unhandled exception logging to MainWindow Add various logging for exceptions that may happen --- App.xaml.cs | 98 -------------------------- LogUtils.cs | 125 +++++++++++++++++++++++++++++++++ MainWindow/MainWindow.xaml.cs | 126 ++++++++++++++++++++++++++++++---- RB4InstrumentMapper.csproj | 1 + 4 files changed, 240 insertions(+), 110 deletions(-) create mode 100644 LogUtils.cs diff --git a/App.xaml.cs b/App.xaml.cs index 1608c97..43dfb10 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -17,103 +17,5 @@ namespace RB4InstrumentMapper /// public partial class App : Application { - /// - /// Event handler for AppDomain.CurrentDomain.UnhandledException. - /// - /// - /// Logs the exception info to a file and prompts the user with the exception message. - /// - public static void App_UnhandledException(object sender, UnhandledExceptionEventArgs args) - { - // The unhandled exception that fired off the event - Exception unhandledException = args.ExceptionObject as Exception; - - // String representation of exception info - string exceptionString = unhandledException.ToString(); - - // Index to substring with to create exceptionMessage - int removeIndex = exceptionString.IndexOf(Environment.NewLine); - // First line of exceptionString (can't use Split since \n isn't a valid char) - // Not using Exception.Message since it doesn't contain the exception type - string exceptionMessage = exceptionString.Substring(0, removeIndex); - - try // Use an alternate message if log can't be written due to an exception - { - // User's Documents folder - string docsFolder = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); - // Documents\RB4InstrumentMapper\Logs - string logFolderPath = Path.Combine(docsFolder, "RB4InstrumentMapper\\Logs"); - if (!Directory.Exists(logFolderPath)) - { - // Create if it doesn't exist - Directory.CreateDirectory(logFolderPath); - } - - // Current date/time - DateTime currentTime = DateTime.Now; - // Date to append to the log file name - string fileDateTime = currentTime.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture); - // Log file name with date appended - string logFile = $"log_{fileDateTime}.txt"; - // Documents\RB4InstrumentMapper\Logs\log_.txt - string logFilePath = Path.Combine(logFolderPath, logFile); - - // Write to log file - using (StreamWriter errorLog = File.AppendText(logFilePath)) - { - // Message to write to the log - StringBuilder message = new StringBuilder(); - - // Current date and time, formatted in Yeat/Month/Date Hour:Minute:Second with the invariant culture - string logDateTime = currentTime.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture); - message.AppendLine($"[{logDateTime}] ERROR:"); - message.AppendLine("------------------------------"); - message.AppendLine(exceptionString); - message.AppendLine("------------------------------"); - - errorLog.Write(message.ToString()); - } - - // Prompt user - MessageBoxResult result = MessageBox.Show( - $"An unhandled error has occured:\n\n{exceptionMessage}\n\nA log of the error has been created, do you want to open it?", - "Error", - MessageBoxButton.YesNo, - MessageBoxImage.Error - ); - // If user requested to, open the log - if (result == MessageBoxResult.Yes) - { - Process.Start(logFilePath); - } - } - catch (Exception e) - { - // String representation of exception info - string fileExceptionString = e.ToString(); - // Index to substring with to create exceptionMessage - int fileExRemoveIndex = fileExceptionString.IndexOf(Environment.NewLine); - // First line of exceptionString (can't use Split since \n isn't a valid char) - // Not using Exception.Message since it doesn't contain the exception type - string fileExceptionMessage = fileExceptionString.Substring(0, fileExRemoveIndex); - - // Alternate prompt indicating log wasn't able to be created - MessageBox.Show( - $"An unhandled error has occured:\n\n{exceptionMessage}\n\nAn attempt to log the error was made, but failed:\n\n{fileExceptionMessage}", - "Error", - MessageBoxButton.OK, - MessageBoxImage.Error - ); - } - - // Close program - MessageBox.Show( - "The program will now shut down.", - "Error", - MessageBoxButton.OK, - MessageBoxImage.Error - ); - Application.Current.Shutdown(); - } } } diff --git a/LogUtils.cs b/LogUtils.cs new file mode 100644 index 0000000..0a905ea --- /dev/null +++ b/LogUtils.cs @@ -0,0 +1,125 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Text; + +namespace RB4InstrumentMapper +{ + /// + /// Provides functionality for logging. + /// + public static class LogUtils + { + /// + /// The path to the folder to write logs to. + /// + /// + /// Currently %USERPROFILE%\Documents\RB4InstrumentMapper\Logs + /// + public static readonly string LogFolderPath = Path.Combine( + System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + $"RB4InstrumentMapper\\Logs" + ); + + /// + /// The path to the folder to write packet logs to. + /// + /// + /// Currently %USERPROFILE%\Documents\RB4InstrumentMapper\Logs + /// + public static readonly string PacketLogFolderPath = Path.Combine( + System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + $"RB4InstrumentMapper\\Logs" + ); + + /// + /// Create a log file stream. + /// + public static StreamWriter CreateLogStream() + { + return CreateFileStream(LogFolderPath); + } + + /// + /// Create a packet log file stream. + /// + public static StreamWriter CreatePacketLogStream() + { + return CreateFileStream(PacketLogFolderPath); + } + + /// + /// Create a file stream in the specified folder. + /// + /// + /// The folder to create the file in. + /// + static StreamWriter CreateFileStream(string folderPath) + { + // Create logs folder if it doesn't exist + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + string currentTimeString = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture); + string filePath = Path.Combine(folderPath, $"log_{currentTimeString}.txt"); + + try + { + return new StreamWriter(filePath); + } + catch (Exception ex) + { + Console.WriteLine($"Couldn't create log file {filePath}:"); + Console.WriteLine(ex.GetFirstLine()); + Debug.WriteLine(ex.ToString()); + return null; + } + } + + // Extension method for getting the first line of Exception.ToString(), + // since Exception.Message doesn't include the exception type + /// + /// Gets the first line of the method, to include the exception type in the message. + /// + /// + /// The exception being extended with this method. + /// + public static string GetFirstLine(this Exception ex) + { + return ex?.ToString().Split('\n')[0]; + } + + // Extension method for logging exceptions to streams in a customized manner + /// + /// Writes an exception to the stream in a particularly emphasized fashion. + /// + /// + /// The StreamWriter being extended with this method. + /// + /// + /// The exception to log. + /// + /// + /// Additional info to log after the stack trace. + /// + public static void WriteException(this StreamWriter stream, Exception ex, string addtlInfo = null) + { + // Current date and time, formatted in Year/Month/Date Hour:Minute:Second with the invariant culture + string timestamp = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture); + + // Log + stream.WriteLine($"[{timestamp}] EXCEPTION:"); + stream.WriteLine("------------------------------"); + stream.WriteLine(ex.ToString()); + // Prevent writing an empty line if additional info is not provided + if (addtlInfo != null) + { + stream.WriteLine(addtlInfo); + } + stream.WriteLine("------------------------------"); + } + } +} diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index ac587fd..772a3da 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -1,7 +1,9 @@ -using PcapDotNet.Core; +using PcapDotNet.Core; using PcapDotNet.Packets; using System; using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Text; using System.Threading; @@ -33,6 +35,17 @@ public partial class MainWindow : Window /// private static Dispatcher uiDispatcher = null; + /// + /// The file to log to. + /// + private static StreamWriter mainLog = null; + + // TODO: Implement logging packets to a file for debugging/research + /// + /// The file to log packets to. + /// + private static StreamWriter packetLog = null; + /// /// Default Pcap packet capture timeout in milliseconds. /// @@ -181,7 +194,8 @@ private enum VigemEnum public MainWindow() { // Assign event handler for unhandled exceptions - AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(App.App_UnhandledException); + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; InitializeComponent(); @@ -199,20 +213,105 @@ private void Window_Loaded(object sender, RoutedEventArgs e) // Connect to console TextBoxConsole.RedirectConsoleToTextBox(messageConsole, displayLinesWithTimestamp: false); + // Initialize log file + mainLog = LogUtils.CreateLogStream(); + // Initialize dropdowns try // PcapDotNet can't be loaded if Pcap isn't installed, so it will cause a run-time exception here { PopulatePcapDropdown(); } - catch(System.IO.FileNotFoundException) + catch (System.IO.FileNotFoundException ex) { - MessageBox.Show("Could not load Pcap interface.\nThe program will now shut down.", "Error Starting Program", MessageBoxButton.OK, MessageBoxImage.Error); + // Message buffer + StringBuilder message = new StringBuilder(); + + // Log + message.AppendLine("FusionLog:"); + try + { + string fusLog = ex.FusionLog; + message.AppendLine(fusLog); + } + catch (Exception fusEx) + { + message.AppendLine("Error getting FusionLog:"); + message.AppendLine(fusEx.ToString()); + } + mainLog.WriteException(ex, message.ToString()); + + // Prompt + message.Clear(); + message.AppendLine("Could not initialize the program:"); + message.AppendLine(); + message.AppendLine(ex.GetFirstLine()); + message.AppendLine(); + message.AppendLine("The program will now shut down."); + + MessageBox.Show(message.ToString(), "Error Starting Program", MessageBoxButton.OK, MessageBoxImage.Error); + Application.Current.Shutdown(); return; } + PopulateControllerDropdowns(); } + /// + /// Event handler for AppDomain.CurrentDomain.UnhandledException. + /// + /// + /// Logs the exception info to a file and prompts the user with the exception message. + /// + public static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) + { + // The unhandled exception + Exception unhandledException = args.ExceptionObject as Exception; + + // MessageBox message + StringBuilder message = new StringBuilder(); + message.AppendLine("An unhandled error has occured:"); + message.AppendLine(); + message.AppendLine(unhandledException.GetFirstLine()); + message.AppendLine(); + + // Use an alternate message if log couldn't be created + if (mainLog != null) + { + // Log exception + mainLog.WriteException(unhandledException); + + // Complete the message buffer + message.AppendLine("A log of the error has been created, do you want to open it?"); + + // Display message + MessageBoxResult result = MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.YesNo, MessageBoxImage.Error); + // If user requested to, open the log + if (result == MessageBoxResult.Yes) + { + Process.Start(LogUtils.LogFolderPath); + } + } + else + { + // Complete the message buffer + message.AppendLine("An error log was unable to be created."); + + // Display message + MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + + // Close program + MessageBox.Show("The program will now shut down.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + Application.Current.Shutdown(); + } + + public static void OnProcessExit(object sender, EventArgs args) + { + // Close the log file + mainLog.Close(); + } + /// /// Called when the window has closed. /// @@ -299,19 +398,20 @@ static bool CreateVigemDevice(uint userIndex) int _userIndex = vigemDevice.UserIndex; Console.WriteLine($"Created new ViGEmBus device with user index {_userIndex}"); } - catch (Exception e) + catch (Exception ex) { // Disconnect the device in case it was connected try { vigemDevice.Disconnect(); } catch {} - // Create brief exception string - // Not using Exception.Message since it doesn't contain the exception type - string exceptionString = e.ToString(); - int removeIndex = exceptionString.IndexOf(Environment.NewLine); - string exceptionMessage = exceptionString.Substring(0, removeIndex); + // Log the exception + mainLog.WriteLine("ViGEmBus device creation failed!"); + mainLog.WriteException(ex); + // Create brief exception string + string exceptionMessage = ex.GetFirstLine(); string instrumentName = Enum.GetName(typeof(VigemEnum), userIndex); Console.WriteLine($"Could not create ViGEmBus device for {instrumentName}: {exceptionMessage}"); + return false; } @@ -1436,9 +1536,10 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) { notPlugged = LivePacketDevice.AllLocalMachine; } - catch (InvalidOperationException) + catch (InvalidOperationException ex) { MessageBox.Show("Could not auto-assign; an error occured.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); + mainLog.WriteException(ex); return; } @@ -1454,9 +1555,10 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) { plugged = LivePacketDevice.AllLocalMachine; } - catch (InvalidOperationException) + catch (InvalidOperationException ex) { MessageBox.Show("Could not auto-assign; an error occured.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); + mainLog.WriteException(ex); return; } diff --git a/RB4InstrumentMapper.csproj b/RB4InstrumentMapper.csproj index 1f5e682..f6e9ddc 100644 --- a/RB4InstrumentMapper.csproj +++ b/RB4InstrumentMapper.csproj @@ -94,6 +94,7 @@ MSBuild:Compile Designer + From 3dfb2d8e6e8100221cf59d25cdb8ba9d28022891 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 5 Apr 2022 15:52:08 -0600 Subject: [PATCH 005/437] Remove unused string field from GuitarPacket --- PacketParsing/GuitarPacket.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/PacketParsing/GuitarPacket.cs b/PacketParsing/GuitarPacket.cs index 219c274..3738a07 100644 --- a/PacketParsing/GuitarPacket.cs +++ b/PacketParsing/GuitarPacket.cs @@ -8,7 +8,6 @@ namespace RB4InstrumentMapper public struct GuitarPacket { public uint InstrumentID; - public string InstrumentIDString; public bool MenuButton; public bool OptionsButton; From 8da4b22c9c72e6f58cd5d206852c2f7b67f8f861 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 6 Apr 2022 00:16:32 -0600 Subject: [PATCH 006/437] Update packet format docs --- Docs/PacketFormats.md | 341 ++++++++++++++++++++++++------------------ 1 file changed, 193 insertions(+), 148 deletions(-) diff --git a/Docs/PacketFormats.md b/Docs/PacketFormats.md index 8b7a6c4..b103c01 100644 --- a/Docs/PacketFormats.md +++ b/Docs/PacketFormats.md @@ -1,188 +1,233 @@ # Xbox One RB4 Instrument Data Packets -This documentation is far from fully comprehensive yet, and it also needs some better formatting than just long lists. +This document provides some details on how Xbox One device data packets are received through Pcap packet captures. -Byte numbers in lists are 0-indexed. +This documentation is far from fully comprehensive, as there are many parts of the Xbox One controller protocol that don't pertain to sniffing inputs. There are also some parts of the receiver header data that are not well understood. -References: +Byte numbers in the lists are 0-indexed. -- [GuitarSniffer guitar packet logs](https://1drv.ms/f/s!AgQGk0OeTMLwhA-uDO9IQHEHqGhv) -- GuitarSniffer guitar packet spreadsheets: [New](https://docs.google.com/spreadsheets/d/1ITZUvRniGpfS_HV_rBpSwlDdGukc3GC1CeOe7SavQBo/edit?usp=sharing), [Old](https://1drv.ms/x/s!AgQGk0OeTMLwg3GBDXFUC3Erj4Wb) +## Table of Contents -To be referenced later: +- [Packet Sections](#packet-sections) + - [Receiver Header](#receiver-header) + - [Command Header](#command-header) + - [Important Command IDs](#important-command-ids) + - [`0x07`: Virtual Keycode](#0x07-virtual-keycode) + - [`0x20`: Input Data](#0x20-input-data) +- [Guitar Input Data](#guitar-input-data) + - [Guitar Packet Samples](#guitar-packet-samples) +- [Drums Input Data](#drums-input-data) + - [Drum Packet Samples](#drum-packet-samples) +- [References](#references) -- +## Packet Sections -## Packet Frames +- Bytes 0-25: Receiver header +- Bytes 26-29: Command header +- Bytes 30 onward: Message data -- Bytes 0-21: Xbox header -- Bytes 22-29: Packet metadata -- Bytes 30 onward: Input data +### Receiver Header -## Header Data +`0x88 0xA0-00 0x00` -- 22 bytes long +- 26 bytes long +- Not well understood, more research needed Bytes: -- Not fully understood, more research needed - - 0-6 seem constant, `8811A0006245B4` - - 2 seems to be `19` instead of `11` for a single packet in the guitar power-on log and in the whammy log - - 7-9 have been observed to be different between the examples here and the GuitarSniffer packet logs folder - - 10-11 seem constant, `7EED` -- 12-15 - Instrument ID - - 12 appears to be constant, `8F` -- 16-21 seem to mirror 4-9 +- Byte 0: Always `0x88`? +- Byte 1: Flags value? + - Typically `0x11` but is `0x19` every so often +- Bytes 2-3: Constant `0xA0-00`? +- Bytes 4-9: Receiver ID? +- Bytes 10-15: Device ID + - This is the value to keep track of for identifying which device is which. +- Bytes 16-21: Redundant receiver ID? (mirrors 4-9) +- Bytes 22-23: Little-endian packet count + - This one is encoded weirdly, it seems to be an 8-bit value that's been bit-shifted left by 4 bits such that the value is split between two different bytes. + - In more explicit terms, the high 4 bits of 22 seem to increment by 1 with every packet, and 23 seems to increment every time 22's high 4 bits roll over from F to 0. +- Byte 24: Another flags value? + - Seems to be `0x01` during power-on or Xbox button packets, and `0x00` everywhere else +- Byte 25: Constant `0x00`? -## Packet Metadata +### Command Header -- 8 bytes long, comes after the 22 header bytes +` ` + +- 4 bytes long Bytes: -- Not well understood, there are some edge cases that need to be researched further -- 22: - - High 4 bits seem to increment by 1 with every packet -- 23: - - Seems to increment every time 22's high 4 bits roll over from F to 0 -- 24: - - Seems to be `01` during power-on or Xbox button packets, and `00` everywhere else -- 25: - - Seems to be a constant `00` -- 26: - - Type of data? seems to be a constant `20` during regular packets and `07` in Xbox button packets -- 27: - - Unsure, seems to be a constant `00` during regular packets and `20` in Xbox button packets -- 28: - - Seems to increment with every packet, but its value doesn't seem to start at the same time as 22 and 23 -- 29: - - Data bytes length? - - This does not seem to be the case for all of the power-on packets logged, but it seems to be the case 100% of the time after power-on - - On guitars, this is `0A`, except in Xbox button packets where it is `02` - - On drums, this is `06`, except presumably in Xbox button packets where it is probably `02` +- Byte 0: Command ID + - This indicates what the data following this header represents. + - Important IDs: + - `0x07`: Virtual keycode (used for the guide button) + - `0x20`: Input report +- Byte 1: Flags/client ID + - This is a combination flags and client ID value for messages. + - Understanding this value isn't important for just sniffing input data. +- Byte 2: Sequence count + - This keeps track of the order/sequence of packets. + - Understanding this value isn't important for just sniffing input data. +- Byte 3: Data length + - Number of bytes in the rest of the packet (some exceptions to this, but none that pertain to input data). + +The bytes following the header are defined per command ID. + +#### Important Command IDs + +##### `0x07`: Virtual Keycode + +This command is used to report virtual keycode presses. It is also used to report the guide button press. + +` ` + +- `pressed` is a boolean value (`0x00` or `0x01`) indicating whether or not the key is pressed. +- `keycode` is the virtual keycode for the key. + - This is `0x5B` when the guide button is pressed, which equates to the Left Windows key. + +##### `0x20`: Input Data + +This command is used to report input data. The actual input data varies per device type. + +The standard Xbox One controller layout is as follows: + +- 12 bytes long +- ` ` + - `buttons`: 16-bit button bitmask + - Bit 0 (`0x0001`) - D-pad Up + - Bit 1 (`0x0002`) - D-pad Down + - Bit 2 (`0x0004`) - D-pad Left + - Bit 3 (`0x0008`) - D-pad Right + - Bit 4 (`0x0010`) - Left Bumper + - Bit 5 (`0x0020`) - Right Bumper + - Bit 6 (`0x0040`) - Left Stick Press + - Bit 7 (`0x0080`) - Right Stick Press + - Bit 8 (`0x0100`) - Sync button + - Bit 9 (`0x0200`) - Unused (undefined?) + - Bit 10 (`0x0400`) - Menu Button + - Bit 11 (`0x0800`) - Options Button + - Bit 12 (`0x1000`) - A Button + - Bit 13 (`0x2000`) - B Button + - Bit 14 (`0x4000`) - X Button + - Bit 15 (`0x8000`) - Y Button + - `left trigger` and `right trigger`: 16-bit little-endian unsigned axis + - `left stick X`/`Y`, `right stick X`/`Y`: 16-bit little-endian signed axis ## Guitar Input Data -- 40 bytes long, including the Xbox header and packet count data -- 10 bytes long without the header and count data -- 32(?) bytes long when the Xbox button is pressed, including the Xbox header and packet count data -- Some random packets here and there are 34 bytes long -- Packet length varies wildly during power-on (anywhere from 32 to 90), but none there seem to be 40 bytes long. - -Bytes: - -- 30 - Buttons - - Bit 0 (`0x01`) - Xbox - - Bit 1 (`0x02`) - Unknown (maybe equivalent to the Share button?) - - Bit 2 (`0x04`) - Menu button - - Bit 3 (`0x08`) - Options button - - Bit 4 (`0x10`) - Active when pressing either of the Green frets (equivalent to the A button on a regular controller?) - - Bit 5 (`0x20`) - Active when pressing either of the Red frets (equivalent to the B button on a regular controller?) - - Bit 6 (`0x40`) - Active when pressing either of the Blue frets (equivalent to the X button on a regular controller?) - - Bit 7 (`0x80`) - Active when pressing either of the Yellow frets (equivalent to the Y button on a regular controller?) -- 31 - D-pad/Strum Bar / Bumpers/Stick Presses - - Bit 0 (`0x01`) - Down (Strum Up) - - Bit 1 (`0x02`) - Up (Strum Down) - - Bit 2 (`0x04`) - Left - - Bit 3 (`0x08`) - Right - - Bit 4 (`0x10`) - Active when pressing either of the Orange frets (equivalent to the LB button on a regular controller?) - - Bit 5 (`0x20`) - Unused (equivalent to the RB button on a regular controller?) - - Bit 6 (`0x40`) - Active when pressing the lower frets (equivalent to the left stick button on a regular controller?) - - Bit 7 (`0x80`) - Unused (equivalent to the right stick button on a regular controller?) -- 32 - Tilt (Axis) - - Has a threshold of `70`? (values below get cut off to `00`) -- 33 - Whammy Bar (Axis) - - Uses full byte range -- 34 - Pickup Switch/Slider (Axis) - - Uses top 4 bytes, possible values are `00`, `10`, `20`, `30`, and `40` -- 35 - Upper Frets - - Bit 0 (`0x01`) - Green - - Bit 1 (`0x02`) - Red - - Bit 2 (`0x04`) - Yellow - - Bit 3 (`0x08`) - Blue - - Bit 4 (`0x10`) - Orange - - Bits 5-7 - Unused -- 36 - Lower Frets +10 bytes long + +` ` + +- `buttons`: 16-bit button bitmask + - Bit 0 (`0x0001`) - D-pad Up/Strum Up + - Bit 1 (`0x0002`) - D-pad Down/Strum Down + - Bit 2 (`0x0004`) - D-pad Left + - Bit 3 (`0x0008`) - D-pad Right + - Bit 4 (`0x0010`) - Orange Fret Flag (equivalent to Left Bumper) + - Bit 5 (`0x0020`) - Unused (equivalent to Right Bumper) + - Bit 6 (`0x0040`) - Lower Fret Flag (equivalent to Left Stick Press) + - Bit 7 (`0x0080`) - Unused (equivalent to Right Stick Press) + - Bit 8 (`0x0100`) - Sync button? + - Bit 9 (`0x0200`) - Unused (undefined?) + - Bit 10 (`0x0400`) - Menu Button + - Bit 11 (`0x0800`) - Options Button + - Bit 12 (`0x1000`) - Green Fret Flag (equivalent to A Button) + - Bit 13 (`0x2000`) - Red Fret Flag (equivalent to B Button) + - Bit 14 (`0x4000`) - Blue Fret Flag (equivalent to X Button) + - Bit 15 (`0x8000`) - Yellow Fret Flag (equivalent to Y Button) +- `tilt`: 8-bit tilt axis + - Has a threshold of `0x70`? (values below get cut off to `0x00`) +- `whammy`: 8-bit whammy bar axis +- `pickup/slider`: 8-bit pickup switch/slider axis + - Seems to use top 4 bytes, values from the Guitar Sniffer logs are `0x00`, `0x10`, `0x20`, `0x30`, and `0x40` +- `upper frets`, `lower frets`: 8-bit fret bitmask - Bit 0 (`0x01`) - Green - Bit 1 (`0x02`) - Red - Bit 2 (`0x04`) - Yellow - Bit 3 (`0x08`) - Blue - Bit 4 (`0x10`) - Orange - Bits 5-7 - Unused -- 37-39 are unknown +- `unused[3]`: unknown data values + +### Guitar Packet Samples + +``` +2021-10-31 01:35:10.730 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A20550000 2000530A 00003C00000000000000 +2021-10-31 01:35:10.742 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A30550000 2000540A 00003E00000000000000 +2021-10-31 01:35:10.750 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A40550000 2000550A 00003D00000000000000 +2021-10-31 01:35:10.758 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A50550000 2000560A 00003C00000000000000 +2021-10-31 01:35:10.766 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A60550000 2000570A 00003B00000000000000 +2021-10-31 01:35:10.783 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A70550000 2000580A 00003C00000000000000 +2021-10-31 01:35:10.799 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A80550000 2000590A 00003B00000000000000 +2021-10-31 01:35:10.807 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A90550000 20005A0A 00003C00000000000000 +``` + +``` +2021-10-31 01:58:23.354 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A209B0000 2000C60A 10008A00400100000000 +2021-10-31 01:58:23.363 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A309B0000 2000C70A 10008900400100000000 +2021-10-31 01:58:23.371 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A409B0000 2000C80A 10008A00400100000000 +2021-10-31 01:58:23.403 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A509B0000 2000C90A 10008B00400100000000 +2021-10-31 01:58:23.411 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A609B0000 2000CA0A 10008D00400100000000 +2021-10-31 01:58:23.443 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A709B0000 2000CB0A 10008F00400100000000 +2021-10-31 01:58:23.459 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A809B0000 2000CC0A 10009000400100000000 +``` ## Drums Input Data -- 36 bytes long, including the Xbox header and packet count data -- 6 bytes long without the header and count data -- Presumably 32(?) bytes long when the Xbox button is pressed, including the Xbox header and packet count data +Some of the data here is speculatory. It needs to be verified using packet captures. -Bytes: +6 bytes long -- 30 - Buttons + Red/Green Pads - - Bit 0 (`0x01`) - Xbox - - Bit 1 (`0x02`) - Unknown (maybe equivalent to the Share button?) - - Bit 2 (`0x04`) - Menu button - - Bit 3 (`0x08`) - Options button - - Bit 4 (`0x10`) - Green Pad (equivalent to the A button on a regular controller?) - - Bit 5 (`0x20`) - Red Pad (equivalent to the B button on a regular controller?) - - Bit 6 (`0x40`) - (Interpolated) Blue Pad? (equivalent to the X button on a regular controller?) - - Bit 7 (`0x80`) - (Interpolated) Yellow Pad? (equivalent to the Y button on a regular controller?) -- 31 - D-pad - - Bit 0 (`0x01`) - Down - - Bit 1 (`0x02`) - Up - - Bit 2 (`0x04`) - Left - - Bit 3 (`0x08`) - Right - - Bit 4 (`0x10`) - 1st Kick Pedal (equivalent to the LB button on a regular controller?) - - Bit 5 (`0x20`) - 2nd Kick Pedal (equivalent to the RB button on a regular controller?) - - Bit 6 (`0x40`) - Unused? (equivalent to the left stick button on a regular controller?) - - Bit 7 (`0x80`) - Unused? (equivalent to the right stick button on a regular controller?) -- 32 - Yellow Pad - - Uses bits 0-3 -- 33 - Blue Pad - - Uses bits 4-7 -- 34 - Yellow & Blue Cymbal - - Bits 0-3 - Blue Cymbal - - Bits 4-7 - Yellow Cymbal -- 35 - Green Cymbal - - Uses bits 4-7 - -## Samples - -Guitar 1 Sample +` ` -``` -2021-10-31 01:35:10.730 [40] 8811A0006245B4E9D18A7EED 8FFE198A 6245B4E9D18A 205500002000530A 00003C00000000000000 -2021-10-31 01:35:10.742 [40] 8811A0006245B4E9D18A7EED 8FFE198A 6245B4E9D18A 305500002000540A 00003E00000000000000 -2021-10-31 01:35:10.750 [40] 8811A0006245B4E9D18A7EED 8FFE198A 6245B4E9D18A 405500002000550A 00003D00000000000000 -2021-10-31 01:35:10.758 [40] 8811A0006245B4E9D18A7EED 8FFE198A 6245B4E9D18A 505500002000560A 00003C00000000000000 -2021-10-31 01:35:10.766 [40] 8811A0006245B4E9D18A7EED 8FFE198A 6245B4E9D18A 605500002000570A 00003B00000000000000 -2021-10-31 01:35:10.783 [40] 8811A0006245B4E9D18A7EED 8FFE198A 6245B4E9D18A 705500002000580A 00003C00000000000000 -2021-10-31 01:35:10.799 [40] 8811A0006245B4E9D18A7EED 8FFE198A 6245B4E9D18A 805500002000590A 00003B00000000000000 -2021-10-31 01:35:10.807 [40] 8811A0006245B4E9D18A7EED 8FFE198A 6245B4E9D18A 9055000020005A0A 00003C00000000000000 -``` +Bytes: -Guitar 2 Sample +- `buttons`: 16-bit button bitmask + - Bit 0 (`0x0001`) - D-pad Up + - Bit 1 (`0x0002`) - D-pad Down + - Bit 2 (`0x0004`) - D-pad Left + - Bit 3 (`0x0008`) - D-pad Right + - Bit 4 (`0x0010`) - 1st Kick Pedal (equivalent to Left Bumper) + - Bit 5 (`0x0020`) - 2nd Kick Pedal (equivalent to Right Bumper) + - Bit 6 (`0x0040`) - Unused? (equivalent to Left Stick Press) + - Bit 7 (`0x0080`) - Unused? (equivalent to Right Stick Press) + - Bit 8 (`0x0100`) - Sync button? + - Bit 9 (`0x0200`) - Unused (undefined?) + - Bit 10 (`0x0400`) - Menu Button + - Bit 11 (`0x0800`) - Options Button + - Bit 12 (`0x1000`) - Green Pad (equivalent to A Button) + - Bit 13 (`0x2000`) - Red Pad (equivalent to B Button) + - Bit 14 (`0x4000`) - Blue Pad (equivalent to X Button) + - Bit 15 (`0x8000`) - Yellow Pad (equivalent to Y Button) +- Bytes 32-33 - Pad velocities + - Bits 0-3 (`0x000F`) - Green Pad? + - Bits 4-7 (`0x00F0`) - Blue Pad? + - Bits 8-11 (`0x0F00`) - Yellow Pad? + - Bits 12-15 (`0xF000`) - Red Pad? +- Bytes 34-35 - Cymbal velocities + - Bits 0-3 (`0x000F`) - Unused? + - Bits 4-7 (`0x00F0`) - Green Cymbal + - Bits 8-11 (`0x0F00`) - Blue Cymbal + - Bits 12-15 (`0xF000`) - Yellow Cymbal + +### Drum Packet Samples ``` -2021-10-31 01:58:23.354 [40] 8811A0006245B4E9D18A7EED 8FFB14BF 6245B4E9D18A 209B00002000C60A 10008A00400100000000 -2021-10-31 01:58:23.363 [40] 8811A0006245B4E9D18A7EED 8FFB14BF 6245B4E9D18A 309B00002000C70A 10008900400100000000 -2021-10-31 01:58:23.371 [40] 8811A0006245B4E9D18A7EED 8FFB14BF 6245B4E9D18A 409B00002000C80A 10008A00400100000000 -2021-10-31 01:58:23.403 [40] 8811A0006245B4E9D18A7EED 8FFB14BF 6245B4E9D18A 509B00002000C90A 10008B00400100000000 -2021-10-31 01:58:23.411 [40] 8811A0006245B4E9D18A7EED 8FFB14BF 6245B4E9D18A 609B00002000CA0A 10008D00400100000000 -2021-10-31 01:58:23.443 [40] 8811A0006245B4E9D18A7EED 8FFB14BF 6245B4E9D18A 709B00002000CB0A 10008F00400100000000 -2021-10-31 01:58:23.459 [40] 8811A0006245B4E9D18A7EED 8FFB14BF 6245B4E9D18A 809B00002000CC0A 10009000400100000000 +2021-10-31 02:25:31.725 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AA0020000 20002B06 0000 0400 0000 +2021-10-31 02:25:31.773 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AB0020000 20002C06 0000 0000 0000 +2021-10-31 02:25:32.038 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AC0020000 20002D06 2000 4000 0000 +2021-10-31 02:25:32.086 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AD0020000 20002E06 0000 0000 0000 +2021-10-31 02:25:32.327 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AE0020000 20002F06 0000 0400 0000 +2021-10-31 02:25:32.367 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AF0020000 20003006 0000 0000 0000 +2021-10-31 02:25:32.608 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18A00030000 20003106 0000 0040 0000 +2021-10-31 02:25:32.656 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18A10030000 20003206 0000 0000 0000 ``` -Drum Sample +## References -``` -2021-10-31 02:25:31.725 [36] 8811A0006245B4E9D18A7EED 8FFFCF6B 6245B4E9D18A A002000020002B06 000004000000 -2021-10-31 02:25:31.773 [36] 8811A0006245B4E9D18A7EED 8FFFCF6B 6245B4E9D18A B002000020002C06 000000000000 -2021-10-31 02:25:32.038 [36] 8811A0006245B4E9D18A7EED 8FFFCF6B 6245B4E9D18A C002000020002D06 200040000000 -2021-10-31 02:25:32.086 [36] 8811A0006245B4E9D18A7EED 8FFFCF6B 6245B4E9D18A D002000020002E06 000000000000 -2021-10-31 02:25:32.327 [36] 8811A0006245B4E9D18A7EED 8FFFCF6B 6245B4E9D18A E002000020002F06 000004000000 -2021-10-31 02:25:32.367 [36] 8811A0006245B4E9D18A7EED 8FFFCF6B 6245B4E9D18A F002000020003006 000000000000 -2021-10-31 02:25:32.608 [36] 8811A0006245B4E9D18A7EED 8FFFCF6B 6245B4E9D18A 0003000020003106 000000400000 -2021-10-31 02:25:32.656 [36] 8811A0006245B4E9D18A7EED 8FFFCF6B 6245B4E9D18A 1003000020003206 000000000000 -``` +- [GuitarSniffer guitar packet logs](https://1drv.ms/f/s!AgQGk0OeTMLwhA-uDO9IQHEHqGhv) +- GuitarSniffer guitar packet spreadsheets: [New](https://docs.google.com/spreadsheets/d/1ITZUvRniGpfS_HV_rBpSwlDdGukc3GC1CeOe7SavQBo/edit?usp=sharing), [Old](https://1drv.ms/x/s!AgQGk0OeTMLwg3GBDXFUC3Erj4Wb) +- https://github.com/quantus/xbox-one-controller-protocol +- https://github.com/medusalix/xone From f4177964aa4437f1760cc63d623577c63ae24440 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 6 Apr 2022 00:39:21 -0600 Subject: [PATCH 007/437] Remove Pcap.Net and add SharpPcap None of the code has been changed yet, this just swaps out the packages. --- App.config | 16 ++++++++++++++-- RB4InstrumentMapper.csproj | 26 ++++++++++++++++++-------- packages.config | 8 +++++++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/App.config b/App.config index fb35790..81ccd7a 100644 --- a/App.config +++ b/App.config @@ -1,7 +1,7 @@ - + - +
@@ -36,4 +36,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/RB4InstrumentMapper.csproj b/RB4InstrumentMapper.csproj index f6e9ddc..d6b7636 100644 --- a/RB4InstrumentMapper.csproj +++ b/RB4InstrumentMapper.csproj @@ -58,17 +58,27 @@ packages\Nefarius.ViGEm.Client.1.17.178\lib\net452\Nefarius.ViGEm.Client.dll - - packages\Pcap.Net.x64.1.0.4.1\lib\net45\PcapDotNet.Base.dll + + packages\PacketDotNet.1.4.0\lib\net47\PacketDotNet.dll - - packages\Pcap.Net.x64.1.0.4.1\lib\net45\PcapDotNet.Core.dll + + packages\SharpPcap.6.1.0\lib\netstandard2.0\SharpPcap.dll - - packages\Pcap.Net.x64.1.0.4.1\lib\net45\PcapDotNet.Core.Extensions.dll + + packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - packages\Pcap.Net.x64.1.0.4.1\lib\net45\PcapDotNet.Packets.dll + + packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + + + + packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + packages\System.Text.Encoding.CodePages.6.0.0\lib\net461\System.Text.Encoding.CodePages.dll False diff --git a/packages.config b/packages.config index 9c8ba4a..5d39a59 100644 --- a/packages.config +++ b/packages.config @@ -1,5 +1,11 @@  - + + + + + + + \ No newline at end of file From 5554767a7003b5c6ec0ce419d3aebcdb6c62c12a Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 6 Apr 2022 03:08:46 -0600 Subject: [PATCH 008/437] Change instrument ID to be a 12-byte value --- MainWindow/MainWindow.xaml.cs | 83 ++++++++++++++++++------- MainWindow/ParsingHelpers.cs | 6 +- PacketParsing/DrumPacket.cs | 16 ++--- PacketParsing/DrumPacketVjoyMapper.cs | 2 +- PacketParsing/DrumViGEmMapper.cs | 2 +- PacketParsing/GuitarPacket.cs | 16 ++--- PacketParsing/GuitarPacketVjoyMapper.cs | 2 +- PacketParsing/GuitarViGEmMapper.cs | 2 +- 8 files changed, 87 insertions(+), 42 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index 772a3da..c085a8e 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using PcapDotNet.Core; +using PcapDotNet.Core; using PcapDotNet.Packets; using System; using System.Collections.Generic; @@ -123,9 +123,9 @@ public partial class MainWindow : Window /// Instrument ID for guitar 1. /// /// - /// An ID of 0x00000000 is assumed to be invalid. + /// An ID of 0 is assumed to be invalid. /// - private static uint guitar1InstrumentId = 0; + private static ulong guitar1InstrumentId = 0; /// /// Index of the selected guitar 2 device. @@ -136,9 +136,9 @@ public partial class MainWindow : Window /// Instrument ID for guitar 2. /// /// - /// An ID of 0x00000000 is assumed to be invalid. + /// An ID of 0 is assumed to be invalid. /// - private static uint guitar2InstrumentId = 0; + private static ulong guitar2InstrumentId = 0; /// /// Index of the selected drum device. @@ -149,9 +149,9 @@ public partial class MainWindow : Window /// Instrument ID for the drumkit. /// /// - /// An ID of 0x00000000 is assumed to be invalid. + /// An ID of 0 is assumed to be invalid. /// - private static uint drumInstrumentId = 0; + private static ulong drumInstrumentId = 0; /// /// Analyzed packet for guitar. @@ -630,7 +630,13 @@ private void PopulateControllerDropdowns() // Load default device IDs // Guitar 1 string hexString = Properties.Settings.Default.currentGuitar1Id; - if (!ParsingHelpers.HexStringToUInt32(hexString, out guitar1InstrumentId)) + // Limit ID to 12 characters, as they are 48 bits, not 64 + if (hexString.Length > 12) + { + Console.WriteLine("Attempted to load an invalid Guitar 1 instrument ID. The ID has been reset."); + guitar1InstrumentId = 0; + } + else if (!ParsingHelpers.HexStringToUInt64(hexString, out guitar1InstrumentId)) { if (string.IsNullOrEmpty(hexString)) { @@ -646,7 +652,13 @@ private void PopulateControllerDropdowns() // Guitar 2 hexString = Properties.Settings.Default.currentGuitar2Id; - if (!ParsingHelpers.HexStringToUInt32(hexString, out guitar2InstrumentId)) + // Limit ID to 12 characters, as they are 48 bits, not 64 + if (hexString.Length > 12) + { + Console.WriteLine("Attempted to load an invalid Guitar 2 instrument ID. The ID has been reset."); + guitar1InstrumentId = 0; + } + else if (!ParsingHelpers.HexStringToUInt64(hexString, out guitar2InstrumentId)) { if (string.IsNullOrEmpty(hexString)) { @@ -662,7 +674,13 @@ private void PopulateControllerDropdowns() // Drum hexString = Properties.Settings.Default.currentDrumId; - if (!ParsingHelpers.HexStringToUInt32(hexString, out drumInstrumentId)) + // Limit ID to 12 characters, as they are 48 bits, not 64 + if (hexString.Length > 12) + { + Console.WriteLine("Attempted to load an invalid Drum instrument ID. The ID has been reset."); + guitar1InstrumentId = 0; + } + else if (!ParsingHelpers.HexStringToUInt64(hexString, out drumInstrumentId)) { if (string.IsNullOrEmpty(hexString)) { @@ -1367,8 +1385,15 @@ private void guitar1IdTextBox_TextChanged(object sender, TextChangedEventArgs e) // Set new ID string hexString = guitar1IdTextBox.Text.ToUpperInvariant(); - uint enteredId; - if (ParsingHelpers.HexStringToUInt32(hexString, out enteredId)) + // Limit ID to 12 characters, as they are 48 bits, not 64 + if (hexString.Length > 12) + { + Console.WriteLine("Hex ID must be 12 characters or less."); + return; + } + + ulong enteredId; + if (ParsingHelpers.HexStringToUInt64(hexString, out enteredId)) { if (enteredId == 0) { @@ -1424,8 +1449,15 @@ private void guitar2IdTextBox_TextChanged(object sender, TextChangedEventArgs e) // Set new ID string hexString = guitar2IdTextBox.Text.ToUpperInvariant(); - uint enteredId; - if (ParsingHelpers.HexStringToUInt32(hexString, out enteredId)) + // Limit ID to 12 characters, as they are 48 bits, not 64 + if (hexString.Length > 12) + { + Console.WriteLine("Hex ID must be 12 characters or less."); + return; + } + + ulong enteredId; + if (ParsingHelpers.HexStringToUInt64(hexString, out enteredId)) { if (enteredId == 0) { @@ -1481,8 +1513,15 @@ private void drumIdTextBox_TextChanged(object sender, TextChangedEventArgs e) // Set new ID string hexString = drumIdTextBox.Text.ToUpperInvariant(); - uint enteredId; - if (ParsingHelpers.HexStringToUInt32(hexString, out enteredId)) + // Limit ID to 12 characters, as they are 48 bits, not 64 + if (hexString.Length > 12) + { + Console.WriteLine("Hex ID must be 12 characters or less."); + return; + } + + ulong enteredId; + if (ParsingHelpers.HexStringToUInt64(hexString, out enteredId)) { if (enteredId == 0) { @@ -1825,12 +1864,14 @@ private bool Read_AutoDetectID() string idString = null; if (packet.Length == 40 || packet.Length == 36) { - // String representation: AA BB CC DD + // String representation: AA BB CC DD EE FF uint id = (uint)( - packet[15] | // DD - (packet[14] << 8) | // CC - (packet[13] << 16) | // BB - (packet[12] << 24) // AA + packet[15] | // FF + (packet[14] << 8) | // EE + (packet[13] << 16) | // DD + (packet[12] << 24) | // CC + (packet[11] << 32) | // BB + (packet[10] << 40) // AA ); idString = Convert.ToString(id, 16).ToUpperInvariant(); diff --git a/MainWindow/ParsingHelpers.cs b/MainWindow/ParsingHelpers.cs index a2063f5..002224c 100644 --- a/MainWindow/ParsingHelpers.cs +++ b/MainWindow/ParsingHelpers.cs @@ -43,19 +43,19 @@ public static string ByteArrayToHexString(byte[] byteArray) } /// - /// Converts a string representing a 32-bit hexadecimal number into a 32-bit unsigned integer. + /// Converts a string representing a 64-bit hexadecimal number into a 64-bit unsigned integer. /// /// The string to be converted. /// The converted number. /// True if the conversion was successful, or false if it failed. - public static bool HexStringToUInt32(string hexString, out uint number) + public static bool HexStringToUInt64(string hexString, out ulong number) { if (hexString.StartsWith("0x") || hexString.StartsWith("&h")) { hexString = hexString.Remove(0, 2); } - return uint.TryParse(hexString, NumberStyles.HexNumber, NumberFormatInfo.CurrentInfo, out number); + return ulong.TryParse(hexString, NumberStyles.HexNumber, NumberFormatInfo.CurrentInfo, out number); } /// diff --git a/PacketParsing/DrumPacket.cs b/PacketParsing/DrumPacket.cs index f5735b8..5bce8d4 100644 --- a/PacketParsing/DrumPacket.cs +++ b/PacketParsing/DrumPacket.cs @@ -7,7 +7,7 @@ namespace RB4InstrumentMapper /// public struct DrumPacket { - public uint InstrumentID; + public ulong InstrumentID; public bool MenuButton; public bool OptionsButton; @@ -113,12 +113,14 @@ public static bool AnalyzePacket(byte[] packet, ref DrumPacket data) if (packet != null && packet.Length == DrumPacketLength) { // Assign instrument ID - // String representation: AA BB CC DD - data.InstrumentID = (uint)( - packet[15] | // DD - (packet[14] << 8) | // CC - (packet[13] << 16) | // BB - (packet[12] << 24) // AA + // String representation: AA BB CC DD EE FF + data.InstrumentID = (ulong)( + packet[15] | // FF + (packet[14] << 8) | // EE + (packet[13] << 16) | // DD + (packet[12] << 24) | // CC + (packet[11] << 32) | // BB + (packet[10] << 40) // AA ); // Map buttons diff --git a/PacketParsing/DrumPacketVjoyMapper.cs b/PacketParsing/DrumPacketVjoyMapper.cs index 451b22b..7c7b66b 100644 --- a/PacketParsing/DrumPacketVjoyMapper.cs +++ b/PacketParsing/DrumPacketVjoyMapper.cs @@ -45,7 +45,7 @@ private enum Buttons : uint /// The vJoy device ID to use. /// The ID of the instrument being mapped. /// True if packet was used and converted, false otherwise. - public static bool MapPacket(DrumPacket packet, vJoy vjoyClient, uint joystickDeviceIndex, uint instrumentId) + public static bool MapPacket(DrumPacket packet, vJoy vjoyClient, uint joystickDeviceIndex, ulong instrumentId) { // Ensure instrument ID is assigned if(instrumentId == 0) diff --git a/PacketParsing/DrumViGEmMapper.cs b/PacketParsing/DrumViGEmMapper.cs index 9eeb187..ce8a979 100644 --- a/PacketParsing/DrumViGEmMapper.cs +++ b/PacketParsing/DrumViGEmMapper.cs @@ -15,7 +15,7 @@ public class DrumPacketViGEmMapper /// The ViGEmBus device to map to. /// The instrument ID. /// True if packet was mapped, false otherwise. - public static bool MapPacket(DrumPacket packet, IXbox360Controller vigemDevice, uint instrumentId) + public static bool MapPacket(DrumPacket packet, IXbox360Controller vigemDevice, ulong instrumentId) { // Ensure instrument ID is assigned if(instrumentId == 0) diff --git a/PacketParsing/GuitarPacket.cs b/PacketParsing/GuitarPacket.cs index 3738a07..269290a 100644 --- a/PacketParsing/GuitarPacket.cs +++ b/PacketParsing/GuitarPacket.cs @@ -7,7 +7,7 @@ namespace RB4InstrumentMapper /// public struct GuitarPacket { - public uint InstrumentID; + public ulong InstrumentID; public bool MenuButton; public bool OptionsButton; @@ -112,12 +112,14 @@ public static bool AnalyzePacket(byte[] packet, ref GuitarPacket data) if (packet != null && packet.Length == GuitarPacketLength) { // Assign instrument ID - // String representation: AA BB CC DD - data.InstrumentID = (uint)( - packet[15] | // DD - (packet[14] << 8) | // CC - (packet[13] << 16) | // BB - (packet[12] << 24) // AA + // String representation: AA BB CC DD EE FF + data.InstrumentID = (ulong)( + packet[15] | // FF + (packet[14] << 8) | // EE + (packet[13] << 16) | // DD + (packet[12] << 24) | // CC + (packet[11] << 32) | // BB + (packet[10] << 40) // AA ); // Map buttons diff --git a/PacketParsing/GuitarPacketVjoyMapper.cs b/PacketParsing/GuitarPacketVjoyMapper.cs index d82ca52..57e437e 100644 --- a/PacketParsing/GuitarPacketVjoyMapper.cs +++ b/PacketParsing/GuitarPacketVjoyMapper.cs @@ -45,7 +45,7 @@ private enum Buttons : uint /// The vJoy device ID to map to. /// The ID of the instrument being mapped. /// True if packet was used and converted, false otherwise. - public static bool MapPacket(GuitarPacket packet, vJoy vjoyClient, uint joystickDeviceIndex, uint instrumentId) + public static bool MapPacket(GuitarPacket packet, vJoy vjoyClient, uint joystickDeviceIndex, ulong instrumentId) { // Ensure instrument ID is assigned if (instrumentId == 0) diff --git a/PacketParsing/GuitarViGEmMapper.cs b/PacketParsing/GuitarViGEmMapper.cs index 487f355..56e1298 100644 --- a/PacketParsing/GuitarViGEmMapper.cs +++ b/PacketParsing/GuitarViGEmMapper.cs @@ -15,7 +15,7 @@ public class GuitarPacketViGEmMapper /// The ViGEmBus device to map to. /// The instrument ID. /// True if packet was mapped, false otherwise. - public static bool MapPacket(GuitarPacket packet, IXbox360Controller vigemDevice, uint instrumentId) + public static bool MapPacket(GuitarPacket packet, IXbox360Controller vigemDevice, ulong instrumentId) { // Ensure instrument ID is assigned if(instrumentId == 0) From 9aaf3db36b0dff48435d73c01948242715194786 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 6 Apr 2022 04:09:09 -0600 Subject: [PATCH 009/437] Migrate code to SharpPcap --- MainWindow/MainWindow.xaml.cs | 247 +++++++++++++++------------------- PacketParsing/DrumPacket.cs | 2 +- PacketParsing/GuitarPacket.cs | 2 +- 3 files changed, 112 insertions(+), 139 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index c085a8e..b728176 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -1,6 +1,4 @@ -using PcapDotNet.Core; -using PcapDotNet.Packets; -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -22,6 +20,8 @@ using System.Windows.Threading; using Nefarius.ViGEm.Client; using Nefarius.ViGEm.Client.Targets; +using SharpPcap; +using SharpPcap.LibPcap; namespace RB4InstrumentMapper { @@ -54,22 +54,12 @@ public partial class MainWindow : Window /// /// List of available Pcap devices. /// - private IList pcapDeviceList = null; + private CaptureDeviceList pcapDeviceList = null; /// /// The selected Pcap device. /// - private LivePacketDevice pcapSelectedDevice = null; - - /// - /// Pcap packet communicator. - /// - private PacketCommunicator pcapCommunicator; - - /// - /// Thread that handles Pcap capture. - /// - private Thread pcapCaptureThread; + private ILiveDevice pcapSelectedDevice = null; /// /// Flag indicating that packet capture is active. @@ -714,7 +704,7 @@ private void PopulatePcapDropdown() // Retrieve the device list from the local machine try { - pcapDeviceList = LivePacketDevice.AllLocalMachine; + pcapDeviceList = CaptureDeviceList.Instance; } catch (InvalidOperationException) { @@ -735,7 +725,7 @@ private void PopulatePcapDropdown() StringBuilder sb = new StringBuilder(); for (int i = 0; i < pcapDeviceList.Count; i++) { - LivePacketDevice device = pcapDeviceList[i]; + ILiveDevice device = pcapDeviceList[i]; sb.Clear(); string itemNumber = $"{i + 1}"; sb.Append($"{itemNumber}. "); @@ -957,11 +947,11 @@ private void StartCapture() } // Retrieve the device list from the local machine - IList allDevices = LivePacketDevice.AllLocalMachine; + var allDevices = CaptureDeviceList.Instance; // Check if the device is still present bool deviceStillPresent = false; - foreach(LivePacketDevice device in allDevices) + foreach (var device in allDevices) { if (device.Name == pcapSelectedDevice.Name) { @@ -1092,42 +1082,27 @@ private void StartCapture() } // Open the device - pcapCommunicator = - pcapSelectedDevice.Open( - 45, // small packets - PacketDeviceOpenAttributes.Promiscuous | PacketDeviceOpenAttributes.MaximumResponsiveness, // promiscuous mode with maximum speed - DefaultPacketCaptureTimeoutMilliseconds); // read timeout - - // Read data - pcapCaptureThread = new Thread(ReadContinously); - pcapCaptureThread.Start(); + pcapSelectedDevice.OnPacketArrival += OnPacketArrival; + pcapSelectedDevice.Open(new DeviceConfiguration() + { + Snaplen = 45, // small packets + Mode = DeviceModes.Promiscuous | DeviceModes.MaxResponsiveness, // promiscuous mode with maximum speed + ReadTimeout = DefaultPacketCaptureTimeoutMilliseconds // read timeout + } + ); + pcapSelectedDevice.StartCapture(); Console.WriteLine($"Listening on {pcapSelectedDevice.Description}..."); } - /// - /// Continously reads packets from the Pcap device. - /// - private void ReadContinously() - { - // start the capture - pcapCommunicator.ReceivePackets(0, PacketHandler); - } - /// /// Callback function invoked by Pcap.Net for every incoming packet /// /// The received packet - private void PacketHandler(Packet packet) + private void OnPacketArrival(object sender, PacketCapture packet) { - // Don't use null packets - if (packet == null) - { - return; - } - // Analyze guitar packets - if (GuitarPacketReader.AnalyzePacket(packet.Buffer, ref guitarPacket)) + if (GuitarPacketReader.AnalyzePacket(packet.Data, ref guitarPacket)) { // Map guitar 1 (if enabled) if (guitar1DeviceIndex > 0 && guitar1InstrumentId != 0 && guitar1InstrumentId == guitarPacket.InstrumentID) @@ -1175,7 +1150,7 @@ private void PacketHandler(Packet packet) } } // Analyze drum packets - else if (DrumPacketReader.AnalyzePacket(packet.Buffer, ref drumPacket)) + else if (DrumPacketReader.AnalyzePacket(packet.Data, ref drumPacket)) { // Map drum (if enabled) if (drumDeviceIndex > 0 && drumInstrumentId != 0 && drumInstrumentId == drumPacket.InstrumentID) @@ -1204,8 +1179,9 @@ private void PacketHandler(Packet packet) // Debugging (if enabled) if (packetDebug) { - string packetHexString = ParsingHelpers.ByteArrayToHexString(packet.Buffer); - Console.WriteLine(packet.Timestamp.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{packet.Length}] " + packetHexString); + RawCapture raw = packet.GetPacket(); + string packetHexString = ParsingHelpers.ByteArrayToHexString(raw.Data); + Console.WriteLine(raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + packetHexString); } // Status reporting (slow) @@ -1228,17 +1204,11 @@ private void PacketHandler(Packet packet) private void StopCapture() { // Stop packet capture - if (pcapCommunicator != null) + if (pcapSelectedDevice != null) { - pcapCommunicator.Break(); - pcapCommunicator = null; - } - - // Stop processing thread - if (pcapCaptureThread != null) - { - pcapCaptureThread.Join(); - pcapCaptureThread = null; + // Close will automatically remove assigned event handlers and call StopCapture(), + // so we don't need to do those ourselves + pcapSelectedDevice.Close(); } // Release drum device @@ -1570,10 +1540,10 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) if (MessageBox.Show("Unplug your receiver, then click OK.", "Auto-Detect Receiver", MessageBoxButton.OKCancel) == MessageBoxResult.OK) { // Get the list of devices for when receiver is unplugged - IList notPlugged = null; + CaptureDeviceList notPlugged = null; try { - notPlugged = LivePacketDevice.AllLocalMachine; + notPlugged = CaptureDeviceList.Instance; } catch (InvalidOperationException ex) { @@ -1589,10 +1559,10 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) Thread.Sleep(1000); // Get the list of devices for when receiver is plugged in - IList plugged = null; + CaptureDeviceList plugged = null; try { - plugged = LivePacketDevice.AllLocalMachine; + plugged = CaptureDeviceList.Instance; } catch (InvalidOperationException ex) { @@ -1608,11 +1578,11 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) // Get device names for both not plugged and plugged lists List notPluggedNames = new List(); List pluggedNames = new List(); - foreach (LivePacketDevice oldDevice in notPlugged) + foreach (ILiveDevice oldDevice in notPlugged) { notPluggedNames.Add(oldDevice.Name); } - foreach (LivePacketDevice newDevice in plugged) + foreach (ILiveDevice newDevice in plugged) { pluggedNames.Add(newDevice.Name); } @@ -1628,8 +1598,8 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) } // Create a list of new devices based on the list of new device names - List newDevices = new List(); - foreach (LivePacketDevice newDevice in plugged) + List newDevices = new List(); + foreach (ILiveDevice newDevice in plugged) { if (newNames.Contains(newDevice.Name)) { @@ -1799,11 +1769,11 @@ private bool Read_AutoDetectID() } // Retrieve the device list from the local machine - IList allDevices = LivePacketDevice.AllLocalMachine; + var allDevices = CaptureDeviceList.Instance; // Check if the device is still present bool deviceStillPresent = false; - foreach(LivePacketDevice device in allDevices) + foreach (ILiveDevice device in allDevices) { if (device.Name == pcapSelectedDevice.Name) { @@ -1827,92 +1797,95 @@ private bool Read_AutoDetectID() } // Open the device - pcapCommunicator = - pcapSelectedDevice.Open( - 45, // small packets - PacketDeviceOpenAttributes.Promiscuous | PacketDeviceOpenAttributes.MaximumResponsiveness, // promiscuous mode with maximum speed - DefaultPacketCaptureTimeoutMilliseconds // read timeout - ); + pcapSelectedDevice.Open(new DeviceConfiguration() + { + Snaplen = 45, // small packets + Mode = DeviceModes.Promiscuous | DeviceModes.MaxResponsiveness, // promiscuous mode with maximum speed + ReadTimeout = DefaultPacketCaptureTimeoutMilliseconds // read timeout + } + ); // Receive packet - Packet packet = null; + PacketCapture packet; int attempts = 6; while (attempts > 0) { - pcapCommunicator.ReceivePacket(out packet); - if (packet != null) + var status = pcapSelectedDevice.GetNextPacket(out packet); + if (status == GetPacketStatus.PacketRead) { - break; - } - - // Short pause before retry - Thread.Sleep(333); - attempts--; - } - - // Process if we got a packet - if (packet != null) - { - // Debugging (if enabled) - if (packetDebug) - { - string packetHexString = ParsingHelpers.ByteArrayToHexString(packet.Buffer); - Console.WriteLine(packet.Timestamp.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{packet.Length}] " + packetHexString); - } + RawCapture raw = packet.GetPacket(); + byte[] data = raw.Data; + // RawCapture cannot be null here, as an instance is always created in the GetPacket function + // if (raw == null) + // { + // return; + // } + + // Debugging (if enabled) + if (packetDebug) + { + string packetHexString = ParsingHelpers.ByteArrayToHexString(data); + Console.WriteLine(raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + packetHexString); + } - // Get ID from packet as Hex string - string idString = null; - if (packet.Length == 40 || packet.Length == 36) - { - // String representation: AA BB CC DD EE FF - uint id = (uint)( - packet[15] | // FF - (packet[14] << 8) | // EE - (packet[13] << 16) | // DD - (packet[12] << 24) | // CC - (packet[11] << 32) | // BB - (packet[10] << 40) // AA - ); - - idString = Convert.ToString(id, 16).ToUpperInvariant(); - } + // Get ID from packet as Hex string + string idString = null; + if (raw.PacketLength == 40 || raw.PacketLength == 36) + { + // String representation: AA BB CC DD EE FF + uint id = (uint)( + data[15] | // FF + (data[14] << 8) | // EE + (data[13] << 16) | // DD + (data[12] << 24) | // CC + (data[11] << 32) | // BB + (data[10] << 40) // AA + ); + + idString = Convert.ToString(id, 16).ToUpperInvariant(); + } - // Check assignment flags and packet length - if (packetGuitar1AutoAssign && packet.Length == 40) - { - // Update UI (assigns instrument ID) - uiDispatcher.Invoke((Action)(() => + // Check assignment flags and packet length + if (packetGuitar1AutoAssign && data.Length == 40) { - guitar1IdTextBox.Text = idString; - })); + // Update UI (assigns instrument ID) + uiDispatcher.Invoke((Action)(() => + { + guitar1IdTextBox.Text = idString; + })); - result = true; - } - else if (packetGuitar2AutoAssign && packet.Length == 40) - { - // Update UI (assigns instrument ID) - uiDispatcher.Invoke((Action)(() => + result = true; + } + else if (packetGuitar2AutoAssign && data.Length == 40) { - guitar2IdTextBox.Text = idString; - })); + // Update UI (assigns instrument ID) + uiDispatcher.Invoke((Action)(() => + { + guitar2IdTextBox.Text = idString; + })); - result = true; - } - else if (packetDrumAutoAssign && packet.Length == 36) - { - // Update UI (assigns instrument ID) - uiDispatcher.Invoke((Action)(() => + result = true; + } + else if (packetDrumAutoAssign && data.Length == 36) { - drumIdTextBox.Text = idString; - })); + // Update UI (assigns instrument ID) + uiDispatcher.Invoke((Action)(() => + { + drumIdTextBox.Text = idString; + })); - result = true; + result = true; + } + break; } + + // Short pause before retry + Thread.Sleep(333); + attempts--; } - // Stop packet reading - pcapCommunicator.Break(); - pcapCommunicator = null; + // Close device + pcapSelectedDevice.Close(); return result; } diff --git a/PacketParsing/DrumPacket.cs b/PacketParsing/DrumPacket.cs index 5bce8d4..98853a3 100644 --- a/PacketParsing/DrumPacket.cs +++ b/PacketParsing/DrumPacket.cs @@ -108,7 +108,7 @@ public enum PacketPosition : int /// The data packet to be analyzed. /// A returned DrumPacket. /// True if packet was used and analyzed, false otherwise. - public static bool AnalyzePacket(byte[] packet, ref DrumPacket data) + public static bool AnalyzePacket(ReadOnlySpan packet, ref DrumPacket data) { if (packet != null && packet.Length == DrumPacketLength) { diff --git a/PacketParsing/GuitarPacket.cs b/PacketParsing/GuitarPacket.cs index 269290a..3c11f2c 100644 --- a/PacketParsing/GuitarPacket.cs +++ b/PacketParsing/GuitarPacket.cs @@ -106,7 +106,7 @@ public enum PacketPosition : int /// The data packet to use. /// A returned GuitarPacket. /// True if packet was used and analyzed, false otherwise. - public static bool AnalyzePacket(byte[] packet, ref GuitarPacket data) + public static bool AnalyzePacket(ReadOnlySpan packet, ref GuitarPacket data) { // Check packet if (packet != null && packet.Length == GuitarPacketLength) From 9656d8fbdb29a770f195767a98beb4c7464a3eb3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 17 Apr 2022 21:56:21 -0600 Subject: [PATCH 010/437] Note that button bits are listed as big-endian --- Docs/PacketFormats.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/PacketFormats.md b/Docs/PacketFormats.md index b103c01..373ca18 100644 --- a/Docs/PacketFormats.md +++ b/Docs/PacketFormats.md @@ -94,7 +94,7 @@ The standard Xbox One controller layout is as follows: - 12 bytes long - ` ` - - `buttons`: 16-bit button bitmask + - `buttons`: 16-bit button bitmask. Note that while other values are little-endian, these are listed in big-endian format. - Bit 0 (`0x0001`) - D-pad Up - Bit 1 (`0x0002`) - D-pad Down - Bit 2 (`0x0004`) - D-pad Left From f3ad189cffa302e8054416078fc2e28c33435b32 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 18 Apr 2022 23:33:02 -0600 Subject: [PATCH 011/437] Prevent race condition exception when getting ViGEm device user index --- MainWindow/MainWindow.xaml.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index b728176..cb6a6b0 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -20,6 +20,7 @@ using System.Windows.Threading; using Nefarius.ViGEm.Client; using Nefarius.ViGEm.Client.Targets; +using Nefarius.ViGEm.Client.Targets.Xbox360; using SharpPcap; using SharpPcap.LibPcap; @@ -381,12 +382,6 @@ static bool CreateVigemDevice(uint userIndex) // Win32Exception // These shouldn't happen in 99% of cases, catching just in case vigemDevice.Connect(); - - // Throws Xbox360UserIndexNotReportedException - // This also shouldn't happen, but it seems to be somewhat prevalent for some reason, - // and if it happens the device is not usable later on in program execution - int _userIndex = vigemDevice.UserIndex; - Console.WriteLine($"Created new ViGEmBus device with user index {_userIndex}"); } catch (Exception ex) { @@ -405,10 +400,25 @@ static bool CreateVigemDevice(uint userIndex) return false; } + // Register a temporary event handler for getting the user index + // Prevents a race condition when getting the user index directly from the device + vigemDevice.FeedbackReceived += OnVigemFeedbackReceived; vigemDictionary.Add(userIndex, vigemDevice); return true; } + /// + /// Temporary event handler for logging the user index of a ViGEm device. + /// + static void OnVigemFeedbackReceived(object sender, Xbox360FeedbackReceivedEventArgs args) + { + // Log the user index + Console.WriteLine($"Created new ViGEmBus device with user index {args.LedNumber}"); + + // Unregister the event handler + (sender as IXbox360Controller).FeedbackReceived -= OnVigemFeedbackReceived; + } + /// /// Populates controller device selection combos. /// From 86b78ae527d717bd55e69c035f3a359a21437d65 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 18 Apr 2022 23:37:57 -0600 Subject: [PATCH 012/437] Update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d854685..c115c7b 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,11 @@ Almost all features for guitars and drums are supported. - Windows 10 64-bit - Npcap in WinPcap compatibility mode, or WinPcap - USBPcap -- vJoy or ViGEmBus +- ViGEmBus or vJoy ## Installation -1. Install [WinPCap](https://www.winpcap.org/install/default.htm) (recommended), or [Npcap](https://nmap.org/npcap/#download) in WinPCap compatibility mode. +1. Install [WinPCap](https://www.winpcap.org/install/default.htm) (recommended as that seems to work best), or [Npcap](https://nmap.org/npcap/#download) in WinPCap compatibility mode. - Make sure you only install one or the other. Installing both at the same time may cause issues. 2. Install [USBPCap](https://desowin.org/usbpcap/). 3. Install [ViGEmBus](https://github.com/ViGEm/ViGEmBus/releases/latest) (recommended) or [vJoy](https://github.com/jshafer817/vJoy/releases/latest). From 98b159128cb40aeb8ca59a9c7a39c9d81619b309 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 19 Apr 2022 00:54:53 -0600 Subject: [PATCH 013/437] Add packet logging functionality --- App.config | 3 ++ MainWindow/MainWindow.xaml | 3 +- MainWindow/MainWindow.xaml.cs | 75 ++++++++++++++++++++++++++++++--- Properties/Settings.Designer.cs | 12 ++++++ Properties/Settings.settings | 3 ++ 5 files changed, 88 insertions(+), 8 deletions(-) diff --git a/App.config b/App.config index 81ccd7a..435cba9 100644 --- a/App.config +++ b/App.config @@ -34,6 +34,9 @@ + + + diff --git a/MainWindow/MainWindow.xaml b/MainWindow/MainWindow.xaml index b586cdf..103c2b9 100644 --- a/MainWindow/MainWindow.xaml +++ b/MainWindow/MainWindow.xaml @@ -12,7 +12,8 @@ private static bool packetDebug = false; + /// + /// Flag indicating if packets should be logged to a file. + /// + private static bool packetDebugLog = false; + /// /// Flag indicating that guitar 1 ID auto assigning is in progress. /// @@ -299,8 +304,9 @@ public static void OnUnhandledException(object sender, UnhandledExceptionEventAr public static void OnProcessExit(object sender, EventArgs args) { - // Close the log file - mainLog.Close(); + // Close the log files + mainLog?.Close(); + packetLog?.Close(); } /// @@ -771,13 +777,19 @@ private void PopulatePcapDropdown() pcapDeviceCombo.SelectedIndex = -1; } - // Preset debugging flag + // Preset debugging flags string currentPacketDebugState = Properties.Settings.Default.currentPacketDebugState; if (currentPacketDebugState == "true") { packetDebugCheckBox.IsChecked = true; } + string currentPacketLogState = Properties.Settings.Default.currentPacketDebugLogState; + if (currentPacketLogState == "true") + { + packetLogCheckBox.IsChecked = true; + } + Console.WriteLine($"Discovered {pcapDeviceList.Count} Pcap devices."); } @@ -994,6 +1006,7 @@ private void StartCapture() pcapAutoDetectButton.IsEnabled = false; pcapRefreshButton.IsEnabled = false; packetDebugCheckBox.IsEnabled = false; + packetLogCheckBox.IsEnabled = false; guitar1Combo.IsEnabled = false; guitar1IdTextBox.IsEnabled = false; @@ -1016,6 +1029,12 @@ private void StartCapture() packetsProcessedCountLabel.Content = "0"; processedPacketCount = 0; + // Initialize packet log + if (packetDebugLog) + { + packetLog = LogUtils.CreatePacketLogStream(); + } + // Initialize vJoy bool vjoyResult; if (joystick != null) @@ -1190,8 +1209,13 @@ private void OnPacketArrival(object sender, PacketCapture packet) if (packetDebug) { RawCapture raw = packet.GetPacket(); - string packetHexString = ParsingHelpers.ByteArrayToHexString(raw.Data); - Console.WriteLine(raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + packetHexString); + string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; + Console.WriteLine(packetLogString); + + if (packetDebugLog) + { + packetLog?.WriteLine(packetLogString); + } } // Status reporting (slow) @@ -1251,6 +1275,9 @@ private void StopCapture() } } vigemDictionary.Clear(); + + // Close packet log file if it was created + packetLog?.Close(); // Disable packet capture active flag packetCaptureActive = false; @@ -1260,6 +1287,7 @@ private void StopCapture() pcapAutoDetectButton.IsEnabled = true; pcapRefreshButton.IsEnabled = true; packetDebugCheckBox.IsEnabled = true; + packetLogCheckBox.IsEnabled = true; guitar1Combo.IsEnabled = true; guitar1IdTextBox.IsEnabled = true; @@ -1310,11 +1338,12 @@ private void startButton_Click(object sender, RoutedEventArgs e) private void packetDebugCheckBox_Checked(object sender, RoutedEventArgs e) { packetDebug = true; + packetLogCheckBox.IsEnabled = true; + packetDebugLog = packetLogCheckBox.IsChecked.GetValueOrDefault(); // Remember selected packet debug state Properties.Settings.Default.currentPacketDebugState = "true"; Properties.Settings.Default.Save(); - } /// @@ -1325,12 +1354,42 @@ private void packetDebugCheckBox_Checked(object sender, RoutedEventArgs e) private void packetDebugCheckBox_Unchecked(object sender, RoutedEventArgs e) { packetDebug = false; + packetLogCheckBox.IsEnabled = false; + packetDebugLog = false; // Remember selected packet debug state Properties.Settings.Default.currentPacketDebugState = "false"; Properties.Settings.Default.Save(); } + /// + /// Handles the packet debug checkbox being checked. + /// + /// + /// + private void packetLogCheckBox_Checked(object sender, RoutedEventArgs e) + { + packetDebugLog = true; + + // Remember selected packet debug state + Properties.Settings.Default.currentPacketDebugLogState = "true"; + Properties.Settings.Default.Save(); + } + + /// + /// Handles the packet debug checkbox being unchecked. + /// + /// + /// + private void packetLogCheckBox_Unchecked(object sender, RoutedEventArgs e) + { + packetDebugLog = false; + + // Remember selected packet debug state + Properties.Settings.Default.currentPacketDebugLogState = "false"; + Properties.Settings.Default.Save(); + } + /// /// Handles the click of the Pcap Refresh button. /// @@ -1703,6 +1762,7 @@ private async void AutoDetectID() pcapAutoDetectButton.IsEnabled = false; pcapRefreshButton.IsEnabled = false; packetDebugCheckBox.IsEnabled = false; + packetLogCheckBox.IsEnabled = false; guitar1Combo.IsEnabled = false; guitar1IdTextBox.IsEnabled = false; @@ -1736,6 +1796,7 @@ private async void AutoDetectID() pcapAutoDetectButton.IsEnabled = true; pcapRefreshButton.IsEnabled = true; packetDebugCheckBox.IsEnabled = true; + packetLogCheckBox.IsEnabled = true; guitar1Combo.IsEnabled = true; guitar1IdTextBox.IsEnabled = true; diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs index 899fee2..e1a2c8d 100644 --- a/Properties/Settings.Designer.cs +++ b/Properties/Settings.Designer.cs @@ -118,5 +118,17 @@ public string currentDrumId { this["currentDrumId"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string currentPacketDebugLogState { + get { + return ((string)(this["currentPacketDebugLogState"])); + } + set { + this["currentPacketDebugLogState"] = value; + } + } } } diff --git a/Properties/Settings.settings b/Properties/Settings.settings index a99c4a9..3ec0513 100644 --- a/Properties/Settings.settings +++ b/Properties/Settings.settings @@ -26,5 +26,8 @@ + + + \ No newline at end of file From dec1e9ba56d2f0ca9d46fee50a8f12f236575e64 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 19 Apr 2022 00:56:48 -0600 Subject: [PATCH 014/437] Other logging improvements Don't create the main log until something is logged to it, so as to not create empty log files Don't assume the main log always exists --- MainWindow/MainWindow.xaml.cs | 51 ++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index 291c798..10a4267 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -199,6 +199,40 @@ public MainWindow() uiDispatcher = this.Dispatcher; } + /// + /// Initializes the log file, if it hasn't been initialized already. + /// + private static void CreateLog() + { + if (mainLog != null) + { + return; + } + + // Initialize log file + mainLog = LogUtils.CreateLogStream(); + } + + /// + /// Writes a line to the log file. + /// + private static void LogLine(string text) + { + CreateLog(); + + mainLog?.WriteLine(text); + } + + /// + /// Writes an exception, and any additonal info, to the log. + /// + private static void LogException(Exception ex, string addtlInfo = null) + { + CreateLog(); + + mainLog?.WriteException(ex, addtlInfo); + } + /// /// Called when the window loads. /// @@ -209,9 +243,6 @@ private void Window_Loaded(object sender, RoutedEventArgs e) // Connect to console TextBoxConsole.RedirectConsoleToTextBox(messageConsole, displayLinesWithTimestamp: false); - // Initialize log file - mainLog = LogUtils.CreateLogStream(); - // Initialize dropdowns try // PcapDotNet can't be loaded if Pcap isn't installed, so it will cause a run-time exception here { @@ -234,7 +265,7 @@ private void Window_Loaded(object sender, RoutedEventArgs e) message.AppendLine("Error getting FusionLog:"); message.AppendLine(fusEx.ToString()); } - mainLog.WriteException(ex, message.ToString()); + LogException(ex, message.ToString()); // Prompt message.Clear(); @@ -271,6 +302,8 @@ public static void OnUnhandledException(object sender, UnhandledExceptionEventAr message.AppendLine(unhandledException.GetFirstLine()); message.AppendLine(); + // Create log if it hasn't been created yet + CreateLog(); // Use an alternate message if log couldn't be created if (mainLog != null) { @@ -395,8 +428,8 @@ static bool CreateVigemDevice(uint userIndex) try { vigemDevice.Disconnect(); } catch {} // Log the exception - mainLog.WriteLine("ViGEmBus device creation failed!"); - mainLog.WriteException(ex); + LogLine("ViGEmBus device creation failed!"); + LogException(ex); // Create brief exception string string exceptionMessage = ex.GetFirstLine(); @@ -1617,7 +1650,7 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) catch (InvalidOperationException ex) { MessageBox.Show("Could not auto-assign; an error occured.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); - mainLog.WriteException(ex); + LogException(ex); return; } @@ -1636,7 +1669,7 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) catch (InvalidOperationException ex) { MessageBox.Show("Could not auto-assign; an error occured.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); - mainLog.WriteException(ex); + LogException(ex); return; } From a6e81afce1ead1d921c08296a80e0ca699cfd12b Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 19 Apr 2022 01:07:27 -0600 Subject: [PATCH 015/437] Make packet logs go into their own folder --- LogUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LogUtils.cs b/LogUtils.cs index 0a905ea..27ce0a6 100644 --- a/LogUtils.cs +++ b/LogUtils.cs @@ -30,7 +30,7 @@ public static class LogUtils /// public static readonly string PacketLogFolderPath = Path.Combine( System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - $"RB4InstrumentMapper\\Logs" + $"RB4InstrumentMapper\\PacketLogs" ); /// From de5f50fea04ef4b115f2ba200487823daa4f5fc7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 19 Apr 2022 01:13:36 -0600 Subject: [PATCH 016/437] Register ViGEm device event before connecting it, just in case --- MainWindow/MainWindow.xaml.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index 10a4267..b69ad4d 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -411,6 +411,10 @@ static bool CreateVigemDevice(uint userIndex) // If subtype ID specification through ViGEmBus becomes possible at some point, // the guitar should be subtype 6, and the drums should be subtype 8 + // Register a temporary event handler for getting the user index + // Prevents a race condition when getting the user index directly from the device + vigemDevice.FeedbackReceived += OnVigemFeedbackReceived; + try { // Throws one of 5 exceptions: @@ -439,9 +443,6 @@ static bool CreateVigemDevice(uint userIndex) return false; } - // Register a temporary event handler for getting the user index - // Prevents a race condition when getting the user index directly from the device - vigemDevice.FeedbackReceived += OnVigemFeedbackReceived; vigemDictionary.Add(userIndex, vigemDevice); return true; } From a797020112f210614ed15855ccfd235974073288 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 20 Apr 2022 18:37:27 -0600 Subject: [PATCH 017/437] Fix ViGEmBus devices not getting disconnected when capture stopped --- MainWindow/MainWindow.xaml.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index b69ad4d..2a78cab 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -1298,15 +1298,9 @@ private void StopCapture() } // Disconnect ViGEmBus controllers - if (vigemDictionary.Count != 0) + foreach (IXbox360Controller device in vigemDictionary.Values) { - for (uint i = 0; i < vigemDictionary.Count; i++) - { - if (vigemDictionary.ContainsKey(i) && vigemDictionary[i] != null) - { - vigemDictionary[i].Disconnect(); - } - } + device?.Disconnect(); } vigemDictionary.Clear(); From 0b1c29b1269dcd5a498c19217945e10e85de01fe Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 20 Apr 2022 20:26:44 -0600 Subject: [PATCH 018/437] Remove try-catch for PopulatePcapDropdown It's no longer necessary, as SharpPcap will always be able to load --- MainWindow/MainWindow.xaml.cs | 42 ++--------------------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index 2a78cab..aa92994 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -244,43 +244,7 @@ private void Window_Loaded(object sender, RoutedEventArgs e) TextBoxConsole.RedirectConsoleToTextBox(messageConsole, displayLinesWithTimestamp: false); // Initialize dropdowns - try // PcapDotNet can't be loaded if Pcap isn't installed, so it will cause a run-time exception here - { - PopulatePcapDropdown(); - } - catch (System.IO.FileNotFoundException ex) - { - // Message buffer - StringBuilder message = new StringBuilder(); - - // Log - message.AppendLine("FusionLog:"); - try - { - string fusLog = ex.FusionLog; - message.AppendLine(fusLog); - } - catch (Exception fusEx) - { - message.AppendLine("Error getting FusionLog:"); - message.AppendLine(fusEx.ToString()); - } - LogException(ex, message.ToString()); - - // Prompt - message.Clear(); - message.AppendLine("Could not initialize the program:"); - message.AppendLine(); - message.AppendLine(ex.GetFirstLine()); - message.AppendLine(); - message.AppendLine("The program will now shut down."); - - MessageBox.Show(message.ToString(), "Error Starting Program", MessageBoxButton.OK, MessageBoxImage.Error); - - Application.Current.Shutdown(); - return; - } - + PopulatePcapDropdown(); PopulateControllerDropdowns(); } @@ -352,8 +316,6 @@ private void Window_Closed(object sender, EventArgs e) // Shutdown if (packetCaptureActive) { - // Same situation as PopulatePcapDropdown can happen here, - // but this function will only be called if the program successfully starts in the first place due to the if(packetCaptureActive) StopCapture(); } From ecbe7c5800dadc2dd7d28fce796105aba1289722 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 20 Apr 2022 20:27:28 -0600 Subject: [PATCH 019/437] Rephrase OnPacketArrival summary --- MainWindow/MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index aa92994..11e1a0a 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -1121,7 +1121,7 @@ private void StartCapture() } /// - /// Callback function invoked by Pcap.Net for every incoming packet + /// Handles captured packets. /// /// The received packet private void OnPacketArrival(object sender, PacketCapture packet) From ebf342d6f0adcbff32b0afa25da0f08eaded804f Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 1 May 2022 12:44:14 -0600 Subject: [PATCH 020/437] Final logging improvements Move all logging members and methods to LogUtils.cs Rename LogUtils to Logging Remove unnecessary OnProcessExit event handler Close logs in unhandled exception and window closed handlers Add additional logging lines for context in log files Add message for packet log location after packet capture stops Prevent further attempts to create the main log file if it couldn't be created the first time --- LogUtils.cs | 163 +++++++++++++++++++++++++++++----- MainWindow/MainWindow.xaml.cs | 100 +++++++-------------- 2 files changed, 175 insertions(+), 88 deletions(-) diff --git a/LogUtils.cs b/LogUtils.cs index 27ce0a6..32cdfa8 100644 --- a/LogUtils.cs +++ b/LogUtils.cs @@ -9,8 +9,36 @@ namespace RB4InstrumentMapper /// /// Provides functionality for logging. /// - public static class LogUtils + public static class Logging { + /// + /// The file to log errors to. + /// + private static StreamWriter mainLog = null; + + /// + /// Gets whether or not the main log exists. + /// + public static bool MainLogExists + { + get => mainLog != null; + } + + private static bool allowMainLogCreation = true; + + /// + /// The current file to log packets to. + /// + private static StreamWriter packetLog = null; + + /// + /// Gets whether or not a packet log exists. + /// + public static bool PacketLogExists + { + get => packetLog != null; + } + /// /// The path to the folder to write logs to. /// @@ -19,43 +47,27 @@ public static class LogUtils /// public static readonly string LogFolderPath = Path.Combine( System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - $"RB4InstrumentMapper\\Logs" + "RB4InstrumentMapper\\Logs" ); /// /// The path to the folder to write packet logs to. /// /// - /// Currently %USERPROFILE%\Documents\RB4InstrumentMapper\Logs + /// Currently %USERPROFILE%\Documents\RB4InstrumentMapper\PacketLogs /// public static readonly string PacketLogFolderPath = Path.Combine( System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - $"RB4InstrumentMapper\\PacketLogs" + "RB4InstrumentMapper\\PacketLogs" ); /// - /// Create a log file stream. - /// - public static StreamWriter CreateLogStream() - { - return CreateFileStream(LogFolderPath); - } - - /// - /// Create a packet log file stream. - /// - public static StreamWriter CreatePacketLogStream() - { - return CreateFileStream(PacketLogFolderPath); - } - - /// - /// Create a file stream in the specified folder. + /// Creates a file stream in the specified folder. /// /// /// The folder to create the file in. /// - static StreamWriter CreateFileStream(string folderPath) + private static StreamWriter CreateFileStream(string folderPath) { // Create logs folder if it doesn't exist if (!Directory.Exists(folderPath)) @@ -79,6 +91,113 @@ static StreamWriter CreateFileStream(string folderPath) } } + /// + /// Creates the main log file. + /// + public static bool CreateMainLog() + { + if (allowMainLogCreation && mainLog == null) + { + mainLog = CreateFileStream(LogFolderPath); + if (mainLog != null) + { + Console.WriteLine("Created main log file."); + return true; + } + else + { + // Log could not be created, don't allow creating it again to prevent console spam + allowMainLogCreation = false; + return false; + } + } + else + { + return false; + } + } + + /// + /// Creates a packet log file. + /// + public static bool CreatePacketLog() + { + if (packetLog == null) + { + packetLog = CreateFileStream(PacketLogFolderPath); + if (packetLog != null) + { + Console.WriteLine("Created packet log file."); + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + + /// + /// Writes a line to the log file. + /// + public static void LogLine(string text) + { + // Create log file if it hasn't been made yet + CreateMainLog(); + + mainLog?.WriteLine(text); + } + + /// + /// Writes an exception, and any additonal info, to the log. + /// + public static void LogException(Exception ex, string addtlInfo = null) + { + // Create log file if it hasn't been made yet + CreateMainLog(); + + mainLog?.WriteException(ex, addtlInfo); + } + + public static void LogPacket(string packetLine) + { + // Create log file if it hasn't been made yet + CreatePacketLog(); + + packetLog?.WriteLine(packetLine); + } + + /// + /// Closes the main log file. + /// + public static void CloseMainLog() + { + mainLog?.Close(); + mainLog = null; + } + + /// + /// Closes the active packet log file. + /// + public static void ClosePacketLog() + { + packetLog?.Close(); + packetLog = null; + } + + /// + /// Closes all log files. + /// + public static void CloseAll() + { + CloseMainLog(); + ClosePacketLog(); + } + // Extension method for getting the first line of Exception.ToString(), // since Exception.Message doesn't include the exception type /// diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index 11e1a0a..4ae7f0a 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -36,17 +36,6 @@ public partial class MainWindow : Window /// private static Dispatcher uiDispatcher = null; - /// - /// The file to log to. - /// - private static StreamWriter mainLog = null; - - // TODO: Implement logging packets to a file for debugging/research - /// - /// The file to log packets to. - /// - private static StreamWriter packetLog = null; - /// /// Default Pcap packet capture timeout in milliseconds. /// @@ -191,7 +180,6 @@ public MainWindow() { // Assign event handler for unhandled exceptions AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; - AppDomain.CurrentDomain.ProcessExit += OnProcessExit; InitializeComponent(); @@ -199,40 +187,6 @@ public MainWindow() uiDispatcher = this.Dispatcher; } - /// - /// Initializes the log file, if it hasn't been initialized already. - /// - private static void CreateLog() - { - if (mainLog != null) - { - return; - } - - // Initialize log file - mainLog = LogUtils.CreateLogStream(); - } - - /// - /// Writes a line to the log file. - /// - private static void LogLine(string text) - { - CreateLog(); - - mainLog?.WriteLine(text); - } - - /// - /// Writes an exception, and any additonal info, to the log. - /// - private static void LogException(Exception ex, string addtlInfo = null) - { - CreateLog(); - - mainLog?.WriteException(ex, addtlInfo); - } - /// /// Called when the window loads. /// @@ -267,12 +221,15 @@ public static void OnUnhandledException(object sender, UnhandledExceptionEventAr message.AppendLine(); // Create log if it hasn't been created yet - CreateLog(); + Logging.CreateMainLog(); // Use an alternate message if log couldn't be created - if (mainLog != null) + if (Logging.MainLogExists) { // Log exception - mainLog.WriteException(unhandledException); + Logging.LogLine("-------------------"); + Logging.LogLine("UNHANDLED EXCEPTION"); + Logging.LogLine("-------------------"); + Logging.LogException(unhandledException); // Complete the message buffer message.AppendLine("A log of the error has been created, do you want to open it?"); @@ -282,7 +239,7 @@ public static void OnUnhandledException(object sender, UnhandledExceptionEventAr // If user requested to, open the log if (result == MessageBoxResult.Yes) { - Process.Start(LogUtils.LogFolderPath); + Process.Start(Logging.LogFolderPath); } } else @@ -294,18 +251,14 @@ public static void OnUnhandledException(object sender, UnhandledExceptionEventAr MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.OK, MessageBoxImage.Error); } + // Close the log files + Logging.CloseAll(); + // Close program MessageBox.Show("The program will now shut down.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); Application.Current.Shutdown(); } - public static void OnProcessExit(object sender, EventArgs args) - { - // Close the log files - mainLog?.Close(); - packetLog?.Close(); - } - /// /// Called when the window has closed. /// @@ -324,6 +277,9 @@ private void Window_Closed(object sender, EventArgs e) { vigemClient.Dispose(); } + + // Close the log files + Logging.CloseAll(); } /// @@ -394,8 +350,8 @@ static bool CreateVigemDevice(uint userIndex) try { vigemDevice.Disconnect(); } catch {} // Log the exception - LogLine("ViGEmBus device creation failed!"); - LogException(ex); + Logging.LogLine("ViGEmBus device creation failed!"); + Logging.LogException(ex); // Create brief exception string string exceptionMessage = ex.GetFirstLine(); @@ -1028,7 +984,11 @@ private void StartCapture() // Initialize packet log if (packetDebugLog) { - packetLog = LogUtils.CreatePacketLogStream(); + if (!Logging.CreatePacketLog()) + { + packetDebugLog = false; + Console.WriteLine("Disabled packet logging for this capture session."); + } } // Initialize vJoy @@ -1210,7 +1170,7 @@ private void OnPacketArrival(object sender, PacketCapture packet) if (packetDebugLog) { - packetLog?.WriteLine(packetLogString); + Logging.LogPacket(packetLogString); } } @@ -1266,8 +1226,10 @@ private void StopCapture() } vigemDictionary.Clear(); - // Close packet log file if it was created - packetLog?.Close(); + // Store whether or not the packet log was created + bool doPacketLogMessage = Logging.PacketLogExists; + // Close packet log file + Logging.ClosePacketLog(); // Disable packet capture active flag packetCaptureActive = false; @@ -1301,6 +1263,10 @@ private void StopCapture() processedPacketCount = 0; Console.WriteLine("Stopped capture."); + if (doPacketLogMessage) + { + Console.WriteLine($"Packet logs may be found in {Logging.PacketLogFolderPath}."); + } } /// @@ -1607,7 +1573,8 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) catch (InvalidOperationException ex) { MessageBox.Show("Could not auto-assign; an error occured.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); - LogException(ex); + Logging.LogLine("Error during auto-assignment:"); + Logging.LogException(ex); return; } @@ -1626,7 +1593,8 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) catch (InvalidOperationException ex) { MessageBox.Show("Could not auto-assign; an error occured.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); - LogException(ex); + Logging.LogLine("Error during auto-assignment:"); + Logging.LogException(ex); return; } From d597966d911b3c165461eb00f8879f505567d94d Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 2 May 2022 11:44:52 -0600 Subject: [PATCH 021/437] Rewrite packet parsing Probably over-engineered, but it's much cleaner and less hard-coded oh also I think I missed a change to App.config when I switched things to SharpPcap, so including that here --- App.config | 4 + MainWindow/MainWindow.xaml.cs | 97 +-------- PacketParsing/DrumPacket.cs | 196 ----------------- PacketParsing/DrumPacketVjoyMapper.cs | 194 ----------------- PacketParsing/DrumViGEmMapper.cs | 130 ------------ PacketParsing/GuitarPacket.cs | 193 ----------------- PacketParsing/GuitarPacketVjoyMapper.cs | 186 ---------------- PacketParsing/GuitarViGEmMapper.cs | 114 ---------- PacketParsing/IDeviceMapper.cs | 28 +++ PacketParsing/PacketDefinitions.cs | 197 +++++++++++++++++ PacketParsing/PacketParser.cs | 167 +++++++++++++++ PacketParsing/ParsingUtils.cs | 82 +++++++ PacketParsing/VigemMapper.cs | 260 +++++++++++++++++++++++ PacketParsing/VigemStatic.cs | 64 ++++++ PacketParsing/VjoyMapper.cs | 270 ++++++++++++++++++++++++ PacketParsing/VjoyStatic.cs | 178 ++++++++++++++++ PacketParsing/XboxDevice.cs | 78 +++++++ RB4InstrumentMapper.csproj | 15 +- 18 files changed, 1340 insertions(+), 1113 deletions(-) delete mode 100644 PacketParsing/DrumPacket.cs delete mode 100644 PacketParsing/DrumPacketVjoyMapper.cs delete mode 100644 PacketParsing/DrumViGEmMapper.cs delete mode 100644 PacketParsing/GuitarPacket.cs delete mode 100644 PacketParsing/GuitarPacketVjoyMapper.cs delete mode 100644 PacketParsing/GuitarViGEmMapper.cs create mode 100644 PacketParsing/IDeviceMapper.cs create mode 100644 PacketParsing/PacketDefinitions.cs create mode 100644 PacketParsing/PacketParser.cs create mode 100644 PacketParsing/ParsingUtils.cs create mode 100644 PacketParsing/VigemMapper.cs create mode 100644 PacketParsing/VigemStatic.cs create mode 100644 PacketParsing/VjoyMapper.cs create mode 100644 PacketParsing/VjoyStatic.cs create mode 100644 PacketParsing/XboxDevice.cs diff --git a/App.config b/App.config index 435cba9..9a2a489 100644 --- a/App.config +++ b/App.config @@ -49,6 +49,10 @@ + + + + \ No newline at end of file diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index 4ae7f0a..e68ac14 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -138,16 +138,6 @@ public partial class MainWindow : Window /// private static ulong drumInstrumentId = 0; - /// - /// Analyzed packet for guitar. - /// - private static GuitarPacket guitarPacket = new GuitarPacket(); - - /// - /// Analyzed packet for the drumkit. - /// - private static DrumPacket drumPacket = new DrumPacket(); - /// /// Counter for processed packets. /// @@ -1066,17 +1056,11 @@ private void StartCapture() } } - // Open the device + // Register handler for packet debugging/logging and status reporting pcapSelectedDevice.OnPacketArrival += OnPacketArrival; - pcapSelectedDevice.Open(new DeviceConfiguration() - { - Snaplen = 45, // small packets - Mode = DeviceModes.Promiscuous | DeviceModes.MaxResponsiveness, // promiscuous mode with maximum speed - ReadTimeout = DefaultPacketCaptureTimeoutMilliseconds // read timeout - } - ); - pcapSelectedDevice.StartCapture(); + // Start capture + PacketParser.StartCapture(pcapSelectedDevice); Console.WriteLine($"Listening on {pcapSelectedDevice.Description}..."); } @@ -1086,81 +1070,6 @@ private void StartCapture() /// The received packet private void OnPacketArrival(object sender, PacketCapture packet) { - // Analyze guitar packets - if (GuitarPacketReader.AnalyzePacket(packet.Data, ref guitarPacket)) - { - // Map guitar 1 (if enabled) - if (guitar1DeviceIndex > 0 && guitar1InstrumentId != 0 && guitar1InstrumentId == guitarPacket.InstrumentID) - { - // vJoy - if (guitar1DeviceIndex < (int)VigemEnum.DeviceIndex && joystick != null) - { - if (GuitarPacketVjoyMapper.MapPacket(guitarPacket, joystick, guitar1DeviceIndex, guitar1InstrumentId)) - { - // Used packet - processedPacketCount++; - } - } - // ViGEmBus - else if (guitar1DeviceIndex == (int)VigemEnum.DeviceIndex && vigemClient != null) - { - if (GuitarPacketViGEmMapper.MapPacket(guitarPacket, vigemDictionary[(uint)VigemEnum.Guitar1], guitar1InstrumentId)) - { - // Used packet - processedPacketCount++; - } - } - } - // Map guitar 2 (if enabled) - else if (guitar2DeviceIndex > 0 && guitar2InstrumentId != 0 && guitar2InstrumentId == guitarPacket.InstrumentID) - { - // vJoy - if (guitar2DeviceIndex < (int)VigemEnum.DeviceIndex && joystick != null) - { - if (GuitarPacketVjoyMapper.MapPacket(guitarPacket, joystick, guitar2DeviceIndex, guitar2InstrumentId)) - { - // Used packet - processedPacketCount++; - } - } - // ViGEmBus - else if (guitar2DeviceIndex == (int)VigemEnum.DeviceIndex && vigemClient != null) - { - if (GuitarPacketViGEmMapper.MapPacket(guitarPacket, vigemDictionary[(uint)VigemEnum.Guitar2], guitar2InstrumentId)) - { - // Used packet - processedPacketCount++; - } - } - } - } - // Analyze drum packets - else if (DrumPacketReader.AnalyzePacket(packet.Data, ref drumPacket)) - { - // Map drum (if enabled) - if (drumDeviceIndex > 0 && drumInstrumentId != 0 && drumInstrumentId == drumPacket.InstrumentID) - { - // vJoy - if (drumDeviceIndex < (int)VigemEnum.DeviceIndex && joystick != null) - { - if (DrumPacketVjoyMapper.MapPacket(drumPacket, joystick, drumDeviceIndex, drumInstrumentId)) - { - // Used packet - processedPacketCount++; - } - } - // ViGEmBus - else if (drumDeviceIndex == (int)VigemEnum.DeviceIndex && vigemClient != null) - { - if (DrumPacketViGEmMapper.MapPacket(drumPacket, vigemDictionary[(uint)VigemEnum.Drum], drumInstrumentId)) - { - // Used packet - processedPacketCount++; - } - } - } - } - // Debugging (if enabled) if (packetDebug) { diff --git a/PacketParsing/DrumPacket.cs b/PacketParsing/DrumPacket.cs deleted file mode 100644 index 98853a3..0000000 --- a/PacketParsing/DrumPacket.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; - -namespace RB4InstrumentMapper -{ - /// - /// Data for a drumkit packet. - /// - public struct DrumPacket - { - public ulong InstrumentID; - - public bool MenuButton; - public bool OptionsButton; - public bool XboxButton; - - public bool DpadUp; - public bool DpadDown; - public bool DpadLeft; - public bool DpadRight; - - public bool RedDrum; - public bool YellowDrum; - public bool BlueDrum; - public bool GreenDrum; - - public bool YellowCymbal; - public bool BlueCymbal; - public bool GreenCymbal; - - public bool BassOne; - public bool BassTwo; - } - - /// - /// Functionality to analyze drumkit packets into a DrumPacket struct. - /// - public class DrumPacketReader - { - /// - /// Packet definitions for the drums/cymbals/kicks. - /// - public enum Drums : byte - { - RedDrum = 0x20, - YellowDrum = 0x0F, - BlueDrum = 0xF0, - GreenDrum = 0x10, - - YellowCymbal = 0xF0, - BlueCymbal = 0x0F, - GreenCymbal = 0xF0, - - BassOne = 0x10, - BassTwo = 0x20, - } - - /// - /// Packet definitions for the face buttons. - /// - [Flags] - public enum Buttons : byte - { - Xbox = 0x01, - Menu = 0x04, - Options = 0x08, - } - - /// - /// Packet definitions for the Dpad. - /// - [Flags] - public enum Dpad : byte - { - Up = 0x01, - Down = 0x02, - Left = 0x04, - Right = 0x08, - } - - /// - /// Size of drumkit packets. - /// - private const int DrumPacketLength = 36; - - /// - /// Size of the packet header. - /// - private const int XboxHeaderLength = 22; - - /// - /// Position in the packet from the header. - /// - public enum PacketPosition : int - { - RedGreenDrum = 8, - YellowDrum = 10, - BlueDrum = 11, - YellowBlueCymbal = 12, - GreenCymbal = 13, - BassPedal = 9, - Buttons = 8, - Dpad = 9, - } - - /// - /// Analyzes a packet and assigns its data to a DrumPacket struct. - /// - /// The data packet to be analyzed. - /// A returned DrumPacket. - /// True if packet was used and analyzed, false otherwise. - public static bool AnalyzePacket(ReadOnlySpan packet, ref DrumPacket data) - { - if (packet != null && packet.Length == DrumPacketLength) - { - // Assign instrument ID - // String representation: AA BB CC DD EE FF - data.InstrumentID = (ulong)( - packet[15] | // FF - (packet[14] << 8) | // EE - (packet[13] << 16) | // DD - (packet[12] << 24) | // CC - (packet[11] << 32) | // BB - (packet[10] << 40) // AA - ); - - // Map buttons - byte buttons = packet[XboxHeaderLength + (int)PacketPosition.Buttons]; - - // Menu - data.MenuButton = (buttons & (byte)Buttons.Menu) != 0; - - // Options - data.OptionsButton = (buttons & (byte)Buttons.Options) != 0; - - // Xbox - data.XboxButton = (buttons & (byte)Buttons.Xbox) != 0; - - // Map Dpad - byte dpad = packet[XboxHeaderLength + (int)PacketPosition.Dpad]; - - // Dpad Up - data.DpadUp = (dpad & (byte)Dpad.Up) != 0; - - // Dpad Down - data.DpadDown = (dpad & (byte)Dpad.Down) != 0; - - // Dpad Left - data.DpadLeft = (dpad & (byte)Dpad.Left) != 0; - - // Dpad Right - data.DpadRight = (dpad & (byte)Dpad.Right) != 0; - - // Map drums - byte redGreenDrum = packet[XboxHeaderLength + (int)PacketPosition.RedGreenDrum]; - byte yellowDrum = packet[XboxHeaderLength + (int)PacketPosition.YellowDrum]; - byte blueDrum = packet[XboxHeaderLength + (int)PacketPosition.BlueDrum]; - byte bassDrum = packet[XboxHeaderLength + (int)PacketPosition.BassPedal]; - - // Red drum - data.RedDrum = (redGreenDrum & (byte)Drums.RedDrum) != 0; - - // Yellow drum - data.YellowDrum = (yellowDrum & (byte)Drums.YellowDrum) != 0; - - // Blue drum - data.BlueDrum = (blueDrum & (byte)Drums.BlueDrum) != 0; - - // Green drum - data.GreenDrum = (redGreenDrum & (byte)Drums.GreenDrum) != 0; - - // Bass drums - data.BassOne = (bassDrum & (byte)Drums.BassOne) != 0; - data.BassTwo = (bassDrum & (byte)Drums.BassTwo) != 0; - - // Map cymbals - byte yellowBlueCymbal = packet[XboxHeaderLength + (int)PacketPosition.YellowBlueCymbal]; - byte greenCymbal = packet[XboxHeaderLength + (int)PacketPosition.GreenCymbal]; - - // Yellow cymbal - data.YellowCymbal = (yellowBlueCymbal & (byte)Drums.YellowCymbal) != 0; - - // Blue cymbal - data.BlueCymbal = (yellowBlueCymbal & (byte)Drums.BlueCymbal) != 0; - - // Green cymbal - data.GreenCymbal = (greenCymbal & (byte)Drums.GreenCymbal) != 0; - - // Packet handled - return true; - } - - // Packet ignored - return false; - } - } -} diff --git a/PacketParsing/DrumPacketVjoyMapper.cs b/PacketParsing/DrumPacketVjoyMapper.cs deleted file mode 100644 index 7c7b66b..0000000 --- a/PacketParsing/DrumPacketVjoyMapper.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using vJoyInterfaceWrap; - -namespace RB4InstrumentMapper -{ - /// - /// Functionality to map analyzed drumkit packets to a vJoy device. - /// - public class DrumPacketVjoyMapper - { - /// - /// The vJoy device state. - /// - private static vJoy.JoystickState iReport; - - [Flags] - /// - /// Button flag definitions. - /// - private enum Buttons : uint - { - One = (uint)1 << 0, - Two = (uint)1 << 1, - Three = (uint)1 << 2, - Four = (uint)1 << 3, - Five = (uint)1 << 4, - Six = (uint)1 << 5, - Seven = (uint)1 << 6, - Eight = (uint)1 << 7, - Nine = (uint)1 << 8, - Ten = (uint)1 << 9, - Eleven = (uint)1 << 10, - Twelve = (uint)1 << 11, - Thirteen = (uint)1 << 12, - Fourteen = (uint)1 << 13, - Fifteen = (uint)1 << 14, - Sixteen = (uint)1 << 15 - } - - /// - /// Maps a DrumPacket to a vJoy device. - /// - /// The pre-analyzed data packet to map. - /// The vJoy client to use. - /// The vJoy device ID to use. - /// The ID of the instrument being mapped. - /// True if packet was used and converted, false otherwise. - public static bool MapPacket(DrumPacket packet, vJoy vjoyClient, uint joystickDeviceIndex, ulong instrumentId) - { - // Ensure instrument ID is assigned - if(instrumentId == 0) - { - return false; - } - - // Match instrument ID - if (instrumentId != packet.InstrumentID) - { - return false; - } - - // Reset report and assign device index - iReport.Buttons = 0; - iReport.bDevice = (byte)joystickDeviceIndex; - - // Face buttons - // Menu - if (packet.MenuButton) - { - iReport.Buttons |= (uint)Buttons.Fifteen; - } - - // Options - if (packet.OptionsButton) - { - iReport.Buttons |= (uint)Buttons.Sixteen; - } - - // Xbox - not mapped - - // D-pad to POV - // Ranges from 0 to 35999 (measured in 1/100 of a degree), clockwise, top 0 - if (packet.DpadUp) - { - if (packet.DpadLeft) - { - iReport.bHats = 31500; - } - else if (packet.DpadRight) - { - iReport.bHats = 4500; - } - else - { - iReport.bHats = 0; - } - } - else if (packet.DpadDown) - { - if (packet.DpadLeft) - { - iReport.bHats = 22500; - } - else if (packet.DpadRight) - { - iReport.bHats = 13500; - } - else - { - iReport.bHats = 18000; - } - } - else - { - if (packet.DpadLeft) - { - iReport.bHats = 27000; - } - else if (packet.DpadRight) - { - iReport.bHats = 9000; - } - else - { - // Set the PoV hat to neutral - iReport.bHats = 0xFFFFFFFF; - } - } - - // Drums - // Red drum - if (packet.RedDrum) - { - iReport.Buttons |= (uint)Buttons.One; - } - - // Yellow drum - if (packet.YellowDrum) - { - iReport.Buttons |= (uint)Buttons.Two; - } - - // Blue drum - if (packet.BlueDrum) - { - iReport.Buttons |= (uint)Buttons.Three; - } - - // Green drum - if (packet.GreenDrum) - { - iReport.Buttons |= (uint)Buttons.Four; - } - - // Bass 1 - if (packet.BassOne) - { - iReport.Buttons |= (uint)Buttons.Five; - } - - // Bass 2 - if (packet.BassTwo) - { - iReport.Buttons |= (uint)Buttons.Nine; - } - - - // Cymbals - // Yellow cymbal - if (packet.YellowCymbal) - { - iReport.Buttons |= (uint)Buttons.Six; - } - - // Blue cymbal - if (packet.BlueCymbal) - { - iReport.Buttons |= (uint)Buttons.Seven; - } - - // Green cymbal - if (packet.GreenCymbal) - { - iReport.Buttons |= (uint)Buttons.Eight; - } - - // Send data - vjoyClient.UpdateVJD(joystickDeviceIndex, ref iReport); - - // Packet handled - return true; - } - } -} diff --git a/PacketParsing/DrumViGEmMapper.cs b/PacketParsing/DrumViGEmMapper.cs deleted file mode 100644 index ce8a979..0000000 --- a/PacketParsing/DrumViGEmMapper.cs +++ /dev/null @@ -1,130 +0,0 @@ -using Nefarius.ViGEm.Client.Targets; -using Nefarius.ViGEm.Client.Targets.Xbox360; - -namespace RB4InstrumentMapper -{ - /// - /// Functionality to map analyzed drumkit packets to a ViGEmBus device. - /// - public class DrumPacketViGEmMapper - { - /// - /// Maps a DrumPacket to a ViGEmBus Xbox 360 controller. - /// - /// The pre-analyzed data packet. - /// The ViGEmBus device to map to. - /// The instrument ID. - /// True if packet was mapped, false otherwise. - public static bool MapPacket(DrumPacket packet, IXbox360Controller vigemDevice, ulong instrumentId) - { - // Ensure instrument ID is assigned - if(instrumentId == 0) - { - return false; - } - - // Match instrument ID - if (instrumentId != packet.InstrumentID) - { - return false; - } - - // Don't auto-submit input reports for performance optimization - if (vigemDevice.AutoSubmitReport) - { - vigemDevice.AutoSubmitReport = false; - } - - // Reset report - vigemDevice.ResetReport(); - - // Menu - vigemDevice.SetButtonState(Xbox360Button.Start, - packet.MenuButton); - // Options - vigemDevice.SetButtonState(Xbox360Button.Back, - packet.OptionsButton); - // Xbox - vigemDevice.SetButtonState(Xbox360Button.Guide, - packet.XboxButton); - - // Dpad Up - vigemDevice.SetButtonState(Xbox360Button.Up, - packet.DpadUp); - // Dpad Down - vigemDevice.SetButtonState(Xbox360Button.Down, - packet.DpadDown); - // Dpad Left - vigemDevice.SetButtonState(Xbox360Button.Left, - packet.DpadLeft); - // Dpad Right - vigemDevice.SetButtonState(Xbox360Button.Right, - packet.DpadRight); - - // Red - vigemDevice.SetButtonState(Xbox360Button.B, - packet.RedDrum); - // Yellow - vigemDevice.SetButtonState(Xbox360Button.Y, - packet.YellowDrum || - packet.YellowCymbal); - // Blue - vigemDevice.SetButtonState(Xbox360Button.X, - packet.BlueDrum || - packet.BlueCymbal); - // Green - vigemDevice.SetButtonState(Xbox360Button.A, - packet.GreenDrum || - packet.GreenCymbal); - - // Pad Flag - vigemDevice.SetButtonState(Xbox360Button.RightThumb, - packet.RedDrum || - packet.YellowDrum || - packet.BlueDrum || - packet.GreenDrum); - // Cymbal Flag - vigemDevice.SetButtonState(Xbox360Button.RightShoulder, - packet.YellowCymbal || - packet.BlueCymbal || - packet.GreenCymbal); - - // Bass One - vigemDevice.SetButtonState(Xbox360Button.LeftShoulder, - packet.BassOne); - // Bass Two - vigemDevice.SetButtonState(Xbox360Button.LeftThumb, - packet.BassTwo); - - // Pad/cymbal velocities, for when those get researched - /* - // Red velocity - vigemDevice.SetAxisValue(Xbox360Axis.LeftThumbX, - packet.RedVelocity != 0 ? (short)((256 - packet.RedVelocity) * 128) : 0); - // if packet velocity is not 0, - // return the velocity inverted (i.e. 0 = hardest hit, 255 = softest) - // and scaled to the positive half of a short - // Yellow velocity - vigemDevice.SetAxisValue(Xbox360Axis.LeftThumbY, - packet.YellowVelocity != 0 ? (short)((256 - packet.YellowVelocity) * -128) : 0); - // if packet velocity is not 0, - // return the velocity inverted (i.e. 0 = hardest hit, 255 = softest) - // and scaled to the negative half of a short - // Blue velocity - vigemDevice.SetAxisValue(Xbox360Axis.LeftThumbX, - packet.BlueVelocity != 0 ? (short)((256 - packet.BlueVelocity) * 128) : 0); - // same as Red - // Green velocity - vigemDevice.SetAxisValue(Xbox360Axis.LeftThumbX, - packet.GreenVelocity != 0 ? (short)((256 - packet.GreenVelocity) * -128) : 0); - // same as Yellow - */ - - // Send data - vigemDevice.SubmitReport(); - - // Packet handled - return true; - } - } -} diff --git a/PacketParsing/GuitarPacket.cs b/PacketParsing/GuitarPacket.cs deleted file mode 100644 index 3c11f2c..0000000 --- a/PacketParsing/GuitarPacket.cs +++ /dev/null @@ -1,193 +0,0 @@ -using System; - -namespace RB4InstrumentMapper -{ - /// - /// Data for a guitar packet. - /// - public struct GuitarPacket - { - public ulong InstrumentID; - - public bool MenuButton; - public bool OptionsButton; - public bool XboxButton; - - public bool DpadUp; - public bool DpadDown; - public bool DpadLeft; - public bool DpadRight; - - public bool UpperGreen; - public bool UpperRed; - public bool UpperYellow; - public bool UpperBlue; - public bool UpperOrange; - - public bool LowerGreen; - public bool LowerRed; - public bool LowerYellow; - public bool LowerBlue; - public bool LowerOrange; - - public byte PickupSwitch; - public byte WhammyBar; - public byte Tilt; - } - - /// - /// Functionality to analyze guitar packets into a GuitarPacket struct. - /// - public class GuitarPacketReader - { - /// - /// Packet definitions for the frets. - /// - [Flags] - public enum Frets : byte - { - Green = 0x01, - Red = 0x02, - Yellow = 0x04, - Blue = 0x08, - Orange = 0x10, - } - - /// - /// Packet definitions for the buttons. - /// - [Flags] - public enum Buttons : byte - { - Xbox = 0x01, - Menu = 0x04, - Options = 0x08, - } - - /// - /// Packet definitions for the Dpad. - /// - [Flags] - public enum Dpad : byte - { - Down = 0x01, // up/down inverted to match usage geometry - Up = 0x02, - Left = 0x04, - Right = 0x08, - } - - /// - /// Size of guitar packets. - /// - private const int GuitarPacketLength = 40; - - /// - /// Size of the packet header. - /// - private const int XboxHeaderLength = 22; - - /// - /// Position in the packet from the header. - /// - public enum PacketPosition : int - { - Buttons = 8, - Dpad = 9, - Tilt = 10, - Whammy = 11, - Slider = 12, - UpperFret = 13, - LowerFret = 14, - } - - /// - /// Analyzes a packet and assigns its data to a GuitarPacket struct. - /// - /// The data packet to use. - /// A returned GuitarPacket. - /// True if packet was used and analyzed, false otherwise. - public static bool AnalyzePacket(ReadOnlySpan packet, ref GuitarPacket data) - { - // Check packet - if (packet != null && packet.Length == GuitarPacketLength) - { - // Assign instrument ID - // String representation: AA BB CC DD EE FF - data.InstrumentID = (ulong)( - packet[15] | // FF - (packet[14] << 8) | // EE - (packet[13] << 16) | // DD - (packet[12] << 24) | // CC - (packet[11] << 32) | // BB - (packet[10] << 40) // AA - ); - - // Map buttons - byte buttons = packet[XboxHeaderLength + (int)PacketPosition.Buttons]; - - // Menu - data.MenuButton = (buttons & (byte)Buttons.Menu) != 0; - - // Options - data.OptionsButton = (buttons & (byte)Buttons.Options) != 0; - - // Xbox - data.XboxButton = (buttons & (byte)Buttons.Xbox) != 0; - - // Map Dpad - byte dpad = packet[XboxHeaderLength + (int)PacketPosition.Dpad]; - - // Dpad Up - data.DpadUp = (dpad & (byte)Dpad.Up) != 0; - - // Dpad Down - data.DpadDown = (dpad & (byte)Dpad.Down) != 0; - - // Dpad Left - data.DpadLeft = (dpad & (byte)Dpad.Left) != 0; - - // Dpad Right - data.DpadRight = (dpad & (byte)Dpad.Right) != 0; - - // Frets - byte upperFret = packet[XboxHeaderLength + (int)PacketPosition.UpperFret]; - byte lowerFret = packet[XboxHeaderLength + (int)PacketPosition.LowerFret]; - - // Fret Green - data.UpperGreen = (upperFret & (byte)Frets.Green) != 0; - data.LowerGreen = (lowerFret & (byte)Frets.Green) != 0; - - // Fret Red - data.UpperRed = (upperFret & (byte)Frets.Red) != 0; - data.LowerRed = (lowerFret & (byte)Frets.Red) != 0; - - // Fret Yellow - data.UpperYellow = (upperFret & (byte)Frets.Yellow) != 0; - data.LowerYellow = (lowerFret & (byte)Frets.Yellow) != 0; - - // Fret Blue - data.UpperBlue = (upperFret & (byte)Frets.Blue) != 0; - data.LowerBlue = (lowerFret & (byte)Frets.Blue) != 0; - - // Fret Orange - data.UpperOrange = (upperFret & (byte)Frets.Orange) != 0; - data.LowerOrange = (lowerFret & (byte)Frets.Orange) != 0; - - // Pickup Switch - data.PickupSwitch = packet[XboxHeaderLength + (int)PacketPosition.Slider]; - - // Whammy Bar - data.WhammyBar = packet[XboxHeaderLength + (int)PacketPosition.Whammy]; - - // Tilt - data.Tilt = packet[XboxHeaderLength + (int)PacketPosition.Tilt]; - - // Packet handled - return true; - } - - // Packet ignored - return false; - } - } -} diff --git a/PacketParsing/GuitarPacketVjoyMapper.cs b/PacketParsing/GuitarPacketVjoyMapper.cs deleted file mode 100644 index 57e437e..0000000 --- a/PacketParsing/GuitarPacketVjoyMapper.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System; -using vJoyInterfaceWrap; - -namespace RB4InstrumentMapper -{ - /// - /// Functionality to map analyzed guitar packets to a vJoy device. - /// - public class GuitarPacketVjoyMapper - { - /// - /// The vJoy device state. - /// - private static vJoy.JoystickState iReport; - - [Flags] - /// - /// Button flag definitions. - /// - private enum Buttons : uint - { - One = (uint)1 << 0, - Two = (uint)1 << 1, - Three = (uint)1 << 2, - Four = (uint)1 << 3, - Five = (uint)1 << 4, - Six = (uint)1 << 5, - Seven = (uint)1 << 6, - Eight = (uint)1 << 7, - Nine = (uint)1 << 8, - Ten = (uint)1 << 9, - Eleven = (uint)1 << 10, - Twelve = (uint)1 << 11, - Thirteen = (uint)1 << 12, - Fourteen = (uint)1 << 13, - Fifteen = (uint)1 << 14, - Sixteen = (uint)1 << 15 - } - - /// - /// Maps a GuitarPacket to a vJoy device. - /// - /// The pre-analyzed data packet to map. - /// The vJoy client to use. - /// The vJoy device ID to map to. - /// The ID of the instrument being mapped. - /// True if packet was used and converted, false otherwise. - public static bool MapPacket(GuitarPacket packet, vJoy vjoyClient, uint joystickDeviceIndex, ulong instrumentId) - { - // Ensure instrument ID is assigned - if (instrumentId == 0) - { - return false; - } - - // Match instrument ID - if (instrumentId != packet.InstrumentID) - { - return false; - } - - // Reset report and assign device index - iReport.Buttons = 0; - iReport.bDevice = (byte)joystickDeviceIndex; - - // Face buttons - // Menu - if (packet.MenuButton) - { - iReport.Buttons |= (uint)Buttons.Fifteen; - } - - // Options - if (packet.OptionsButton) - { - iReport.Buttons |= (uint)Buttons.Sixteen; - } - - // Xbox - not mapped - // Ranges from 0 to 35999 (measured in 1/100 of a degree), clockwise, top 0 - - // D-pad to POV - if (packet.DpadUp) - { - if (packet.DpadLeft) - { - iReport.bHats = 31500; - } - else if (packet.DpadRight) - { - iReport.bHats = 4500; - } - else - { - iReport.bHats = 0; - } - } - else if (packet.DpadDown) - { - if (packet.DpadLeft) - { - iReport.bHats = 22500; - } - else if (packet.DpadRight) - { - iReport.bHats = 13500; - } - else - { - iReport.bHats = 18000; - } - } - else - { - if (packet.DpadLeft) - { - iReport.bHats = 27000; - } - else if (packet.DpadRight) - { - iReport.bHats = 9000; - } - else - { - // Set the PoV hat to neutral - iReport.bHats = 0xFFFFFFFF; - } - } - - // Frets - // Fret Green - if (packet.UpperGreen || packet.LowerGreen) - { - iReport.Buttons |= (uint)Buttons.One; - } - // Fret Red - if (packet.UpperRed || packet.LowerRed) - { - iReport.Buttons |= (uint)Buttons.Two; - } - // Fret Yellow - if (packet.UpperYellow || packet.LowerYellow) - { - iReport.Buttons |= (uint)Buttons.Three; - } - // Fret Blue - if (packet.UpperBlue || packet.LowerBlue) - { - iReport.Buttons |= (uint)Buttons.Four; - } - // Fret Orange - if (packet.UpperOrange || packet.LowerOrange) - { - iReport.Buttons |= (uint)Buttons.Five; - } - - // Axes - - // vJoy axis range is 0x0...0x7FFF(0...32767), 50 % = 0x4000(16384). - - // Map pickup switch to X-axis - // input is 0, 16, 32, 48, and 64. - int xAxis = packet.PickupSwitch; - xAxis *= (32768 / 64); - iReport.AxisX = xAxis; - - // Map whammy to Y-axis - // input ranges from 0 (default) to 255 (depressed) - int yAxis = packet.WhammyBar; - yAxis *= (32768 / 256); - iReport.AxisY = yAxis; - - // Map tilt to Z-axis - // input ranges from 0 (horizontal) to 255 (vertical) - int zAxis = packet.Tilt; - zAxis *= (32768 / 256); - iReport.AxisZ = zAxis; - - // Send data - vjoyClient.UpdateVJD(joystickDeviceIndex, ref iReport); - - // Packet handled - return true; - } - } -} diff --git a/PacketParsing/GuitarViGEmMapper.cs b/PacketParsing/GuitarViGEmMapper.cs deleted file mode 100644 index 56e1298..0000000 --- a/PacketParsing/GuitarViGEmMapper.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Nefarius.ViGEm.Client.Targets; -using Nefarius.ViGEm.Client.Targets.Xbox360; - -namespace RB4InstrumentMapper -{ - /// - /// Functionality to map analyzed guitar packets to a ViGEmBus device. - /// - public class GuitarPacketViGEmMapper - { - /// - /// Maps a GuitarPacket to a ViGEmBus Xbox 360 controller. - /// - /// The pre-analyzed data packet. - /// The ViGEmBus device to map to. - /// The instrument ID. - /// True if packet was mapped, false otherwise. - public static bool MapPacket(GuitarPacket packet, IXbox360Controller vigemDevice, ulong instrumentId) - { - // Ensure instrument ID is assigned - if(instrumentId == 0) - { - return false; - } - - // Match instrument ID - if (instrumentId != packet.InstrumentID) - { - return false; - } - - // Don't auto-submit input reports for performance optimization - if (vigemDevice.AutoSubmitReport) - { - vigemDevice.AutoSubmitReport = false; - } - - // Reset report - vigemDevice.ResetReport(); - - // Face buttons - // Menu - vigemDevice.SetButtonState(Xbox360Button.Start, - packet.MenuButton); - // Options - vigemDevice.SetButtonState(Xbox360Button.Back, - packet.OptionsButton); - // Xbox - vigemDevice.SetButtonState(Xbox360Button.Guide, - packet.XboxButton); - - // D-pad - // Dpad Up - vigemDevice.SetButtonState(Xbox360Button.Up, - packet.DpadUp); - // Dpad Down - vigemDevice.SetButtonState(Xbox360Button.Down, - packet.DpadDown); - // Dpad Left - vigemDevice.SetButtonState(Xbox360Button.Left, - packet.DpadLeft); - // Dpad Right - vigemDevice.SetButtonState(Xbox360Button.Right, - packet.DpadRight); - - // Frets - // Fret Green - vigemDevice.SetButtonState(Xbox360Button.A, - packet.UpperGreen || - packet.LowerGreen); - // Fret Red - vigemDevice.SetButtonState(Xbox360Button.B, - packet.UpperRed || - packet.LowerRed); - // Fret Yellow - vigemDevice.SetButtonState(Xbox360Button.Y, - packet.UpperYellow || - packet.LowerYellow); - // Fret Blue - vigemDevice.SetButtonState(Xbox360Button.X, - packet.UpperBlue || - packet.LowerBlue); - // Fret Orange - vigemDevice.SetButtonState(Xbox360Button.LeftShoulder, - packet.UpperOrange || - packet.LowerOrange); - // Solo fret flag - vigemDevice.SetButtonState(Xbox360Button.LeftThumb, - packet.LowerGreen || - packet.LowerRed || - packet.LowerYellow || - packet.LowerBlue || - packet.LowerOrange); - - // Axes - // Whammy - vigemDevice.SetAxisValue(Xbox360Axis.RightThumbX, - (short)((packet.WhammyBar * 257) - 32768)); - // Multiply by 257 to scale into a ushort, then subtract by 32768 to shift into a signed short - // Tilt - vigemDevice.SetAxisValue(Xbox360Axis.RightThumbY, - (short)((packet.Tilt * 257) - 32768)); - // Pickup Switch - vigemDevice.SetSliderValue(Xbox360Slider.LeftTrigger, - packet.PickupSwitch); - - // Send data - vigemDevice.SubmitReport(); - - // Packet handled - return true; - } - } -} diff --git a/PacketParsing/IDeviceMapper.cs b/PacketParsing/IDeviceMapper.cs new file mode 100644 index 0000000..73fc79d --- /dev/null +++ b/PacketParsing/IDeviceMapper.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using SharpPcap; +using SharpPcap.LibPcap; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Common interface for device mappers. + /// + interface IDeviceMapper + { + /// + /// Parses an input packet. + /// + void ParseInput(ReadOnlySpan data, byte length); + + /// + /// Parses a virtual keycode packet. + /// + void ParseVirtualKey(ReadOnlySpan data, byte length); + + /// + /// Performs cleanup for the mapper. + /// + void Close(); + } +} \ No newline at end of file diff --git a/PacketParsing/PacketDefinitions.cs b/PacketParsing/PacketDefinitions.cs new file mode 100644 index 0000000..ea03908 --- /dev/null +++ b/PacketParsing/PacketDefinitions.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Definitions for the receiver header. + /// + static class Length + { + public const int + ReceiverHeader = 26, + CommandHeader = 4, + VirtualKey = 2, + Input_Gamepad = 0x0C, + Input_Guitar = 0x0A, + Input_Drums = 0x06; + } + + /// + /// Definitions for the receiver header. + /// + static class HeaderOffset + { + public const int + DeviceId = 10; + } + + /// + /// Command header offsets relative to the end of the receiver header. + /// + static class CommandOffset + { + public const int + CommandId = 0, + Flags = 1, + SequenceCount = 2, + DataLength = 3; + } + + /// + /// Command IDs to be parsed. + /// + static class CommandId + { + public const int + VirtualKey = 0x07, + Input = 0x20; + } + + /// + /// Virtual keycodes that will be recognized. + /// + static class Keycodes + { + public const byte LeftWin = 0x5b; + } + + /// + /// Virtual keycode packet offsets relative to the end of the command header. + /// + static class KeycodeOffset + { + public const int + PressedState = 0, + Keycode = 1; + } + + /// + /// Flag definitions for the buttons bytes. + /// + static class GamepadButton + { + public const ushort + DpadUp = 0x01, + DpadDown = 0x02, + DpadLeft = 0x04, + DpadRight = 0x08, + LeftBumper = 0x10, + RightBumper = 0x20, + LeftStickPress = 0x40, + RightStickPress = 0x80, + + // Nothing useful can be done with this + // Sync = 0x0100, + // No known use for this bit + // Unused = 0x0200, + + Menu = 0x0400, + Options = 0x0800, + A = 0x1000, + B = 0x2000, + X = 0x4000, + Y = 0x8000; + } + + static class GamepadOffset + { + public const int + Buttons = 0, + LeftTrigger = 2, + RightTrigger = 4, + LeftStickX = 6, + LeftStickY = 8, + RightStickX = 10, + RightStickY = 12; + } + + /// + /// Guitar input data offsets relative to the end of the command header. + /// + static class GuitarOffset + { + public const int + Buttons = 0, + Tilt = 2, + WhammyBar = 3, + PickupSwitch = 4, + UpperFrets = 5, + LowerFrets = 6; + + // Final 3 bytes are uknown + } + + static class GuitarButton + { + public const ushort + StrumUp = GamepadButton.DpadUp, + StrumDown = GamepadButton.DpadDown, + GreenFret = GamepadButton.A, + RedFret = GamepadButton.B, + YellowFret = GamepadButton.Y, + BlueFret = GamepadButton.X, + OrangeFret = GamepadButton.LeftBumper, + LowerFretFlag = GamepadButton.LeftStickPress; + } + + /// + /// Flag definitions for the guitar fret bytes. + /// + static class GuitarFret + { + public const byte + Green = 0x01, + Red = 0x02, + Yellow = 0x04, + Blue = 0x08, + Orange = 0x10; + } + + /// + /// Drums input data offsets relative to the end of the command header. + /// + static class DrumOffset + { + public const int + Buttons = 0, + PadVels = 2, + CymbalVels = 4; + } + + static class DrumButton + { + public const ushort + RedPad = GamepadButton.B, + YellowPad = GamepadButton.Y, + BluePad = GamepadButton.X, + GreenPad = GamepadButton.A, + KickOne = GamepadButton.LeftBumper, + KickTwo = GamepadButton.RightBumper; + } + + /// + /// Definitions for drumkit pad velocity data. + /// + static class DrumPadVel + { + public const byte + Red = 0xF0, + Yellow = 0x0F, + Blue = 0xF0, + Green = 0x0F; + } + + /// + /// Definitions for drumkit pad velocity data. + /// + static class DrumCymVel + { + public const byte + // No red cymbal + // Red = 0, + Yellow = 0xF0, + Blue = 0x0F, + Green = 0xF0; + } +} \ No newline at end of file diff --git a/PacketParsing/PacketParser.cs b/PacketParsing/PacketParser.cs new file mode 100644 index 0000000..321858a --- /dev/null +++ b/PacketParsing/PacketParser.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using SharpPcap; +using RB4InstrumentMapper.Parsing; + +// This is in the regular namespace to keep the other packet parsing stuff from bogging up +// auto-completions when the code in this file is the only code that needs to be referenced elsewhere +namespace RB4InstrumentMapper +{ + /// + /// Emulated devices that can be parsed to. + /// + public enum ParsingMode + { + ViGEmBus = 1, + vJoy = 2 + } + + /// + /// Handles packets from a capture device. + /// + public static class PacketParser + { + /// + /// Device IDs detected during Pcap. + /// + private static Dictionary pcapIds = new Dictionary(); + + /// + /// Gets or sets the current parsing mode. + /// + public static ParsingMode ParseMode { get; set; } = (ParsingMode)0; + + /// + /// Whether or not capture has been started through StartCapture(). + /// + private static bool captureStarted = false; + + /// + /// Starts capture from a given device. + /// + public static bool StartCapture(ILiveDevice device) + { + // Disallow starting capture multiple times without first stopping capture + if (device.Started == true) + { + return false; + } + + // Disallow starting while ParseMode is not set yet + if (ParseMode == (ParsingMode)0) + { + return false; + } + + // Initialize selected device's static client + switch (ParseMode) + { + case ParsingMode.ViGEmBus: + if (!VigemStatic.Initialize()) + { + return false; + } + break; + + case ParsingMode.vJoy: + if (!VjoyStatic.Available) + { + return false; + } + break; + + default: + // Parse mode has not been set yet + return false; + } + + // Open the device + device.Open(new DeviceConfiguration() + { + Snaplen = 45, // Capture small packets + Mode = DeviceModes.Promiscuous | DeviceModes.MaxResponsiveness, // Promiscuous mode with maximum speed + ReadTimeout = 50 // Read timeout + }); + + // Configure event handlers + device.OnPacketArrival += HandlePcapPacket; + device.OnCaptureStopped += OnCaptureStop; + + // Start capture + device.StartCapture(); + captureStarted = true; + return true; + } + + /// + /// Handles a received Pcap packet. + /// + private static void HandlePcapPacket(object sender, PacketCapture packet) + { + // Disallow parsing of packets if StartCapture() hasn't been called yet + if (!captureStarted) + { + return; + } + + // Packet must be at least 30 bytes long + if (packet.Data.Length < (Length.ReceiverHeader + Length.CommandHeader)) + { + return; + } + + // Get device ID + ulong deviceId = (ulong)( + packet.Data[HeaderOffset.DeviceId + 5] | + (packet.Data[HeaderOffset.DeviceId + 4] << 8) | + (packet.Data[HeaderOffset.DeviceId + 3] << 16) | + (packet.Data[HeaderOffset.DeviceId + 2] << 24) | + (packet.Data[HeaderOffset.DeviceId + 1] << 32) | + (packet.Data[HeaderOffset.DeviceId] << 40) + ); + + try + { + // Check if ID has been encountered yet + if (!pcapIds.ContainsKey(deviceId)) + { + pcapIds.Add(deviceId, new XboxDevice(ParseMode)); + Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); + } + + // Strip off receiver header and send the data to be parsed + pcapIds[deviceId].ParseCommand(packet.Data.Slice(Length.ReceiverHeader)); + } + catch (Exception e) + { + Console.WriteLine($"Error while handling packet: {e.GetFirstLine()}"); + Logging.LogException(e); + + // Stop capture + (sender as ILiveDevice).Close(); + return; + } + } + + // TODO: Add libusb support + + /// + /// Cleans up when capture stops. + /// + private static void OnCaptureStop(object sender, CaptureStoppedEventStatus status) + { + foreach (XboxDevice device in pcapIds.Values) + { + device.Close(); + } + + // Clear IDs list + pcapIds.Clear(); + + VigemStatic.Close(); + VjoyStatic.Close(); + + captureStarted = false; + } + } +} diff --git a/PacketParsing/ParsingUtils.cs b/PacketParsing/ParsingUtils.cs new file mode 100644 index 0000000..a5648fe --- /dev/null +++ b/PacketParsing/ParsingUtils.cs @@ -0,0 +1,82 @@ +using System; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Common helper functions used in parsing. + /// + static class ParsingUtils + { + /// + /// Scales this byte to an int. + /// + public static int ScaleToInt32(this byte input) + { + // Duplicate the input value to the higher 8-bit regions by multiplying by a number with the + // first bit of each region set to 1, then OR with the negative bit to ensure correct sign + return (int)((input * 0x01010101) | 0x80000000); + } + + /// + /// Scales this byte to a uint. + /// + public static uint ScaleToUInt32(this byte input) + { + // Duplicate the input value to the higher 8-bit regions by multiplying by a number with the + // first bit of each region set to 1 + return (uint)(input * 0x01010101); + } + + /// + /// Scales a byte to a short. + /// + public static short ScaleToInt16(this byte input) + { + // Duplicate the input value to the higher 8-bit regions by multiplying by a number with the + // first bit of each region set to 1, then OR with the negative bit to ensure correct sign + return (short)((input * 0x0101) | 0x8000); + } + + /// + /// Scales a byte to an unsigned short. + /// + public static ushort ScaleToUInt16(this byte input) + { + // Duplicate the input value to the higher 8-bit regions by multiplying by a number with the + // first bit of each region set to 1 + return (ushort)(input * 0x0101); + } + + /// + /// Gets an unsigned short value from a specified index, parsed as a little-endian value. + /// + public static ushort GetUInt16LE(this ReadOnlySpan span, int index) + { + return (ushort)(span[index + 1] << 8 | span[index]); + } + + /// + /// Gets an unsigned short value from a specified index, parsed as a big-endian value. + /// + public static ushort GetUInt16BE(this ReadOnlySpan span, int index) + { + return (ushort)(span[index] << 8 | span[index + 1]); + } + + /// + /// Gets a short value from a specified index, parsed as a little-endian value. + /// + public static short GetInt16LE(this ReadOnlySpan span, int index) + { + return (short)(span[index + 1] << 8 | span[index]); + } + + /// + /// Gets a short value from a specified index, parsed as a big-endian value. + /// + public static short GetInt16BE(this ReadOnlySpan span, int index) + { + return (short)(span[index] << 8 | span[index + 1]); + } + } +} diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs new file mode 100644 index 0000000..f74fdf9 --- /dev/null +++ b/PacketParsing/VigemMapper.cs @@ -0,0 +1,260 @@ +using System; +using System.Threading.Tasks; +using Nefarius.ViGEm.Client.Targets; +using Nefarius.ViGEm.Client.Targets.Xbox360; + +namespace RB4InstrumentMapper.Parsing +{ + class VigemMapper : IDeviceMapper + { + /// + /// The device to map to. + /// + private IXbox360Controller device; + + /// + /// Creates a new VigemMapper. + /// + public VigemMapper() + { + device = VigemStatic.CreateDevice(); + device.FeedbackReceived += ReceiveUserIndex; + device.Connect(); + device.AutoSubmitReport = false; + } + + /// + /// Performs cleanup on object finalization. + /// + ~VigemMapper() + { + Close(); + } + + /// + /// Temporary event handler for logging the user index of a ViGEm device. + /// + static void ReceiveUserIndex(object sender, Xbox360FeedbackReceivedEventArgs args) + { + // Log the user index + Console.WriteLine($"Created new ViGEmBus device with user index {args.LedNumber}"); + + // Unregister the event handler + (sender as IXbox360Controller).FeedbackReceived -= ReceiveUserIndex; + } + + /// + /// Parses an input report. + /// + public void ParseInput(ReadOnlySpan data, byte length) + { + // Reset report + device.ResetReport(); + + switch (length) + { +#if DEBUG + // Gamepad report parsing for debugging purposes + case Length.Input_Gamepad: + ParseGamepad(data); + break; +#endif + + case Length.Input_Guitar: + ParseGuitar(data); + break; + + case Length.Input_Drums: + ParseDrums(data); + break; + + default: + // Don't parse unknown button data + break; + } + + // Send data + device.SubmitReport(); + } + + /// + /// Parses common button data from an input report. + /// + private void ParseCoreButtons(ushort buttons) + { + // Menu + device.SetButtonState(Xbox360Button.Start, (buttons | GamepadButton.Menu) != 0); + // Options + device.SetButtonState(Xbox360Button.Back, (buttons | GamepadButton.Options) != 0); + + // Dpad + device.SetButtonState(Xbox360Button.Up, (buttons | GamepadButton.DpadUp) != 0); + device.SetButtonState(Xbox360Button.Down, (buttons | GamepadButton.DpadDown) != 0); + device.SetButtonState(Xbox360Button.Left, (buttons | GamepadButton.DpadLeft) != 0); + device.SetButtonState(Xbox360Button.Right, (buttons | GamepadButton.DpadRight) != 0); + + // Other buttons are not mapped here since they may have specific uses + } + +#if DEBUG + // Gamepad report parsing for debugging purposes + /// + /// Parses gamepad input data from an input report. + /// + private void ParseGamepad(ReadOnlySpan data) + { + // Buttons + ushort buttons = data.GetUInt16BE(GamepadOffset.Buttons); + ParseCoreButtons(buttons); + + device.SetButtonState(Xbox360Button.A, (buttons | GamepadButton.A) != 0); + device.SetButtonState(Xbox360Button.B, (buttons | GamepadButton.B) != 0); + device.SetButtonState(Xbox360Button.X, (buttons | GamepadButton.X) != 0); + device.SetButtonState(Xbox360Button.Y, (buttons | GamepadButton.Y) != 0); + + device.SetButtonState(Xbox360Button.LeftShoulder, (buttons | GamepadButton.LeftBumper) != 0); + device.SetButtonState(Xbox360Button.RightShoulder, (buttons | GamepadButton.RightBumper) != 0); + device.SetButtonState(Xbox360Button.LeftThumb, (buttons | GamepadButton.LeftStickPress) != 0); + device.SetButtonState(Xbox360Button.RightThumb, (buttons | GamepadButton.RightStickPress) != 0); + + // Sticks + device.SetAxisValue(Xbox360Axis.LeftThumbX, data.GetInt16LE(GamepadOffset.LeftStickX)); + device.SetAxisValue(Xbox360Axis.LeftThumbY, data.GetInt16LE(GamepadOffset.LeftStickY)); + device.SetAxisValue(Xbox360Axis.RightThumbX, data.GetInt16LE(GamepadOffset.RightStickX)); + device.SetAxisValue(Xbox360Axis.RightThumbY, data.GetInt16LE(GamepadOffset.RightStickY)); + + // Triggers + device.SetSliderValue(Xbox360Slider.LeftTrigger, (byte)(data.GetInt16LE(GamepadOffset.LeftTrigger) >> 8)); + device.SetSliderValue(Xbox360Slider.RightTrigger, (byte)(data.GetInt16LE(GamepadOffset.RightTrigger) >> 8)); + } +#endif + + /// + /// Parses guitar input data from an input report. + /// + private void ParseGuitar(ReadOnlySpan data) + { + // Buttons + ParseCoreButtons(data.GetUInt16BE(GuitarOffset.Buttons)); + + // Frets + byte frets = data[GuitarOffset.UpperFrets]; + frets |= data[GuitarOffset.LowerFrets]; + + device.SetButtonState(Xbox360Button.A, (frets | GuitarFret.Green) != 0); + device.SetButtonState(Xbox360Button.B, (frets | GuitarFret.Red) != 0); + device.SetButtonState(Xbox360Button.Y, (frets | GuitarFret.Yellow) != 0); + device.SetButtonState(Xbox360Button.X, (frets | GuitarFret.Blue) != 0); + device.SetButtonState(Xbox360Button.LeftShoulder, (frets | GuitarFret.Orange) != 0); + + // Whammy + device.SetAxisValue(Xbox360Axis.RightThumbX, data[GuitarOffset.WhammyBar].ScaleToInt16()); + // Tilt + device.SetAxisValue(Xbox360Axis.RightThumbY, data[GuitarOffset.Tilt].ScaleToInt16()); + // Pickup Switch + device.SetSliderValue(Xbox360Slider.LeftTrigger, data[GuitarOffset.PickupSwitch]); + } + + /// + /// Parses drums input data from an input report. + /// + private void ParseDrums(ReadOnlySpan data) + { + // Buttons + ParseCoreButtons(data.GetUInt16BE(DrumOffset.Buttons)); + + // Pads and cymbals + byte redPad = (byte)(data[DrumOffset.PadVels] >> 4); + byte yellowPad = (byte)(data[DrumOffset.PadVels] | DrumPadVel.Yellow); + byte bluePad = (byte)(data[DrumOffset.PadVels + 1] >> 4); + byte greenPad = (byte)(data[DrumOffset.PadVels + 1] | DrumPadVel.Green); + + byte yellowCym = (byte)(data[DrumOffset.CymbalVels] >> 4); + byte blueCym = (byte)(data[DrumOffset.CymbalVels] | DrumPadVel.Blue); + byte greenCym = (byte)(data[DrumOffset.CymbalVels + 1] >> 4); + + // Color flags + device.SetButtonState(Xbox360Button.B, (redPad) != 0); + device.SetButtonState(Xbox360Button.Y, (yellowPad | yellowCym) != 0); + device.SetButtonState(Xbox360Button.X, (bluePad | blueCym) != 0); + device.SetButtonState(Xbox360Button.A, (greenPad | greenCym) != 0); + + // Pad flag + device.SetButtonState(Xbox360Button.RightThumb, + (redPad | yellowPad | bluePad | greenPad) != 0); + // Cymbal flag + device.SetButtonState(Xbox360Button.RightShoulder, + (yellowCym | blueCym | greenCym) != 0); + + // Velocities + device.SetAxisValue( + Xbox360Axis.LeftThumbX, + ByteToVelocity(redPad) + ); + device.SetAxisValue( + Xbox360Axis.LeftThumbY, + ByteToVelocityNegative((byte)(yellowPad | yellowCym)) + ); + device.SetAxisValue( + Xbox360Axis.RightThumbX, + ByteToVelocity((byte)(bluePad | blueCym)) + ); + device.SetAxisValue( + Xbox360Axis.RightThumbY, + ByteToVelocityNegative((byte)(greenPad | greenCym)) + ); + + /// + /// Scales a byte to a drums velocity value. + /// + short ByteToVelocity(byte value) + { + // TODO: Figure out if this is necessary + // Currently, this assumes the max from the kit is 0x04 + value = (byte)(value * 0x40 - 1); + + return (short)( + (~value.ScaleToUInt16()) >> 1 + ); + } + + /// + /// Scales a byte to a negative drums velocity value. + /// + short ByteToVelocityNegative(byte value) + { + // TODO: Figure out if this is necessary + // Currently, this assumes the max from the kit is 0x04 + value = (byte)(value * 0x40 - 1); + + return (short)( + ((~value.ScaleToUInt16()) >> 1) | 0x8000 + ); + } + } + + /// + /// Parses a virtual key report. + /// + public void ParseVirtualKey(ReadOnlySpan data, byte length) + { + // Only respond to the Left Windows keycode, as this is what the guide button reports. + if (data[KeycodeOffset.Keycode] == Keycodes.LeftWin) + { + // Don't reset the report to preserve other button information + // device.ResetReport(); + device.SetButtonState(Xbox360Button.Guide, data[KeycodeOffset.PressedState] != 0); + device.SubmitReport(); + } + } + + /// + /// Performs cleanup for the object. + /// + public void Close() + { + try { device.Disconnect(); } catch {} + device = null; + } + } +} diff --git a/PacketParsing/VigemStatic.cs b/PacketParsing/VigemStatic.cs new file mode 100644 index 0000000..9add02d --- /dev/null +++ b/PacketParsing/VigemStatic.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using SharpPcap; +using SharpPcap.LibPcap; +using Nefarius.ViGEm.Client; +using Nefarius.ViGEm.Client.Targets; +using Nefarius.ViGEm.Client.Exceptions; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Static vJoy client. + /// + static class VigemStatic + { + /// + /// Static ViGEmBus client. + /// + private static ViGEmClient client = null; + + /// + /// Whether or not the ViGEmBus client has been initialized. + /// + public static bool Initialized + { + get => client != null; + } + + /// + /// Initializes the ViGEmBus client. + /// + public static bool Initialize() + { + try + { + client = new ViGEmClient(); + return true; + } + catch + { + client = null; + return false; + } + } + + /// + /// Performs cleanup for the ViGEmBus client. + /// + public static void Close() + { + client?.Dispose(); + client = null; + } + + /// + /// Creates a new Xbox 360 device with the Xbox 360 Rock Band wireless instrument vendor/product IDs. + /// + public static IXbox360Controller CreateDevice() => client.CreateXbox360Controller(0x1BAD, 0x0719); + // Rock Band Guitar: USB\VID_1BAD&PID_0719&IG_00 XUSB\TYPE_00\SUB_86\VEN_1BAD\REV_0002 + // Rock Band Drums: USB\VID_1BAD&PID_0719&IG_02 XUSB\TYPE_00\SUB_88\VEN_1BAD\REV_0002 + // If subtype ID specification through ViGEmBus becomes possible at some point, + // the guitar should be subtype 6, and the drums should be subtype 8 + } +} \ No newline at end of file diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs new file mode 100644 index 0000000..128799d --- /dev/null +++ b/PacketParsing/VjoyMapper.cs @@ -0,0 +1,270 @@ +using System; +using vJoyInterfaceWrap; + +namespace RB4InstrumentMapper.Parsing +{ + class VjoyMapper : IDeviceMapper + { + private vJoy.JoystickState state = new vJoy.JoystickState(); + private uint deviceId = 0; + + /// + /// Creates a new VjoyMapper. + /// + public VjoyMapper() + { + deviceId = VjoyStatic.ClaimNextAvailableDevice(); + state.bDevice = (byte)deviceId; + Console.WriteLine($"Created new vJoy device with device ID {deviceId}"); + } + + /// + /// Performs cleanup on object finalization. + /// + ~VjoyMapper() + { + Close(); + } + + /// + /// Parses an input report. + /// + public void ParseInput(ReadOnlySpan data, byte length) + { + // Reset report + state.ResetState(); + + // Parse the respective device + switch (length) + { +#if DEBUG + // Gamepad report parsing for debugging purposes + case Length.Input_Gamepad: + ParseGamepad(data); + break; +#endif + + case Length.Input_Guitar: + ParseGuitar(data); + break; + + case Length.Input_Drums: + ParseDrums(data); + break; + + default: + break; + } + + // Send data + VjoyStatic.Client.UpdateVJD(deviceId, ref state); + } + + /// + /// Parses common button data from an input report. + /// + private void ParseCoreButtons(ushort buttons) + { + // Menu + if ((buttons | GamepadButton.Menu) != 0) + { + state.Buttons |= VjoyStatic.Button.Fifteen; + } + + // Options + if ((buttons | GamepadButton.Options) != 0) + { + state.Buttons |= VjoyStatic.Button.Sixteen; + } + + // D-pad to POV + if ((buttons | GamepadButton.DpadUp) != 0) + { + if ((buttons | GamepadButton.DpadLeft) != 0) + { + state.bHats = VjoyStatic.PoV.UpLeft; + } + else if ((buttons | GamepadButton.DpadRight) != 0) + { + state.bHats = VjoyStatic.PoV.UpRight; + } + else + { + state.bHats = VjoyStatic.PoV.Up; + } + } + else if ((buttons | GamepadButton.DpadDown) != 0) + { + if ((buttons | GamepadButton.DpadLeft) != 0) + { + state.bHats = VjoyStatic.PoV.DownLeft; + } + else if ((buttons | GamepadButton.DpadRight) != 0) + { + state.bHats = VjoyStatic.PoV.DownRight; + } + else + { + state.bHats = VjoyStatic.PoV.Down; + } + } + else + { + if ((buttons | GamepadButton.DpadLeft) != 0) + { + state.bHats = VjoyStatic.PoV.Left; + } + else if ((buttons | GamepadButton.DpadRight) != 0) + { + state.bHats = VjoyStatic.PoV.Right; + } + else + { + state.bHats = VjoyStatic.PoV.Neutral; + } + } + + // Other buttons are not mapped here since they may have specific uses + } + +#if DEBUG + // Gamepad report parsing for debugging purposes + /// + /// Parses gamepad input data from an input report. + /// + private void ParseGamepad(ReadOnlySpan data) + { + // Buttons + ushort buttons = data.GetUInt16BE(GamepadOffset.Buttons); + ParseCoreButtons(buttons); + + // Left stick + state.AxisX = data.GetInt16LE(GamepadOffset.LeftStickX); + state.AxisY = data.GetInt16LE(GamepadOffset.LeftStickY); + + // Don't map anything else, as there are not enough axes and this is meant for debug purposes only + } +#endif + + /// + /// Parses guitar input data from an input report. + /// + private void ParseGuitar(ReadOnlySpan data) + { + // Buttons + ParseCoreButtons(data.GetUInt16BE(GuitarOffset.Buttons)); + + // Frets + // The fret data aligns with how we want it to be set in the vJoy device, so it can be mapped directly + state.Buttons |= data[GuitarOffset.UpperFrets]; + // Lower frets are mapped on top of the upper frets to allow both sets to be used in-game + state.Buttons |= data[GuitarOffset.LowerFrets]; + + // Whammy + // Value ranges from 0 (not pressed) to 255 (fully pressed) + state.AxisY = data[GuitarOffset.WhammyBar].ScaleToInt32(); + + // Tilt + // Value ranges from 0 to 255 + // It seems to have a threshold of around 0x70 though, + // after a certain point values will get floored to 0 + state.AxisZ = data[GuitarOffset.Tilt].ScaleToInt32(); + + // Pickup switch + // Reported values are 0x00, 0x10, 0x20, 0x30, and 0x40 (ranges from 0 to 64) + state.AxisX = data[GuitarOffset.PickupSwitch].ScaleToInt32(); + } + + /// + /// Parses drums input data from an input report. + /// + private void ParseDrums(ReadOnlySpan data) + { + // Buttons + ParseCoreButtons(data.GetUInt16BE(DrumOffset.Buttons)); + + // Pads + // Red pad + if ((data[DrumOffset.PadVels] | DrumPadVel.Red) != 0) + { + state.Buttons |= VjoyStatic.Button.One; + } + + // Yellow pad + if ((data[DrumOffset.PadVels] | DrumPadVel.Yellow) != 0) + { + state.Buttons |= VjoyStatic.Button.Two; + } + + // Blue pad + if ((data[DrumOffset.PadVels] | DrumPadVel.Blue) != 0) + { + state.Buttons |= VjoyStatic.Button.Three; + } + + // Green pad + if ((data[DrumOffset.PadVels] | DrumPadVel.Green) != 0) + { + state.Buttons |= VjoyStatic.Button.Four; + } + + + // Cymbals + // Yellow cymbal + if ((data[DrumOffset.CymbalVels] | DrumCymVel.Yellow) != 0) + { + state.Buttons |= VjoyStatic.Button.Six; + } + + // Blue cymbal + if ((data[DrumOffset.CymbalVels] | DrumCymVel.Blue) != 0) + { + state.Buttons |= VjoyStatic.Button.Seven; + } + + // Green cymbal + if ((data[DrumOffset.CymbalVels] | DrumCymVel.Green) != 0) + { + state.Buttons |= VjoyStatic.Button.Eight; + } + + + // Kick pedals + // Kick 1 + if ((data[DrumOffset.Buttons] | DrumButton.KickOne) != 0) + { + state.Buttons |= VjoyStatic.Button.Five; + } + + // Kick 2 + if ((data[DrumOffset.Buttons] | DrumButton.KickTwo) != 0) + { + state.Buttons |= VjoyStatic.Button.Nine; + } + } + + /// + /// Parses a virtual key report. + /// + public void ParseVirtualKey(ReadOnlySpan data, byte length) + { + // Only respond to the Left Windows keycode, as this is what the guide button reports. + if (data[KeycodeOffset.Keycode] == Keycodes.LeftWin) + { + // Don't reset the state to preserve other button information + // state.ResetState(); + + state.Buttons |= (data[KeycodeOffset.PressedState] != 0) ? VjoyStatic.Button.Fourteen : 0; + VjoyStatic.Client.UpdateVJD(deviceId, ref state); + } + } + + /// + /// Performs cleanup for the vJoy mapper. + /// + public void Close() + { + VjoyStatic.ReleaseDevice(deviceId); + } + } +} diff --git a/PacketParsing/VjoyStatic.cs b/PacketParsing/VjoyStatic.cs new file mode 100644 index 0000000..189347b --- /dev/null +++ b/PacketParsing/VjoyStatic.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using SharpPcap; +using SharpPcap.LibPcap; +using vJoyInterfaceWrap; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Static vJoy client. + /// + static class VjoyStatic + { + /// + /// Static vJoy client. + /// + private static vJoy client = new vJoy(); + + /// + /// Gets the vJoy client. + /// + public static vJoy Client + { + get => client; + } + + /// + /// Gets whether or not vJoy is available. + /// + public static bool Available + { + get + { + // Check that vJoy is available, and at least one valid device is available + if (!client.vJoyEnabled() || GetNextAvailableID() == 0) + { + return false; + } + + return true; + } + } + + /// + /// Gets the next available device ID. + /// + public static uint GetNextAvailableID() + { + // Get available devices + for (uint deviceId = 1; deviceId <= 16; deviceId++) + { + // Ensure device is available + if (client.GetVJDStatus(deviceId) == VjdStat.VJD_STAT_FREE) + { + // Check that vJoy device is configured correctly + int numButtons = client.GetVJDButtonNumber(deviceId); + int numContPov = client.GetVJDContPovNumber(deviceId); + bool xExists = client.GetVJDAxisExist(deviceId, HID_USAGES.HID_USAGE_X); // X axis + bool yExists = client.GetVJDAxisExist(deviceId, HID_USAGES.HID_USAGE_Y); // Y axis + bool zExists = client.GetVJDAxisExist(deviceId, HID_USAGES.HID_USAGE_Z); // Z axis + + if (numButtons >= 16 && + numContPov >= 1 && + xExists && + yExists && + zExists + ) + { + return deviceId; + } + } + } + + // No devices available + return 0; + } + + /// + /// Claims a vJoy device. + /// + public static uint ClaimNextAvailableDevice() + { + uint deviceId = GetNextAvailableID(); + + if (deviceId == 0) + { + // This shouldn't ever be hit since data is validated before any mapper construction is done, + // this is to ensure that things don't explode if stuff goes horribly wrong + throw new Exception("No new vJoy devices are available."); + } + + client.AcquireVJD(deviceId); + return deviceId; + } + + /// + /// Releases a vJoy device. + /// + public static void ReleaseDevice(uint deviceId) + { + // Ensure device is owned + if (client.GetVJDStatus(deviceId) == VjdStat.VJD_STAT_OWN) + { + client.RelinquishVJD(deviceId); + } + } + + /// + /// Resets the values of this state. + /// + public static void ResetState(this vJoy.JoystickState state) + { + // Only these values are used, don't reset anything else to save on performance + state.Buttons = Button.None; + state.bHats = PoV.Neutral; + state.AxisX = 0; + state.AxisY = 0; + state.AxisZ = 0; + } + + /// + /// Performs cleanup for the vJoy client. + /// + public static void Close() + { + for (uint deviceId = 1; deviceId <= 16; deviceId++) + { + ReleaseDevice(deviceId); + } + + client.ResetAll(); + } + + /// + /// vJoy button flag constants. + /// + public class Button + { + public const uint + None = 0, + One = 1 << 0, + Two = 1 << 1, + Three = 1 << 2, + Four = 1 << 3, + Five = 1 << 4, + Six = 1 << 5, + Seven = 1 << 6, + Eight = 1 << 7, + Nine = 1 << 8, + Ten = 1 << 9, + Eleven = 1 << 10, + Twelve = 1 << 11, + Thirteen = 1 << 12, + Fourteen = 1 << 13, + Fifteen = 1 << 14, + Sixteen = 1 << 15; + } + + /// + /// vJoy PoV hat constants. + /// + public class PoV + { + // vJoy continuous PoV hat values range from 0 to 35999 (measured in 1/100 of a degree). + // The value is measured clockwise, with up being 0. + public const uint + Neutral = 0xFFFFFFFF, + Up = 0, + UpRight = 4500, + Right = 9000, + DownRight = 13500, + Down = 18000, + DownLeft = 22500, + Left = 27000, + UpLeft = 31500; + } + } +} diff --git a/PacketParsing/XboxDevice.cs b/PacketParsing/XboxDevice.cs new file mode 100644 index 0000000..133bf1e --- /dev/null +++ b/PacketParsing/XboxDevice.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using SharpPcap; +using SharpPcap.LibPcap; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Interface for Xbox devices. + /// + class XboxDevice + { + /// + /// Mapper interface to use. + /// + private IDeviceMapper deviceMapper; + + // Lock off parameterless constructor + private XboxDevice() {} + + /// + /// Creates a new XboxDevice with the given device ID and parsing mode. + /// + public XboxDevice(ParsingMode parseMode) + { + switch (parseMode) + { + case ParsingMode.ViGEmBus: + deviceMapper = new VigemMapper(); + break; + + case ParsingMode.vJoy: + deviceMapper = new VjoyMapper(); + break; + } + } + + /// + /// Performs cleanup on object finalization. + /// + ~XboxDevice() + { + Close(); + } + + /// + /// Parses command data from a packet. + /// + public void ParseCommand(ReadOnlySpan commandData) + { + switch (commandData[CommandOffset.CommandId]) + { + case CommandId.Input: + deviceMapper.ParseInput(commandData.Slice(Length.CommandHeader), commandData[CommandOffset.DataLength]); + break; + + // Probably don't actually want to parse the guide button and output it to the device, + // so as to not interfere with Windows processes that use it + // case CommandId.VirtualKey: + // deviceMapper.ParseVirtualKey(commandData.Slice(Length.CommandHeader), commandData[CommandOffset.DataLength]); + // break; + + default: + // Don't do anything with unrecognized command IDs + break; + } + } + + /// + /// Performs cleanup for the device. + /// + public void Close() + { + deviceMapper.Close(); + deviceMapper = null; + } + } +} \ No newline at end of file diff --git a/RB4InstrumentMapper.csproj b/RB4InstrumentMapper.csproj index d6b7636..d0aad05 100644 --- a/RB4InstrumentMapper.csproj +++ b/RB4InstrumentMapper.csproj @@ -105,12 +105,15 @@ Designer - - - - - - + + + + + + + + + From 11fd34398b07b0ca952f12a1c1cfbc810aec2938 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 3 May 2022 17:17:36 -0600 Subject: [PATCH 022/437] Fix ScaleToInt32 and 16 --- PacketParsing/ParsingUtils.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/PacketParsing/ParsingUtils.cs b/PacketParsing/ParsingUtils.cs index a5648fe..5d097ff 100644 --- a/PacketParsing/ParsingUtils.cs +++ b/PacketParsing/ParsingUtils.cs @@ -3,18 +3,18 @@ namespace RB4InstrumentMapper.Parsing { /// - /// Common helper functions used in parsing. + /// Helper functions for parsing. /// static class ParsingUtils { /// - /// Scales this byte to an int. + /// Scales this byte to an int, starting from the negative end. /// public static int ScaleToInt32(this byte input) { // Duplicate the input value to the higher 8-bit regions by multiplying by a number with the - // first bit of each region set to 1, then OR with the negative bit to ensure correct sign - return (int)((input * 0x01010101) | 0x80000000); + // first bit of each region set to 1, then XOR with the negative bit to make the range start from the negative end + return (int)((input * 0x01010101) ^ 0x80000000); } /// @@ -28,13 +28,13 @@ public static uint ScaleToUInt32(this byte input) } /// - /// Scales a byte to a short. + /// Scales a byte to a short, starting from the negative end. /// public static short ScaleToInt16(this byte input) { // Duplicate the input value to the higher 8-bit regions by multiplying by a number with the - // first bit of each region set to 1, then OR with the negative bit to ensure correct sign - return (short)((input * 0x0101) | 0x8000); + // first bit of each region set to 1, then XOR with the negative bit to make the range start from the negative end + return (short)((input * 0x0101) ^ 0x8000); } /// From 2f6396790bb02b2cf7438d300f579997a861eae1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 4 May 2022 19:12:45 -0600 Subject: [PATCH 023/437] Comment out ViGEm drums velocity code Isn't the most essential, and the logic hasn't been verified yet --- PacketParsing/VigemMapper.cs | 70 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index f74fdf9..9994eb2 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -187,50 +187,50 @@ private void ParseDrums(ReadOnlySpan data) (yellowCym | blueCym | greenCym) != 0); // Velocities - device.SetAxisValue( - Xbox360Axis.LeftThumbX, - ByteToVelocity(redPad) - ); - device.SetAxisValue( - Xbox360Axis.LeftThumbY, - ByteToVelocityNegative((byte)(yellowPad | yellowCym)) - ); - device.SetAxisValue( - Xbox360Axis.RightThumbX, - ByteToVelocity((byte)(bluePad | blueCym)) - ); - device.SetAxisValue( - Xbox360Axis.RightThumbY, - ByteToVelocityNegative((byte)(greenPad | greenCym)) - ); + // device.SetAxisValue( + // Xbox360Axis.LeftThumbX, + // ByteToVelocity(redPad) + // ); + // device.SetAxisValue( + // Xbox360Axis.LeftThumbY, + // ByteToVelocityNegative((byte)(yellowPad | yellowCym)) + // ); + // device.SetAxisValue( + // Xbox360Axis.RightThumbX, + // ByteToVelocity((byte)(bluePad | blueCym)) + // ); + // device.SetAxisValue( + // Xbox360Axis.RightThumbY, + // ByteToVelocityNegative((byte)(greenPad | greenCym)) + // ); /// /// Scales a byte to a drums velocity value. /// - short ByteToVelocity(byte value) - { - // TODO: Figure out if this is necessary - // Currently, this assumes the max from the kit is 0x04 - value = (byte)(value * 0x40 - 1); + // short ByteToVelocity(byte value) + // { + // // TODO: Figure out if this is necessary + // // Currently, this assumes the max from the kit is 0x04 + // value = (byte)(value * 0x40 - 1); - return (short)( - (~value.ScaleToUInt16()) >> 1 - ); - } + // return (short)( + // (~value.ScaleToUInt16()) >> 1 + // ); + // } /// /// Scales a byte to a negative drums velocity value. /// - short ByteToVelocityNegative(byte value) - { - // TODO: Figure out if this is necessary - // Currently, this assumes the max from the kit is 0x04 - value = (byte)(value * 0x40 - 1); - - return (short)( - ((~value.ScaleToUInt16()) >> 1) | 0x8000 - ); - } + // short ByteToVelocityNegative(byte value) + // { + // // TODO: Figure out if this is necessary + // // Currently, this assumes the max from the kit is 0x04 + // value = (byte)(value * 0x40 - 1); + + // return (short)( + // ((~value.ScaleToUInt16()) >> 1) | 0x8000 + // ); + // } } /// From edc62d1b4ee9ffa49438c66b041386963748538d Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 5 May 2022 20:11:50 -0600 Subject: [PATCH 024/437] Various parsing fixes and improvements --- PacketParsing/IDeviceMapper.cs | 3 - PacketParsing/PacketDefinitions.cs | 3 - PacketParsing/PacketParser.cs | 139 +++++++++-------------------- PacketParsing/ParseException.cs | 20 +++++ PacketParsing/VigemMapper.cs | 34 +++++-- PacketParsing/VigemStatic.cs | 30 +------ PacketParsing/VjoyMapper.cs | 2 +- PacketParsing/VjoyStatic.cs | 54 ++++------- PacketParsing/XboxDevice.cs | 5 +- RB4InstrumentMapper.csproj | 1 + 10 files changed, 113 insertions(+), 178 deletions(-) create mode 100644 PacketParsing/ParseException.cs diff --git a/PacketParsing/IDeviceMapper.cs b/PacketParsing/IDeviceMapper.cs index 73fc79d..7f6cf2a 100644 --- a/PacketParsing/IDeviceMapper.cs +++ b/PacketParsing/IDeviceMapper.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using SharpPcap; -using SharpPcap.LibPcap; namespace RB4InstrumentMapper.Parsing { diff --git a/PacketParsing/PacketDefinitions.cs b/PacketParsing/PacketDefinitions.cs index ea03908..7796050 100644 --- a/PacketParsing/PacketDefinitions.cs +++ b/PacketParsing/PacketDefinitions.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace RB4InstrumentMapper.Parsing { /// diff --git a/PacketParsing/PacketParser.cs b/PacketParsing/PacketParser.cs index 321858a..b807e86 100644 --- a/PacketParsing/PacketParser.cs +++ b/PacketParsing/PacketParser.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using SharpPcap; using RB4InstrumentMapper.Parsing; // This is in the regular namespace to keep the other packet parsing stuff from bogging up @@ -32,136 +31,86 @@ public static class PacketParser public static ParsingMode ParseMode { get; set; } = (ParsingMode)0; /// - /// Whether or not capture has been started through StartCapture(). + /// Whether or not new devices can be added. /// - private static bool captureStarted = false; - - /// - /// Starts capture from a given device. - /// - public static bool StartCapture(ILiveDevice device) - { - // Disallow starting capture multiple times without first stopping capture - if (device.Started == true) - { - return false; - } - - // Disallow starting while ParseMode is not set yet - if (ParseMode == (ParsingMode)0) - { - return false; - } - - // Initialize selected device's static client - switch (ParseMode) - { - case ParsingMode.ViGEmBus: - if (!VigemStatic.Initialize()) - { - return false; - } - break; - - case ParsingMode.vJoy: - if (!VjoyStatic.Available) - { - return false; - } - break; - - default: - // Parse mode has not been set yet - return false; - } - - // Open the device - device.Open(new DeviceConfiguration() - { - Snaplen = 45, // Capture small packets - Mode = DeviceModes.Promiscuous | DeviceModes.MaxResponsiveness, // Promiscuous mode with maximum speed - ReadTimeout = 50 // Read timeout - }); - - // Configure event handlers - device.OnPacketArrival += HandlePcapPacket; - device.OnCaptureStopped += OnCaptureStop; - - // Start capture - device.StartCapture(); - captureStarted = true; - return true; - } + private static bool canHandleNewDevices = true; /// /// Handles a received Pcap packet. /// - private static void HandlePcapPacket(object sender, PacketCapture packet) + public static void HandlePcapPacket(ReadOnlySpan data) { - // Disallow parsing of packets if StartCapture() hasn't been called yet - if (!captureStarted) - { - return; - } - // Packet must be at least 30 bytes long - if (packet.Data.Length < (Length.ReceiverHeader + Length.CommandHeader)) + if (data.Length < (Length.ReceiverHeader + Length.CommandHeader)) { return; } // Get device ID ulong deviceId = (ulong)( - packet.Data[HeaderOffset.DeviceId + 5] | - (packet.Data[HeaderOffset.DeviceId + 4] << 8) | - (packet.Data[HeaderOffset.DeviceId + 3] << 16) | - (packet.Data[HeaderOffset.DeviceId + 2] << 24) | - (packet.Data[HeaderOffset.DeviceId + 1] << 32) | - (packet.Data[HeaderOffset.DeviceId] << 40) + data[HeaderOffset.DeviceId + 5] | + (data[HeaderOffset.DeviceId + 4] << 8) | + (data[HeaderOffset.DeviceId + 3] << 16) | + (data[HeaderOffset.DeviceId + 2] << 24) | + (data[HeaderOffset.DeviceId + 1] << 32) | + (data[HeaderOffset.DeviceId] << 40) | + 0x0000000000000000 // Ensures that no garbage data gets through to the final number ); - try + // Check if ID has been encountered yet + if (!pcapIds.ContainsKey(deviceId)) { - // Check if ID has been encountered yet - if (!pcapIds.ContainsKey(deviceId)) + if (!canHandleNewDevices) { - pcapIds.Add(deviceId, new XboxDevice(ParseMode)); - Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); + return; } - // Strip off receiver header and send the data to be parsed - pcapIds[deviceId].ParseCommand(packet.Data.Slice(Length.ReceiverHeader)); - } - catch (Exception e) - { - Console.WriteLine($"Error while handling packet: {e.GetFirstLine()}"); - Logging.LogException(e); + XboxDevice device; + try + { + device = new XboxDevice(ParseMode); + } + catch (ParseException ex) + { + canHandleNewDevices = false; + Console.WriteLine("Device limit reached, or an error occured when creating virtual device. No more devices will be registered."); + Console.WriteLine($"Exception: {ex.GetFirstLine()}"); + return; + } - // Stop capture - (sender as ILiveDevice).Close(); - return; + pcapIds.Add(deviceId, device); + Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); } + + // Strip off receiver header and send the data to be parsed + pcapIds[deviceId].ParseCommand(data.Slice(Length.ReceiverHeader)); } // TODO: Add libusb support /// - /// Cleans up when capture stops. + /// Performs cleanup for the parser. /// - private static void OnCaptureStop(object sender, CaptureStoppedEventStatus status) + public static void Close() { + // Clean up devices foreach (XboxDevice device in pcapIds.Values) { device.Close(); } + // Just in case... + // At least in debug builds, stopping capture can take a while and cause the devices to not get disconnected + // if (VjoyStatic.Client.vJoyEnabled()) + // { + // VjoyStatic.FreeAllDevices(); + // } + // Clear IDs list pcapIds.Clear(); - VigemStatic.Close(); - VjoyStatic.Close(); - - captureStarted = false; + // Reset flags + canHandleNewDevices = true; } } } diff --git a/PacketParsing/ParseException.cs b/PacketParsing/ParseException.cs new file mode 100644 index 0000000..f31bc17 --- /dev/null +++ b/PacketParsing/ParseException.cs @@ -0,0 +1,20 @@ +using System; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Used to wrap or create exceptions that should be handled as part of parsing. + /// + class ParseException : Exception + { + public ParseException() + : base() {} + + public ParseException(string message) + : base(message) {} + + public ParseException(string message, Exception innerException) + : base(message, innerException) {} + + } +} \ No newline at end of file diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 9994eb2..993a3b5 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -1,5 +1,5 @@ using System; -using System.Threading.Tasks; +using Nefarius.ViGEm.Client.Exceptions; using Nefarius.ViGEm.Client.Targets; using Nefarius.ViGEm.Client.Targets.Xbox360; @@ -12,6 +12,11 @@ class VigemMapper : IDeviceMapper /// private IXbox360Controller device; + /// + /// Whether or not feedback has been received to indicate that the device has connected. + /// + private bool deviceConnected = false; + /// /// Creates a new VigemMapper. /// @@ -19,7 +24,17 @@ public VigemMapper() { device = VigemStatic.CreateDevice(); device.FeedbackReceived += ReceiveUserIndex; - device.Connect(); + + try + { + device.Connect(); + } + catch (VigemNoFreeSlotException ex) + { + device = null; + throw new ParseException("ViGEmBus device slots are full.", ex); + } + device.AutoSubmitReport = false; } @@ -34,8 +49,11 @@ public VigemMapper() /// /// Temporary event handler for logging the user index of a ViGEm device. /// - static void ReceiveUserIndex(object sender, Xbox360FeedbackReceivedEventArgs args) + void ReceiveUserIndex(object sender, Xbox360FeedbackReceivedEventArgs args) { + // Device has connected + deviceConnected = true; + // Log the user index Console.WriteLine($"Created new ViGEmBus device with user index {args.LedNumber}"); @@ -48,6 +66,12 @@ static void ReceiveUserIndex(object sender, Xbox360FeedbackReceivedEventArgs arg /// public void ParseInput(ReadOnlySpan data, byte length) { + if (!deviceConnected) + { + // Device has not connected yet + return; + } + // Reset report device.ResetReport(); @@ -70,7 +94,7 @@ public void ParseInput(ReadOnlySpan data, byte length) default: // Don't parse unknown button data - break; + return; } // Send data @@ -253,7 +277,7 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length) /// public void Close() { - try { device.Disconnect(); } catch {} + try { device?.Disconnect(); } catch {} device = null; } } diff --git a/PacketParsing/VigemStatic.cs b/PacketParsing/VigemStatic.cs index 9add02d..4278335 100644 --- a/PacketParsing/VigemStatic.cs +++ b/PacketParsing/VigemStatic.cs @@ -1,10 +1,5 @@ -using System; -using System.Collections.Generic; -using SharpPcap; -using SharpPcap.LibPcap; using Nefarius.ViGEm.Client; using Nefarius.ViGEm.Client.Targets; -using Nefarius.ViGEm.Client.Exceptions; namespace RB4InstrumentMapper.Parsing { @@ -26,30 +21,9 @@ public static bool Initialized get => client != null; } - /// - /// Initializes the ViGEmBus client. - /// - public static bool Initialize() - { - try - { - client = new ViGEmClient(); - return true; - } - catch - { - client = null; - return false; - } - } - - /// - /// Performs cleanup for the ViGEmBus client. - /// - public static void Close() + static VigemStatic() { - client?.Dispose(); - client = null; + client = new ViGEmClient(); } /// diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index 128799d..41e39f9 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -15,7 +15,7 @@ public VjoyMapper() { deviceId = VjoyStatic.ClaimNextAvailableDevice(); state.bDevice = (byte)deviceId; - Console.WriteLine($"Created new vJoy device with device ID {deviceId}"); + Console.WriteLine($"Acquired vJoy device with ID of {deviceId}"); } /// diff --git a/PacketParsing/VjoyStatic.cs b/PacketParsing/VjoyStatic.cs index 189347b..ee4fa89 100644 --- a/PacketParsing/VjoyStatic.cs +++ b/PacketParsing/VjoyStatic.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using SharpPcap; -using SharpPcap.LibPcap; using vJoyInterfaceWrap; namespace RB4InstrumentMapper.Parsing @@ -24,23 +20,6 @@ public static vJoy Client get => client; } - /// - /// Gets whether or not vJoy is available. - /// - public static bool Available - { - get - { - // Check that vJoy is available, and at least one valid device is available - if (!client.vJoyEnabled() || GetNextAvailableID() == 0) - { - return false; - } - - return true; - } - } - /// /// Gets the next available device ID. /// @@ -52,7 +31,7 @@ public static uint GetNextAvailableID() // Ensure device is available if (client.GetVJDStatus(deviceId) == VjdStat.VJD_STAT_FREE) { - // Check that vJoy device is configured correctly + // Check that the vJoy device is configured correctly int numButtons = client.GetVJDButtonNumber(deviceId); int numContPov = client.GetVJDContPovNumber(deviceId); bool xExists = client.GetVJDAxisExist(deviceId, HID_USAGES.HID_USAGE_X); // X axis @@ -84,12 +63,14 @@ public static uint ClaimNextAvailableDevice() if (deviceId == 0) { - // This shouldn't ever be hit since data is validated before any mapper construction is done, - // this is to ensure that things don't explode if stuff goes horribly wrong - throw new Exception("No new vJoy devices are available."); + throw new ParseException("No new vJoy devices are available."); + } + + if (!client.AcquireVJD(deviceId)) + { + throw new ParseException($"Could not claim vJoy device {deviceId}."); } - client.AcquireVJD(deviceId); return deviceId; } @@ -105,6 +86,14 @@ public static void ReleaseDevice(uint deviceId) } } + public static void FreeAllDevices() + { + for (uint i = 1; i <= 16; i++) + { + ReleaseDevice(i); + } + } + /// /// Resets the values of this state. /// @@ -118,19 +107,6 @@ public static void ResetState(this vJoy.JoystickState state) state.AxisZ = 0; } - /// - /// Performs cleanup for the vJoy client. - /// - public static void Close() - { - for (uint deviceId = 1; deviceId <= 16; deviceId++) - { - ReleaseDevice(deviceId); - } - - client.ResetAll(); - } - /// /// vJoy button flag constants. /// diff --git a/PacketParsing/XboxDevice.cs b/PacketParsing/XboxDevice.cs index 133bf1e..6bd6df2 100644 --- a/PacketParsing/XboxDevice.cs +++ b/PacketParsing/XboxDevice.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using SharpPcap; -using SharpPcap.LibPcap; namespace RB4InstrumentMapper.Parsing { @@ -71,7 +68,7 @@ public void ParseCommand(ReadOnlySpan commandData) /// public void Close() { - deviceMapper.Close(); + deviceMapper?.Close(); deviceMapper = null; } } diff --git a/RB4InstrumentMapper.csproj b/RB4InstrumentMapper.csproj index d0aad05..ae1302d 100644 --- a/RB4InstrumentMapper.csproj +++ b/RB4InstrumentMapper.csproj @@ -108,6 +108,7 @@ + From 44711395e2fdf43d6483d0602c6a31492a1e2d39 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 5 May 2022 20:27:39 -0600 Subject: [PATCH 025/437] Set assembly version attributes --- Properties/AssemblyInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index cf4731f..1d2a3d9 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.23.0.0")] +[assembly: AssemblyFileVersion("1.23.0.0")] From dabe0bc8d7e75e5e2b2690e47833128796d3e94b Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 5 May 2022 20:27:55 -0600 Subject: [PATCH 026/437] Update main window layout and rewrite logic --- App.config | 29 +- MainWindow/MainWindow.xaml | 54 +- MainWindow/MainWindow.xaml.cs | 1964 ++++++++----------------------- Properties/Settings.Designer.cs | 92 +- Properties/Settings.settings | 29 +- 5 files changed, 541 insertions(+), 1627 deletions(-) diff --git a/App.config b/App.config index 9a2a489..21eff07 100644 --- a/App.config +++ b/App.config @@ -10,32 +10,17 @@ - + - - - - - + + False - - + + False - - - - - - - - - - - - - - + + -1 diff --git a/MainWindow/MainWindow.xaml b/MainWindow/MainWindow.xaml index 103c2b9..1526790 100644 --- a/MainWindow/MainWindow.xaml +++ b/MainWindow/MainWindow.xaml @@ -5,48 +5,32 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:RB4InstrumentMapper" mc:Ignorable="d" - Title="RB4InstrumentMapper" Height="631" Width="800" Loaded="Window_Loaded" Closed="Window_Closed" ResizeMode="CanMinimize"> + Title="RB4InstrumentMapper" Height="499" Width="800" Loaded="Window_Loaded" Closed="Window_Closed" ResizeMode="CanMinimize" MinWidth="800" MinHeight="2"> - private static Dispatcher uiDispatcher = null; - /// - /// Default Pcap packet capture timeout in milliseconds. - /// - private const int DefaultPacketCaptureTimeoutMilliseconds = 50; - /// /// List of available Pcap devices. /// @@ -54,113 +48,38 @@ public partial class MainWindow : Window /// /// Flag indicating that packet capture is active. /// - private static bool packetCaptureActive = false; + private bool packetCaptureActive = false; /// /// Flag indicating if packets should be shown. /// - private static bool packetDebug = false; + private bool packetDebug = false; /// /// Flag indicating if packets should be logged to a file. /// - private static bool packetDebugLog = false; - - /// - /// Flag indicating that guitar 1 ID auto assigning is in progress. - /// - private static bool packetGuitar1AutoAssign = false; - /// - /// Flag indicating that guitar 2 ID auto assigning is in progress. - /// - private static bool packetGuitar2AutoAssign = false; - /// - /// Flag indicating that drum ID auto assigning is in progress. - /// - private static bool packetDrumAutoAssign = false; + private bool packetDebugLog = false; /// /// Common name for Pcap combo box items. /// private const string pcapComboBoxItemName = "pcapDeviceComboBoxItem"; - /// - /// Common name for controller combo box items. - /// - private const string controllerComboBoxItemName = "controllerComboBoxItem"; - /// /// vJoy client. /// - private static vJoy joystick; - - /// - /// ViGEmBus client. - /// - private static ViGEmClient vigemClient = null; - - /// - /// Index of the selected guitar 1 device. - /// - private static uint guitar1DeviceIndex = 0; - - /// - /// Instrument ID for guitar 1. - /// - /// - /// An ID of 0 is assumed to be invalid. - /// - private static ulong guitar1InstrumentId = 0; - - /// - /// Index of the selected guitar 2 device. - /// - private static uint guitar2DeviceIndex = 0; - - /// - /// Instrument ID for guitar 2. - /// - /// - /// An ID of 0 is assumed to be invalid. - /// - private static ulong guitar2InstrumentId = 0; - - /// - /// Index of the selected drum device. - /// - private static uint drumDeviceIndex = 0; - - /// - /// Instrument ID for the drumkit. - /// - /// - /// An ID of 0 is assumed to be invalid. - /// - private static ulong drumInstrumentId = 0; + private static readonly vJoy vjoy = new vJoy(); /// /// Counter for processed packets. /// - private static ulong processedPacketCount = 0; + private ulong processedPacketCount = 0; - /// - /// Dictionary for ViGEmBus controllers. - /// - /// - /// uint = identifier for the instrument (1 for guitar 1, 2 for guitar 2, and 3 for drum) - ///
IXbox360Controller = the controller associated with the instrument.
- ///
- private static Dictionary vigemDictionary = new Dictionary(); - - /// - /// Enumeration for ViGEmBus stuff. - /// - private enum VigemEnum + private enum ControllerType { - Guitar1 = 1, - Guitar2 = 2, - Drum = 3, - DeviceIndex = 17 + None = -1, + vJoy = 0, + VigemBus = 1 } /// @@ -173,6 +92,12 @@ public MainWindow() InitializeComponent(); + Version version = System.Reflection.Assembly.GetEntryAssembly().GetName().Version; + versionLabel.Content = $"v{version.ToString()}"; +#if DEBUG + versionLabel.Content += " Debug"; +#endif + // Capture Dispatcher object for use in callback uiDispatcher = this.Dispatcher; } @@ -187,66 +112,73 @@ private void Window_Loaded(object sender, RoutedEventArgs e) // Connect to console TextBoxConsole.RedirectConsoleToTextBox(messageConsole, displayLinesWithTimestamp: false); - // Initialize dropdowns - PopulatePcapDropdown(); - PopulateControllerDropdowns(); - } - - /// - /// Event handler for AppDomain.CurrentDomain.UnhandledException. - /// - /// - /// Logs the exception info to a file and prompts the user with the exception message. - /// - public static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) - { - // The unhandled exception - Exception unhandledException = args.ExceptionObject as Exception; - - // MessageBox message - StringBuilder message = new StringBuilder(); - message.AppendLine("An unhandled error has occured:"); - message.AppendLine(); - message.AppendLine(unhandledException.GetFirstLine()); - message.AppendLine(); + // Load saved settings + packetDebugCheckBox.IsChecked = Properties.Settings.Default.packetDebug; + packetLogCheckBox.IsChecked = Properties.Settings.Default.packetDebugLog; + int deviceType = controllerDeviceTypeCombo.SelectedIndex = Properties.Settings.Default.controllerDeviceType; - // Create log if it hasn't been created yet - Logging.CreateMainLog(); - // Use an alternate message if log couldn't be created - if (Logging.MainLogExists) + // Check for vJoy + bool vjoyFound = vjoy.vJoyEnabled(); + if (vjoyFound) { - // Log exception - Logging.LogLine("-------------------"); - Logging.LogLine("UNHANDLED EXCEPTION"); - Logging.LogLine("-------------------"); - Logging.LogException(unhandledException); - - // Complete the message buffer - message.AppendLine("A log of the error has been created, do you want to open it?"); - - // Display message - MessageBoxResult result = MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.YesNo, MessageBoxImage.Error); - // If user requested to, open the log - if (result == MessageBoxResult.Yes) + // Log vJoy driver attributes (Vendor Name, Product Name, Version Number) + Console.WriteLine("vJoy found! - Vendor: " + vjoy.GetvJoyManufacturerString() + ", Product: " + vjoy.GetvJoyProductString() + ", Version Number: " + vjoy.GetvJoySerialNumberString()); + if (CountAvailableVjoyDevices() > 0) { - Process.Start(Logging.LogFolderPath); + (controllerDeviceTypeCombo.Items[0] as ComboBoxItem).IsEnabled = true; + } + else + { + // Reset device type selection if it was set to vJoy + if (deviceType == (int)ControllerType.vJoy) + { + controllerDeviceTypeCombo.SelectedIndex = (int)ControllerType.None; + } } } else { - // Complete the message buffer - message.AppendLine("An error log was unable to be created."); + Console.WriteLine("No vJoy driver found, or vJoy is disabled. vJoy selection will be unavailable."); - // Display message - MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.OK, MessageBoxImage.Error); + // Reset device type selection if it was set to vJoy + if (deviceType == (int)ControllerType.vJoy) + { + controllerDeviceTypeCombo.SelectedIndex = (int)ControllerType.None; + } } - // Close the log files - Logging.CloseAll(); + // Check for ViGEmBus + bool vigemFound; + try + { + var vigem = new ViGEmClient(); + vigemFound = true; + Console.WriteLine("ViGEmBus found!"); + vigem.Dispose(); + (controllerDeviceTypeCombo.Items[1] as ComboBoxItem).IsEnabled = true; + } + catch (VigemBusNotFoundException) + { + vigemFound = false; + Console.WriteLine("ViGEmBus not found. ViGEmBus selection will be unavailable."); - // Close program - MessageBox.Show("The program will now shut down.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); - Application.Current.Shutdown(); + // Reset device type selection if it was set to ViGEmBus + if (deviceType == (int)ControllerType.vJoy) + { + controllerDeviceTypeCombo.SelectedIndex = (int)ControllerType.None; + } + } + + // Exit if neither ViGEmBus nor vJoy are installed + if (!(vjoyFound || vigemFound)) + { + MessageBox.Show("No controller emulators found! Please install vJoy or ViGEmBus.\nThe program will now shut down.", "No Controller Emulators Found", MessageBoxButton.OK, MessageBoxImage.Error); + Application.Current.Shutdown(); + return; + } + + // Initialize Pcap dropdown + PopulatePcapDropdown(); } /// @@ -262,717 +194,204 @@ private void Window_Closed(object sender, EventArgs e) StopCapture(); } - // Dispose of the ViGEmBus client - if (vigemClient != null) - { - vigemClient.Dispose(); - } - // Close the log files Logging.CloseAll(); } /// - /// Acquires a vJoy device. + /// Populates the Pcap device combo. /// - /// The vJoy client to use. - /// The device ID of the vJoy device to acquire. - /// True if device was successfully acquired, false otherwise. - static bool AcquirevJoyDevice(vJoy joystick, uint deviceId) + /// + /// Used both when initializing, and when refreshing. + /// + private void PopulatePcapDropdown() { - // Get the state of the requested device - VjdStat status = joystick.GetVJDStatus(deviceId); + // Clear combo list + pcapDeviceCombo.Items.Clear(); + + // Retrieve the device list from the local machine + pcapDeviceList = CaptureDeviceList.Instance; - // Acquire the device - if ((status == VjdStat.VJD_STAT_OWN) || ((status == VjdStat.VJD_STAT_FREE) && (!joystick.AcquireVJD(deviceId)))) + if (pcapDeviceList.Count == 0) { - Console.WriteLine($"Failed to acquire vJoy device number {deviceId}."); - return false; + Console.WriteLine("No Pcap devices found!"); + return; } - else + + // Load saved device name + string currentPcapSelection = Properties.Settings.Default.pcapDevice; + + // Populate combo and print the list + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < pcapDeviceList.Count; i++) { - // Get the number of buttons - int nButtons = joystick.GetVJDButtonNumber(deviceId); + ILiveDevice device = pcapDeviceList[i]; + string itemNumber = $"{i + 1}"; + + sb.Clear(); + sb.Append($"{itemNumber}. "); + if (device.Description != null) + { + sb.Append(device.Description); + sb.Append($" ({device.Name})"); + } + else + { + sb.Append(device.Name); + } + + string deviceName = sb.ToString(); + string itemName = pcapComboBoxItemName + itemNumber; + bool isSelected = device.Name.Equals(currentPcapSelection) || device.Name.Equals(pcapSelectedDevice?.Name); + + if (isSelected) + { + pcapSelectedDevice = device; + } - Console.WriteLine($"Acquired vJoy device number {deviceId} with {nButtons} buttons."); - return true; + pcapDeviceCombo.Items.Add(new ComboBoxItem() + { + Name = itemName, + Content = deviceName, + IsEnabled = true, + IsSelected = isSelected + }); + } + + // Set selection to nothing if saved device not detected + if (pcapSelectedDevice == null) + { + pcapDeviceCombo.SelectedIndex = -1; } + + Console.WriteLine($"Discovered {pcapDeviceList.Count} Pcap devices."); } /// - /// Creates a ViGEmBus device. + /// Populates the controller device list as vJoy. /// - /// The user index to index into the ViGEm dictionary. - /// True if device was successfully created or already exists, false otherwise. - static bool CreateVigemDevice(uint userIndex) + private int CountAvailableVjoyDevices() { - // Don't add duplicate entries - if (vigemDictionary.ContainsKey(userIndex)) + if (!vjoy.vJoyEnabled()) { - // Returns true since it's already added - return true; + return 0; } - IXbox360Controller vigemDevice = vigemClient.CreateXbox360Controller(0x1BAD, 0x0719); // Xbox 360 Rock Band wireless instrument vendor/product IDs - // Rock Band Guitar: USB\VID_1BAD&PID_0719&IG_00 XUSB\TYPE_00\SUB_86\VEN_1BAD\REV_0002 - // Rock Band Drums: USB\VID_1BAD&PID_0719&IG_02 XUSB\TYPE_00\SUB_88\VEN_1BAD\REV_0002 - // If subtype ID specification through ViGEmBus becomes possible at some point, - // the guitar should be subtype 6, and the drums should be subtype 8 - - // Register a temporary event handler for getting the user index - // Prevents a race condition when getting the user index directly from the device - vigemDevice.FeedbackReceived += OnVigemFeedbackReceived; - - try + // Loop through vJoy IDs and populate list + int freeDeviceCount = 0; + for (uint id = 1; id <= 16; id++) { - // Throws one of 5 exceptions: - // VigemBusNotFoundException - // VigemTargetUninitializedException - // VigemAlreadyConnectedException - // VigemNoFreeSlotException - // Win32Exception - // These shouldn't happen in 99% of cases, catching just in case - vigemDevice.Connect(); + if (vjoy.GetVJDStatus(id) == VjdStat.VJD_STAT_FREE) + { + // Check that the vJoy device is configured correctly + int numButtons = vjoy.GetVJDButtonNumber(id); + int numContPov = vjoy.GetVJDContPovNumber(id); + bool xExists = vjoy.GetVJDAxisExist(id, HID_USAGES.HID_USAGE_X); // X axis + bool yExists = vjoy.GetVJDAxisExist(id, HID_USAGES.HID_USAGE_Y); // Y axis + bool zExists = vjoy.GetVJDAxisExist(id, HID_USAGES.HID_USAGE_Z); // Z axis + + if (numButtons >= 16 && + numContPov >= 1 && + xExists && + yExists && + zExists + ) + { + freeDeviceCount++; + } + } } - catch (Exception ex) - { - // Disconnect the device in case it was connected - try { vigemDevice.Disconnect(); } catch {} - // Log the exception - Logging.LogLine("ViGEmBus device creation failed!"); - Logging.LogException(ex); + switch (freeDeviceCount) + { + case 0: + Console.WriteLine($"No vJoy devices available! Please configure some in the Configure vJoy application."); + Console.WriteLine($"Devices must be configured with 16 or more buttons, 1 or more continuous POV hats, and have the X, Y, and Z axes."); + break; - // Create brief exception string - string exceptionMessage = ex.GetFirstLine(); - string instrumentName = Enum.GetName(typeof(VigemEnum), userIndex); - Console.WriteLine($"Could not create ViGEmBus device for {instrumentName}: {exceptionMessage}"); + case 1: + Console.WriteLine($"{freeDeviceCount} vJoy device available."); + break; - return false; + default: + Console.WriteLine($"{freeDeviceCount} vJoy devices available."); + break; } - vigemDictionary.Add(userIndex, vigemDevice); - return true; + return freeDeviceCount; } - /// - /// Temporary event handler for logging the user index of a ViGEm device. - /// - static void OnVigemFeedbackReceived(object sender, Xbox360FeedbackReceivedEventArgs args) + private void SetStartButtonEnabled() { - // Log the user index - Console.WriteLine($"Created new ViGEmBus device with user index {args.LedNumber}"); - - // Unregister the event handler - (sender as IXbox360Controller).FeedbackReceived -= OnVigemFeedbackReceived; + startButton.IsEnabled = ( + controllerDeviceTypeCombo.SelectedIndex != (int)ControllerType.None && + pcapDeviceCombo.SelectedIndex != -1 + ); } /// - /// Populates controller device selection combos. + /// Configures the Pcap device and controller devices, and starts packet capture. /// - /// - /// Used both when initializing and when refreshing. - /// - private void PopulateControllerDropdowns() + private void StartCapture() { - // Initialize the vJoy client - joystick = new vJoy(); - - // Check if vJoy is enabled - bool vjoyFound = joystick.vJoyEnabled(); - if (!vjoyFound) - { - Console.WriteLine("No vJoy driver found, or vJoy is disabled. vJoy selections will be unavailable."); - } - else + // Check if a device has been selected + if (pcapSelectedDevice == null) { - // Log vJoy driver attributes (Vendor Name, Product Name, Version Number) - Console.WriteLine("vJoy found! - Vendor: " + joystick.GetvJoyManufacturerString() + ", Product: " + joystick.GetvJoyProductString() + ", Version Number: " + joystick.GetvJoySerialNumberString()); + Console.WriteLine("Please select a Pcap device from the Pcap dropdown."); + return; } - // Check if ViGEmBus is installed - bool vigemFound = false; - if (vigemClient == null) + // Check if the device is still present + bool deviceStillPresent = false; + foreach (var device in CaptureDeviceList.Instance) { - try - { - vigemClient = new ViGEmClient(); - vigemFound = true; - Console.WriteLine("ViGEmBus found!"); - } - catch (Nefarius.ViGEm.Client.Exceptions.VigemBusNotFoundException) + if (device.Name == pcapSelectedDevice.Name) { - vigemClient = null; - vigemFound = false; - Console.WriteLine("ViGEmBus not found. ViGEmBus selection will be unavailable."); + deviceStillPresent = true; + break; } } - else - { - vigemFound = true; - } - // Check if neither vJoy nor ViGEmBus were found - if (!(vjoyFound || vigemFound)) + if (!deviceStillPresent) { - MessageBox.Show("No controller emulators found! Please install either vJoy or ViGEmBus.\nThe program will now shut down.", "No Controller Emulators Found", MessageBoxButton.OK, MessageBoxImage.Error); - Application.Current.Shutdown(); + // Invalidate selected device (but not the saved preference) + pcapSelectedDevice = null; + + // Notify user + MessageBox.Show( + "Pcap device list has changed and the selected device is no longer present.\nPlease re-select your device from the list and try again.", + "Pcap Device Not Found", + MessageBoxButton.OK, + MessageBoxImage.Exclamation + ); + + // Force a refresh + PopulatePcapDropdown(); return; } - // Get default settings - string currentGuitar1Selection = Properties.Settings.Default.currentGuitar1Selection; - string currentGuitar2Selection = Properties.Settings.Default.currentGuitar2Selection; - string currentDrumSelection = Properties.Settings.Default.currentDrumSelection; + // Enable packet capture active flag + packetCaptureActive = true; - // Reset combo boxes - guitar1Combo.Items.Clear(); - guitar2Combo.Items.Clear(); - drumCombo.Items.Clear(); + // Set window controls + pcapDeviceCombo.IsEnabled = false; + pcapAutoDetectButton.IsEnabled = false; + pcapRefreshButton.IsEnabled = false; + packetDebugCheckBox.IsEnabled = false; + packetLogCheckBox.IsEnabled = false; - // Loop through vJoy IDs and populate dropdowns - int freeDeviceCount = 0; - for (uint id = 1; id <= 16; id++) - { - string vjoyDeviceName = $"vJoy Device {id}"; - string vjoyItemName = $"{controllerComboBoxItemName}{id}"; - bool isEnabled = false; + controllerDeviceTypeCombo.IsEnabled = false; - // Get the state of the requested device - if (vjoyFound) - { - VjdStat status = joystick.GetVJDStatus(id); - switch (status) - { - case VjdStat.VJD_STAT_OWN: - vjoyDeviceName += " (device is already owned by this feeder)"; - break; - case VjdStat.VJD_STAT_FREE: - int numButtons = joystick.GetVJDButtonNumber(id); - int numContPov = joystick.GetVJDContPovNumber(id); - bool xExists = joystick.GetVJDAxisExist(id, HID_USAGES.HID_USAGE_X); // X axis - bool yExists = joystick.GetVJDAxisExist(id, HID_USAGES.HID_USAGE_Y); // Y axis - bool zExists = joystick.GetVJDAxisExist(id, HID_USAGES.HID_USAGE_Z); // Z axis - // Check that vJoy device is configured correctly - if (numButtons >= 16 && - numContPov >= 1 && - xExists && - yExists && - zExists - ) - { - isEnabled = true; - freeDeviceCount++; - } - else - { - vjoyDeviceName += " (device misconfigured, use 16 buttons, X/Y/Z axes, and 1 continuous POV)"; - } - break; - case VjdStat.VJD_STAT_BUSY: - vjoyDeviceName += " (device is already owned by another feeder)"; - break; - case VjdStat.VJD_STAT_MISS: - vjoyDeviceName += " (device is not installed or disabled)"; - break; - default: - vjoyDeviceName += " (general error)"; - break; - }; - } - else - { - vjoyDeviceName += " (vJoy disabled/not found)"; - } + startButton.Content = "Stop"; - // Add combo item to combos - // Guitar 1 combo - ComboBoxItem vjoyComboBoxItem = new ComboBoxItem(); - vjoyComboBoxItem.Content = vjoyDeviceName; - vjoyComboBoxItem.Name = vjoyItemName; - vjoyComboBoxItem.IsEnabled = isEnabled; - vjoyComboBoxItem.IsSelected = vjoyItemName.Equals(currentGuitar1Selection) && isEnabled; - guitar1Combo.Items.Add(vjoyComboBoxItem); - - // Guitar 2 combo - vjoyComboBoxItem = new ComboBoxItem(); - vjoyComboBoxItem.Content = vjoyDeviceName; - vjoyComboBoxItem.Name = vjoyItemName; - vjoyComboBoxItem.IsEnabled = isEnabled; - vjoyComboBoxItem.IsSelected = vjoyItemName.Equals(currentGuitar2Selection) && isEnabled; - guitar2Combo.Items.Add(vjoyComboBoxItem); - - // Drum combo - vjoyComboBoxItem = new ComboBoxItem(); - vjoyComboBoxItem.Content = vjoyDeviceName; - vjoyComboBoxItem.Name = vjoyItemName; - vjoyComboBoxItem.IsEnabled = isEnabled; - vjoyComboBoxItem.IsSelected = vjoyItemName.Equals(currentDrumSelection) && isEnabled; - drumCombo.Items.Add(vjoyComboBoxItem); - } + // Enable packet count display + packetsProcessedLabel.Visibility = Visibility.Visible; + packetsProcessedCountLabel.Visibility = Visibility.Visible; + packetsProcessedCountLabel.Content = "0"; + processedPacketCount = 0; - if (vjoyFound) - { - Console.WriteLine($"Discovered {freeDeviceCount} free vJoy devices."); - } - - // Create ViGEmBus device dropdown item - string vigemDeviceName = $"ViGEmBus Device"; - if (!vigemFound) - { - vigemDeviceName += " (ViGEmBus not found)"; - } - string vigemItemName = $"{controllerComboBoxItemName}17"; - - // Add ViGEmBus combo item - // Guitar 1 combo - ComboBoxItem vigemComboBoxItem = new ComboBoxItem(); - vigemComboBoxItem.Content = vigemDeviceName; - vigemComboBoxItem.Name = vigemItemName; - vigemComboBoxItem.IsEnabled = vigemFound; - vigemComboBoxItem.IsSelected = vigemItemName.Equals(currentGuitar1Selection) && vigemFound; - guitar1Combo.Items.Add(vigemComboBoxItem); - - // Guitar 2 combo - vigemComboBoxItem = new ComboBoxItem(); - vigemComboBoxItem.Content = vigemDeviceName; - vigemComboBoxItem.Name = vigemItemName; - vigemComboBoxItem.IsEnabled = vigemFound; - vigemComboBoxItem.IsSelected = vigemItemName.Equals(currentGuitar2Selection) && vigemFound; - guitar2Combo.Items.Add(vigemComboBoxItem); - - // Drum combo - vigemComboBoxItem = new ComboBoxItem(); - vigemComboBoxItem.Content = vigemDeviceName; - vigemComboBoxItem.Name = vigemItemName; - vigemComboBoxItem.IsEnabled = vigemFound; - vigemComboBoxItem.IsSelected = vigemItemName.Equals(currentDrumSelection) && vigemFound; - drumCombo.Items.Add(vigemComboBoxItem); - - // Add None option - // Guitar 1 combo - string noneItemName = $"{controllerComboBoxItemName}0"; - ComboBoxItem noneComboBoxItem = new ComboBoxItem(); - noneComboBoxItem.Content = "None"; - noneComboBoxItem.Name = noneItemName; - noneComboBoxItem.IsEnabled = true; - noneComboBoxItem.IsSelected = noneItemName.Equals(currentGuitar1Selection) || string.IsNullOrEmpty(currentGuitar1Selection); // Default to this selection - guitar1Combo.Items.Add(noneComboBoxItem); - - // Guitar 2 combo - noneComboBoxItem = new ComboBoxItem(); - noneComboBoxItem.Content = "None"; - noneComboBoxItem.Name = noneItemName; - noneComboBoxItem.IsEnabled = true; - noneComboBoxItem.IsSelected = noneItemName.Equals(currentGuitar2Selection) || string.IsNullOrEmpty(currentGuitar2Selection); // Default to this selection - guitar2Combo.Items.Add(noneComboBoxItem); - - // Drum combo - noneComboBoxItem = new ComboBoxItem(); - noneComboBoxItem.Content = "None"; - noneComboBoxItem.Name = noneItemName; - noneComboBoxItem.IsEnabled = true; - noneComboBoxItem.IsSelected = noneItemName.Equals(currentDrumSelection) || string.IsNullOrEmpty(currentDrumSelection); // Default to this selection - drumCombo.Items.Add(noneComboBoxItem); - - // Load default device IDs - // Guitar 1 - string hexString = Properties.Settings.Default.currentGuitar1Id; - // Limit ID to 12 characters, as they are 48 bits, not 64 - if (hexString.Length > 12) - { - Console.WriteLine("Attempted to load an invalid Guitar 1 instrument ID. The ID has been reset."); - guitar1InstrumentId = 0; - } - else if (!ParsingHelpers.HexStringToUInt64(hexString, out guitar1InstrumentId)) - { - if (string.IsNullOrEmpty(hexString)) - { - guitar1InstrumentId = 0; - } - else - { - guitar1InstrumentId = 0; - Console.WriteLine("Attempted to load an invalid Guitar 1 instrument ID. The ID has been reset."); - } - } - guitar1IdTextBox.Text = (guitar1InstrumentId == 0) ? string.Empty : hexString; - - // Guitar 2 - hexString = Properties.Settings.Default.currentGuitar2Id; - // Limit ID to 12 characters, as they are 48 bits, not 64 - if (hexString.Length > 12) - { - Console.WriteLine("Attempted to load an invalid Guitar 2 instrument ID. The ID has been reset."); - guitar1InstrumentId = 0; - } - else if (!ParsingHelpers.HexStringToUInt64(hexString, out guitar2InstrumentId)) - { - if (string.IsNullOrEmpty(hexString)) - { - guitar2InstrumentId = 0; - } - else - { - guitar2InstrumentId = 0; - Console.WriteLine("Attempted to load an invalid Guitar 2 instrument ID. The ID has been reset."); - } - } - guitar2IdTextBox.Text = (guitar2InstrumentId == 0) ? string.Empty : hexString; - - // Drum - hexString = Properties.Settings.Default.currentDrumId; - // Limit ID to 12 characters, as they are 48 bits, not 64 - if (hexString.Length > 12) - { - Console.WriteLine("Attempted to load an invalid Drum instrument ID. The ID has been reset."); - guitar1InstrumentId = 0; - } - else if (!ParsingHelpers.HexStringToUInt64(hexString, out drumInstrumentId)) - { - if (string.IsNullOrEmpty(hexString)) - { - drumInstrumentId = 0; - } - else - { - drumInstrumentId = 0; - Console.WriteLine("Attempted to load an invalid Drum instrument ID. The ID has been reset."); - } - } - drumIdTextBox.Text = (drumInstrumentId == 0) ? string.Empty : hexString; - } - - /// - /// Populates the Pcap device combo. - /// - /// - /// Used both when initializing, and when refreshing. - /// - private void PopulatePcapDropdown() - { - // Disable auto-detect ID buttons - guitar1IdAutoDetectButton.IsEnabled = false; - guitar2IdAutoDetectButton.IsEnabled = false; - drumIdAutoDetectButton.IsEnabled = false; - - // Clear combo list - pcapDeviceCombo.Items.Clear(); - - // Retrieve the device list from the local machine - try - { - pcapDeviceList = CaptureDeviceList.Instance; - } - catch (InvalidOperationException) - { - Console.WriteLine("Could not retrieve list of Pcap interfaces."); - return; - } - - if (pcapDeviceList == null || pcapDeviceList.Count == 0) - { - Console.WriteLine("No Pcap interfaces found!"); - return; - } - - // Get default settings - string currentPcapSelection = Properties.Settings.Default.currentPcapSelection; - - // Populate combo and print the list - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < pcapDeviceList.Count; i++) - { - ILiveDevice device = pcapDeviceList[i]; - sb.Clear(); - string itemNumber = $"{i + 1}"; - sb.Append($"{itemNumber}. "); - if (device.Description != null) - { - sb.Append(device.Description); - } - sb.Append($" ({device.Name})"); - - string deviceName = sb.ToString(); - string itemName = pcapComboBoxItemName + itemNumber; - ComboBoxItem comboBoxItem = new ComboBoxItem(); - comboBoxItem.Name = itemName; - comboBoxItem.Content = deviceName; - comboBoxItem.IsEnabled = true; - bool isSelected = device.Name.Equals(currentPcapSelection) || device.Name.Equals(pcapSelectedDevice?.Name); - comboBoxItem.IsSelected = isSelected; - if (isSelected) - { - // Re-enable auto-detect ID buttons and assign internal device reference - guitar1IdAutoDetectButton.IsEnabled = true; - guitar2IdAutoDetectButton.IsEnabled = true; - drumIdAutoDetectButton.IsEnabled = true; - pcapSelectedDevice = device; - } - - pcapDeviceCombo.Items.Add(comboBoxItem); - } - - // Set selection to nothing if saved device not detected - if (pcapSelectedDevice == null) - { - pcapDeviceCombo.SelectedIndex = -1; - } - - // Preset debugging flags - string currentPacketDebugState = Properties.Settings.Default.currentPacketDebugState; - if (currentPacketDebugState == "true") - { - packetDebugCheckBox.IsChecked = true; - } - - string currentPacketLogState = Properties.Settings.Default.currentPacketDebugLogState; - if (currentPacketLogState == "true") - { - packetLogCheckBox.IsChecked = true; - } - - Console.WriteLine($"Discovered {pcapDeviceList.Count} Pcap devices."); - } - - /// - /// Handles Pcap device selection changes. - /// - /// - /// - private void pcapDeviceCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - // Get selected combo box item - ComboBoxItem typeItem = (ComboBoxItem)pcapDeviceCombo.SelectedItem; - // Attempting to use typeItem's properties while null will cause a NullReferenceException - if (typeItem == null) - { - Properties.Settings.Default.currentPcapSelection = String.Empty; - Properties.Settings.Default.Save(); - return; - } - string itemName = typeItem.Name; - - // Get index of selected Pcap device - int pcapDeviceIndex = -1; - if (int.TryParse(itemName.Substring(pcapComboBoxItemName.Length), out pcapDeviceIndex)) - { - // Adjust index count (UI->Logical) - pcapDeviceIndex -= 1; - - // Assign device - pcapSelectedDevice = pcapDeviceList[pcapDeviceIndex]; - Console.WriteLine($"Selected Pcap device {pcapSelectedDevice.Description}"); - - // Enable auto-detect ID buttons - guitar1IdAutoDetectButton.IsEnabled = true; - guitar2IdAutoDetectButton.IsEnabled = true; - drumIdAutoDetectButton.IsEnabled = true; - - // Remember selected Pcap device's name - Properties.Settings.Default.currentPcapSelection = pcapSelectedDevice.Name; - Properties.Settings.Default.Save(); - } - } - - /// - /// Handles guitar 1 controller selection changes. - /// - /// - /// - private void guitar1Combo_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - // Only allow a device to be selected by one selection, unless it is the None or ViGEmBus Device selections - if (guitar1Combo.SelectedIndex < (int)VigemEnum.DeviceIndex) - { - if (guitar1Combo.SelectedIndex == guitar2Combo.SelectedIndex) - { - guitar2Combo.SelectedIndex = -1; - } - - if (guitar1Combo.SelectedIndex == drumCombo.SelectedIndex) - { - drumCombo.SelectedIndex = -1; - } - } - - // Get selected guitar device - ComboBoxItem typeItem = (ComboBoxItem)guitar1Combo.SelectedItem; - // Attempting to use typeItem's properties while null will cause a NullReferenceException - if (typeItem == null) - { - Properties.Settings.Default.currentGuitar1Selection = String.Empty; - Properties.Settings.Default.Save(); - return; - } - string itemName = typeItem.Name; - - // Get index of selected guitar device - if (uint.TryParse(typeItem.Name.Substring(controllerComboBoxItemName.Length), out guitar1DeviceIndex)) - { - // Remember selected guitar device - Properties.Settings.Default.currentGuitar1Selection = itemName; - Properties.Settings.Default.Save(); - } - } - - /// - /// Handles guitar 2 controller selection changes. - /// - /// - /// - private void guitar2Combo_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - // Only allow a device to be selected by one selection, unless it is the None or ViGEmBus Device selections - if (guitar2Combo.SelectedIndex < (int)VigemEnum.DeviceIndex) - { - if (guitar2Combo.SelectedIndex == guitar1Combo.SelectedIndex) - { - guitar1Combo.SelectedIndex = -1; - } - - if (guitar2Combo.SelectedIndex == drumCombo.SelectedIndex) - { - drumCombo.SelectedIndex = -1; - } - } - - // Get selected guitar device - ComboBoxItem typeItem = (ComboBoxItem)guitar2Combo.SelectedItem; - // Attempting to use typeItem's properties while null will cause a NullReferenceException - if (typeItem == null) - { - Properties.Settings.Default.currentGuitar2Selection = String.Empty; - Properties.Settings.Default.Save(); - return; - } - string itemName = typeItem.Name; - - // Get index of selected guitar device - if (uint.TryParse(typeItem.Name.Substring(controllerComboBoxItemName.Length), out guitar2DeviceIndex)) - { - // Remember selected guitar device - Properties.Settings.Default.currentGuitar2Selection = itemName; - Properties.Settings.Default.Save(); - } - } - - /// - /// Handles drum controller selection changes. - /// - /// - /// - private void drumCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - // Only allow a device to be selected by one selection, unless it is the None or ViGEmBus Device selections - if (drumCombo.SelectedIndex < (int)VigemEnum.DeviceIndex) - { - if (drumCombo.SelectedIndex == guitar1Combo.SelectedIndex) - { - guitar1Combo.SelectedIndex = -1; - } - - if (drumCombo.SelectedIndex == guitar2Combo.SelectedIndex) - { - guitar2Combo.SelectedIndex = -1; - } - } - - // Get selected drum device - ComboBoxItem typeItem = (ComboBoxItem)drumCombo.SelectedItem; - // Attempting to use typeItem's properties while null will cause a NullReferenceException - if (typeItem == null) - { - Properties.Settings.Default.currentDrumSelection = String.Empty; - Properties.Settings.Default.Save(); - return; - } - string itemName = typeItem.Name; - - // Get index of selected drum device - if (uint.TryParse(typeItem.Name.Substring(controllerComboBoxItemName.Length), out drumDeviceIndex)) - { - // Remember selected drum device - Properties.Settings.Default.currentDrumSelection = itemName; - Properties.Settings.Default.Save(); - } - } - - /// - /// Configures the Pcap device and controller devices, and starts packet capture. - /// - private void StartCapture() - { - // Check if a device has been selected - if (pcapSelectedDevice == null) - { - Console.WriteLine("Please select a Pcap device from the Pcap dropdown."); - return; - } - - // Retrieve the device list from the local machine - var allDevices = CaptureDeviceList.Instance; - - // Check if the device is still present - bool deviceStillPresent = false; - foreach (var device in allDevices) - { - if (device.Name == pcapSelectedDevice.Name) - { - deviceStillPresent = true; - break; - } - } - - if (!deviceStillPresent) - { - // Invalidate selected device (but not the saved preference) - pcapSelectedDevice = null; - // Notify user - MessageBox.Show( - "Pcap device list has changed and the selected device is no longer present.\nPlease re-select your device from the list and try again.", - "Pcap Device Not Found", - MessageBoxButton.OK, - MessageBoxImage.Exclamation - ); - // Force a refresh - PopulatePcapDropdown(); - return; - } - - // Enable packet capture active flag - packetCaptureActive = true; - - // Disable window controls - pcapDeviceCombo.IsEnabled = false; - pcapAutoDetectButton.IsEnabled = false; - pcapRefreshButton.IsEnabled = false; - packetDebugCheckBox.IsEnabled = false; - packetLogCheckBox.IsEnabled = false; - - guitar1Combo.IsEnabled = false; - guitar1IdTextBox.IsEnabled = false; - guitar1IdAutoDetectButton.IsEnabled = false; - - guitar2Combo.IsEnabled = false; - guitar2IdTextBox.IsEnabled = false; - guitar2IdAutoDetectButton.IsEnabled = false; - - drumCombo.IsEnabled = false; - drumIdTextBox.IsEnabled = false; - drumIdAutoDetectButton.IsEnabled = false; - - controllerRefreshButton.IsEnabled = false; - startButton.Content = "Stop"; - - // Enable packet count display - packetsProcessedLabel.Visibility = Visibility.Visible; - packetsProcessedCountLabel.Visibility = Visibility.Visible; - packetsProcessedCountLabel.Content = "0"; - processedPacketCount = 0; - - // Initialize packet log - if (packetDebugLog) + // Initialize packet log + if (packetDebugLog) { if (!Logging.CreatePacketLog()) { @@ -981,122 +400,25 @@ private void StartCapture() } } - // Initialize vJoy - bool vjoyResult; - if (joystick != null) - { - // Reset buttons and axis - joystick.ResetAll(); - - // Acquire vJoy devices - if (guitar1DeviceIndex > 0 && guitar1DeviceIndex < (int)VigemEnum.DeviceIndex) - { - vjoyResult = AcquirevJoyDevice(joystick, guitar1DeviceIndex); - if (!vjoyResult) - { - StopCapture(); - return; - } - } - - if (guitar2DeviceIndex > 0 && guitar2DeviceIndex < (int)VigemEnum.DeviceIndex) - { - vjoyResult = AcquirevJoyDevice(joystick, guitar2DeviceIndex); - if (!vjoyResult) - { - StopCapture(); - return; - } - } - - if (drumDeviceIndex > 0 && drumDeviceIndex < (int)VigemEnum.DeviceIndex) - { - vjoyResult = AcquirevJoyDevice(joystick, drumDeviceIndex); - if (!vjoyResult) - { - StopCapture(); - return; - } - } - } + // Register handler for packet debugging/logging and status reporting + pcapSelectedDevice.OnPacketArrival += OnPacketArrival; - // Initialize ViGEmBus devices - bool vigemResult; - if (vigemClient != null) + // Open the device + pcapSelectedDevice.Open(new DeviceConfiguration() { - // Create ViGEmBus devices for each - if (guitar1DeviceIndex == (int)VigemEnum.DeviceIndex) - { - vigemResult = CreateVigemDevice((uint)VigemEnum.Guitar1); - if (!vigemResult) - { - StopCapture(); - return; - } - } - - if (guitar2DeviceIndex == (int)VigemEnum.DeviceIndex) - { - vigemResult = CreateVigemDevice((uint)VigemEnum.Guitar2); - if (!vigemResult) - { - StopCapture(); - return; - } - } - - if (drumDeviceIndex == (int)VigemEnum.DeviceIndex) - { - vigemResult = CreateVigemDevice((uint)VigemEnum.Drum); - if (!vigemResult) - { - StopCapture(); - return; - } - } - } + Snaplen = 45, // Capture small packets + Mode = DeviceModes.Promiscuous | DeviceModes.MaxResponsiveness, // Promiscuous mode with maximum speed + ReadTimeout = 50 // Read timeout + }); - // Register handler for packet debugging/logging and status reporting + // Configure packet receive event handler pcapSelectedDevice.OnPacketArrival += OnPacketArrival; - + // Start capture - PacketParser.StartCapture(pcapSelectedDevice); + pcapSelectedDevice.StartCapture(); Console.WriteLine($"Listening on {pcapSelectedDevice.Description}..."); } - /// - /// Handles captured packets. - /// - /// The received packet - private void OnPacketArrival(object sender, PacketCapture packet) - { - // Debugging (if enabled) - if (packetDebug) - { - RawCapture raw = packet.GetPacket(); - string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; - Console.WriteLine(packetLogString); - - if (packetDebugLog) - { - Logging.LogPacket(packetLogString); - } - } - - // Status reporting (slow) - if ((processedPacketCount < 10) || - ((processedPacketCount < 100) && (processedPacketCount % 10 == 0)) || - (processedPacketCount % 100 == 0)) - { - // Update UI - uiDispatcher.Invoke(() => - { - string ulongString = processedPacketCount.ToString("N0"); - packetsProcessedCountLabel.Content = ulongString; - }); - } - } - /// /// Stops packet capture/mapping and resets Pcap/controller objects. /// @@ -1110,30 +432,7 @@ private void StopCapture() pcapSelectedDevice.Close(); } - // Release drum device - if (joystick != null && drumDeviceIndex > 0) - { - joystick.RelinquishVJD(drumDeviceIndex); - } - - // Release guitar 1 device - if (joystick != null && guitar1DeviceIndex > 0) - { - joystick.RelinquishVJD(guitar1DeviceIndex); - } - - // Release guitar 2 device - if (joystick != null && guitar2DeviceIndex > 0) - { - joystick.RelinquishVJD(guitar2DeviceIndex); - } - - // Disconnect ViGEmBus controllers - foreach (IXbox360Controller device in vigemDictionary.Values) - { - device?.Disconnect(); - } - vigemDictionary.Clear(); + PacketParser.Close(); // Store whether or not the packet log was created bool doPacketLogMessage = Logging.PacketLogExists; @@ -1143,26 +442,15 @@ private void StopCapture() // Disable packet capture active flag packetCaptureActive = false; - // Enable window controls + // Set window controls pcapDeviceCombo.IsEnabled = true; pcapAutoDetectButton.IsEnabled = true; pcapRefreshButton.IsEnabled = true; packetDebugCheckBox.IsEnabled = true; packetLogCheckBox.IsEnabled = true; - guitar1Combo.IsEnabled = true; - guitar1IdTextBox.IsEnabled = true; - guitar1IdAutoDetectButton.IsEnabled = true; + controllerDeviceTypeCombo.IsEnabled = true; - guitar2Combo.IsEnabled = true; - guitar2IdTextBox.IsEnabled = true; - guitar2IdAutoDetectButton.IsEnabled = true; - - drumCombo.IsEnabled = true; - drumIdTextBox.IsEnabled = true; - drumIdAutoDetectButton.IsEnabled = true; - - controllerRefreshButton.IsEnabled = true; startButton.Content = "Start"; // Disable packet count display @@ -1171,661 +459,393 @@ private void StopCapture() packetsProcessedCountLabel.Content = string.Empty; processedPacketCount = 0; + // Force a refresh of the controller textbox + controllerDeviceTypeCombo_SelectionChanged(null, null); + Console.WriteLine("Stopped capture."); if (doPacketLogMessage) { - Console.WriteLine($"Packet logs may be found in {Logging.PacketLogFolderPath}."); + Console.WriteLine($"Packet logs may be found in {Logging.PacketLogFolderPath}"); } } /// - /// Handles the click of the Start button. + /// Handles captured packets. /// - /// - /// - private void startButton_Click(object sender, RoutedEventArgs e) + /// The received packet + private void OnPacketArrival(object sender, PacketCapture packet) { - if (!packetCaptureActive) + try { - StartCapture(); + PacketParser.HandlePcapPacket(packet.Data); } - else + catch (Exception ex) { - StopCapture(); - } - } - - /// - /// Handles the packet debug checkbox being checked. - /// - /// - /// - private void packetDebugCheckBox_Checked(object sender, RoutedEventArgs e) - { - packetDebug = true; - packetLogCheckBox.IsEnabled = true; - packetDebugLog = packetLogCheckBox.IsChecked.GetValueOrDefault(); - - // Remember selected packet debug state - Properties.Settings.Default.currentPacketDebugState = "true"; - Properties.Settings.Default.Save(); - } - - /// - /// Handles the packet debug checkbox being unchecked. - /// - /// - /// - private void packetDebugCheckBox_Unchecked(object sender, RoutedEventArgs e) - { - packetDebug = false; - packetLogCheckBox.IsEnabled = false; - packetDebugLog = false; - - // Remember selected packet debug state - Properties.Settings.Default.currentPacketDebugState = "false"; - Properties.Settings.Default.Save(); - } - - /// - /// Handles the packet debug checkbox being checked. - /// - /// - /// - private void packetLogCheckBox_Checked(object sender, RoutedEventArgs e) - { - packetDebugLog = true; - - // Remember selected packet debug state - Properties.Settings.Default.currentPacketDebugLogState = "true"; - Properties.Settings.Default.Save(); - } - - /// - /// Handles the packet debug checkbox being unchecked. - /// - /// - /// - private void packetLogCheckBox_Unchecked(object sender, RoutedEventArgs e) - { - packetDebugLog = false; - - // Remember selected packet debug state - Properties.Settings.Default.currentPacketDebugLogState = "false"; - Properties.Settings.Default.Save(); - } - - /// - /// Handles the click of the Pcap Refresh button. - /// - /// - /// - private void pcapRefreshButton_Click(object sender, RoutedEventArgs e) - { - // Re-populate dropdown - PopulatePcapDropdown(); - } - - /// - /// Handles the click of the controller Refresh button. - /// - /// - /// - private void controllerRefreshButton_Click(object sender, RoutedEventArgs e) - { - // Re-populate dropdowns - PopulateControllerDropdowns(); - } - - /// - /// Handles the guitar 1 instrument ID textbox having its text changed. - /// - /// - /// - private void guitar1IdTextBox_TextChanged(object sender, TextChangedEventArgs e) - { - // Reset assignment - guitar1InstrumentId = 0; + Console.WriteLine($"Error while handling packet: {ex.GetFirstLine()}"); + Logging.LogException(ex); - // Set new ID - string hexString = guitar1IdTextBox.Text.ToUpperInvariant(); - // Limit ID to 12 characters, as they are 48 bits, not 64 - if (hexString.Length > 12) - { - Console.WriteLine("Hex ID must be 12 characters or less."); + // Stop capture + StopCapture(); return; } - ulong enteredId; - if (ParsingHelpers.HexStringToUInt64(hexString, out enteredId)) + // Debugging (if enabled) + if (packetDebug) { - if (enteredId == 0) - { - // Clear ID - Console.WriteLine("Cleared Hex ID for Guitar 1."); - hexString = string.Empty; - } - else if (enteredId == guitar2InstrumentId) - { - // Enforce unique guitar instrument ID - Console.WriteLine("Guitar 1 ID must be different from Guitar 2 ID."); - hexString = string.Empty; - } - else + RawCapture raw = packet.GetPacket(); + string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; + Console.WriteLine(packetLogString); + + if (packetDebugLog) { - // Set ID - guitar1InstrumentId = enteredId; - Console.WriteLine($"Guitar 1 instrument Hex ID set to {hexString}."); + Logging.LogPacket(packetLogString); } } - else if (string.IsNullOrEmpty(hexString)) - { - // Clear ID - Console.WriteLine("Cleared Hex ID for Guitar 1."); - hexString = string.Empty; - } - else - { - Console.WriteLine("Invalid Hex ID entered for Guitar 1."); - hexString = string.Empty; - } - // Update UI - uiDispatcher.Invoke(() => + // Status reporting (slow) + if ((processedPacketCount < 10) || + ((processedPacketCount < 100) && (processedPacketCount % 10 == 0)) || + (processedPacketCount % 100 == 0)) { - guitar1IdTextBox.Text = hexString; - }); - - // Remember guitar 1 ID - Properties.Settings.Default.currentGuitar1Id = hexString; - Properties.Settings.Default.Save(); + // Update UI + uiDispatcher.Invoke(() => + { + string ulongString = processedPacketCount.ToString("N0"); + packetsProcessedCountLabel.Content = ulongString; + }); + } } /// - /// Handles the guitar 2 instrument ID textbox having its text changed. + /// Handles Pcap device selection changes. /// /// /// - private void guitar2IdTextBox_TextChanged(object sender, TextChangedEventArgs e) + private void pcapDeviceCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) { - // Reset assignment - guitar2InstrumentId = 0; - - // Set new ID - string hexString = guitar2IdTextBox.Text.ToUpperInvariant(); - // Limit ID to 12 characters, as they are 48 bits, not 64 - if (hexString.Length > 12) + // Get selected combo box item + ComboBoxItem selection = pcapDeviceCombo.SelectedItem as ComboBoxItem; + if (selection == null) { - Console.WriteLine("Hex ID must be 12 characters or less."); + // Disable start button + startButton.IsEnabled = false; + + // Clear saved device + Properties.Settings.Default.pcapDevice = String.Empty; + Properties.Settings.Default.Save(); return; } + string itemName = selection.Name; - ulong enteredId; - if (ParsingHelpers.HexStringToUInt64(hexString, out enteredId)) - { - if (enteredId == 0) - { - // Clear ID - Console.WriteLine("Cleared Hex ID for Guitar 2."); - hexString = string.Empty; - } - else if (enteredId == guitar1InstrumentId) - { - // Enforce unique guitar instrument ID - Console.WriteLine("Guitar 2 ID must be different from Guitar 1 ID."); - hexString = string.Empty; - } - else - { - // Set ID - guitar2InstrumentId = enteredId; - Console.WriteLine($"Guitar 2 instrument Hex ID set to {hexString}."); - } - } - else if (string.IsNullOrEmpty(hexString)) - { - // Clear ID - Console.WriteLine("Cleared Hex ID for Guitar 2."); - hexString = string.Empty; - } - else + // Get index of selected Pcap device + int pcapDeviceIndex = -1; + if (int.TryParse(itemName.Substring(pcapComboBoxItemName.Length), out pcapDeviceIndex)) { - Console.WriteLine("Invalid Hex ID entered for Guitar 2."); - hexString = string.Empty; - } + // Adjust index count (UI->Logical) + pcapDeviceIndex -= 1; - // Update UI - uiDispatcher.Invoke(() => - { - guitar2IdTextBox.Text = hexString; - }); + // Assign device + pcapSelectedDevice = pcapDeviceList[pcapDeviceIndex]; + Console.WriteLine($"Selected Pcap device {pcapSelectedDevice.Description}"); - // Remember guitar 2 ID - Properties.Settings.Default.currentGuitar2Id = hexString; - Properties.Settings.Default.Save(); + // Enable start button + SetStartButtonEnabled(); + + // Remember selected Pcap device's name + Properties.Settings.Default.pcapDevice = pcapSelectedDevice.Name; + Properties.Settings.Default.Save(); + } } /// - /// Handles the drum instrument ID textbox having its text changed. + /// Handles the click of the Start button. /// /// /// - private void drumIdTextBox_TextChanged(object sender, TextChangedEventArgs e) + private void startButton_Click(object sender, RoutedEventArgs e) { - // Reset assignment - drumInstrumentId = 0; - - // Set new ID - string hexString = drumIdTextBox.Text.ToUpperInvariant(); - // Limit ID to 12 characters, as they are 48 bits, not 64 - if (hexString.Length > 12) - { - Console.WriteLine("Hex ID must be 12 characters or less."); - return; - } - - ulong enteredId; - if (ParsingHelpers.HexStringToUInt64(hexString, out enteredId)) - { - if (enteredId == 0) - { - // Clear ID - Console.WriteLine("Cleared Hex ID for Drum."); - hexString = string.Empty; - } - else - { - // Set ID - drumInstrumentId = enteredId; - Console.WriteLine($"Drum instrument Hex ID set to {hexString}."); - } - } - else if (string.IsNullOrEmpty(hexString)) + if (!packetCaptureActive) { - // Clear ID - Console.WriteLine("Cleared Hex ID for Drum."); - hexString = string.Empty; + StartCapture(); } else { - Console.WriteLine("Invalid Hex ID entered for Drum."); - hexString = string.Empty; + StopCapture(); } - - // Update UI - uiDispatcher.Invoke(() => - { - drumIdTextBox.Text = hexString; - }); - - // Remember drum ID - Properties.Settings.Default.currentDrumId = hexString; - Properties.Settings.Default.Save(); } /// - /// Handles the Pcap auto-detect button being clicked. + /// Handles the packet debug checkbox being checked. /// /// /// - private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) + private void packetDebugCheckBox_Checked(object sender, RoutedEventArgs e) { - // Prompt user to unplug their receiver - if (MessageBox.Show("Unplug your receiver, then click OK.", "Auto-Detect Receiver", MessageBoxButton.OKCancel) == MessageBoxResult.OK) - { - // Get the list of devices for when receiver is unplugged - CaptureDeviceList notPlugged = null; - try - { - notPlugged = CaptureDeviceList.Instance; - } - catch (InvalidOperationException ex) - { - MessageBox.Show("Could not auto-assign; an error occured.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); - Logging.LogLine("Error during auto-assignment:"); - Logging.LogException(ex); - return; - } - - // Prompt user to plug in their receiver - if (MessageBox.Show("Now plug in your receiver, wait a bit for it to register, then click OK.\n(A 1-second delay will be taken after clicking OK to ensure that it registers.)", "Auto-Detect Receiver", MessageBoxButton.OKCancel) == MessageBoxResult.OK) - { - // Wait for a moment before getting the new list, seems like clicking OK too quickly after plugging it in makes it not get registered - Thread.Sleep(1000); - - // Get the list of devices for when receiver is plugged in - CaptureDeviceList plugged = null; - try - { - plugged = CaptureDeviceList.Instance; - } - catch (InvalidOperationException ex) - { - MessageBox.Show("Could not auto-assign; an error occured.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); - Logging.LogLine("Error during auto-assignment:"); - Logging.LogException(ex); - return; - } - - // Check for devices in the new list that aren't in the initial list - // Have to check names specifically, because doing `notPlugged.Contains(newDevice)` - // always adds every device in the list, even if it's not new - - // Get device names for both not plugged and plugged lists - List notPluggedNames = new List(); - List pluggedNames = new List(); - foreach (ILiveDevice oldDevice in notPlugged) - { - notPluggedNames.Add(oldDevice.Name); - } - foreach (ILiveDevice newDevice in plugged) - { - pluggedNames.Add(newDevice.Name); - } - - // Compare the lists and find what notPlugged doesn't contain - List newNames = new List(); - foreach (string pluggedName in pluggedNames) - { - if (!notPluggedNames.Contains(pluggedName)) - { - newNames.Add(pluggedName); - } - } - - // Create a list of new devices based on the list of new device names - List newDevices = new List(); - foreach (ILiveDevice newDevice in plugged) - { - if (newNames.Contains(newDevice.Name)) - { - newDevices.Add(newDevice); - } - } - - // If there's (strictly) one new device, assign it - if (newDevices.Count == 1) - { - // Assign the new device - pcapSelectedDevice = newDevices.First(); - - // Remember the new device - Properties.Settings.Default.currentPcapSelection = pcapSelectedDevice.Name; - Properties.Settings.Default.Save(); - } - else - { - // If there's more than one, don't auto-assign any of them - if (newDevices.Count > 1) - { - MessageBox.Show("Could not auto-assign; more than one new device was detected.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); - } - // If there's no new ones, don't do anything - else if (newDevices.Count == 0) - { - MessageBox.Show("Could not auto-assign; no new devices were detected.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); - } - } + packetDebug = true; + packetLogCheckBox.IsEnabled = true; + packetDebugLog = packetLogCheckBox.IsChecked.GetValueOrDefault(); - // Refresh the dropdown - PopulatePcapDropdown(); - } - } + // Remember selected packet debug state + Properties.Settings.Default.packetDebug = true; + Properties.Settings.Default.Save(); } /// - /// Handles the guitar 1 ID auto-detect button being clicked. + /// Handles the packet debug checkbox being unchecked. /// /// /// - private void guitar1IdAutoDetectButton_Click(object sender, RoutedEventArgs e) + private void packetDebugCheckBox_Unchecked(object sender, RoutedEventArgs e) { - // Set auto-assign flag - packetGuitar1AutoAssign = true; + packetDebug = false; + packetLogCheckBox.IsEnabled = false; + packetDebugLog = false; - // Auto-detect ID - AutoDetectID(); + // Remember selected packet debug state + Properties.Settings.Default.packetDebug = false; + Properties.Settings.Default.Save(); } /// - /// Handles the guitar 2 ID auto-detect button being clicked. + /// Handles the packet debug checkbox being checked. /// /// /// - private void guitar2IdAutoDetectButton_Click(object sender, RoutedEventArgs e) + private void packetLogCheckBox_Checked(object sender, RoutedEventArgs e) { - // Set auto-assign flag - packetGuitar2AutoAssign = true; + packetDebugLog = true; - // Auto-detect ID - AutoDetectID(); + // Remember selected packet debug state + Properties.Settings.Default.packetDebugLog = true; + Properties.Settings.Default.Save(); } /// - /// Handles the drum ID auto-detect button being clicked. + /// Handles the packet debug checkbox being unchecked. /// /// /// - private void drumIdAutoDetectButton_Click(object sender, RoutedEventArgs e) + private void packetLogCheckBox_Unchecked(object sender, RoutedEventArgs e) { - // Set auto-assign flag - packetDrumAutoAssign = true; + packetDebugLog = false; - // Auto-detect ID - AutoDetectID(); + // Remember selected packet debug state + Properties.Settings.Default.packetDebugLog = false; + Properties.Settings.Default.Save(); } /// - /// Automatically detects the instrument ID of a given packet. + /// Handles the click of the Pcap Refresh button. /// /// /// - private async void AutoDetectID() + private void pcapRefreshButton_Click(object sender, RoutedEventArgs e) { - // Disable all controls and show the auto-assign instruction label - uiDispatcher.Invoke(() => - { - pcapDeviceCombo.IsEnabled = false; - pcapAutoDetectButton.IsEnabled = false; - pcapRefreshButton.IsEnabled = false; - packetDebugCheckBox.IsEnabled = false; - packetLogCheckBox.IsEnabled = false; - - guitar1Combo.IsEnabled = false; - guitar1IdTextBox.IsEnabled = false; - guitar1IdAutoDetectButton.IsEnabled = false; - - guitar2Combo.IsEnabled = false; - guitar2IdTextBox.IsEnabled = false; - guitar2IdAutoDetectButton.IsEnabled = false; - - drumCombo.IsEnabled = false; - drumIdTextBox.IsEnabled = false; - drumIdAutoDetectButton.IsEnabled = false; - - controllerRefreshButton.IsEnabled = false; - controllerAutoAssignLabel.Visibility = Visibility.Visible; - - startButton.IsEnabled = false; - }); - - // Await the result of auto-assignment - bool result = await Task.Run(Read_AutoDetectID); - if (!result) - { - MessageBox.Show("Failed to auto-assign ID.", "Auto-Assign ID", MessageBoxButton.OK, MessageBoxImage.Warning); - } + // Re-populate dropdown + PopulatePcapDropdown(); + } - // Re-enable all controls and hide the auto-assign instruction label - uiDispatcher.Invoke(() => + private void controllerDeviceTypeCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + // Set parsing mode + switch (controllerDeviceTypeCombo.SelectedIndex) { - pcapDeviceCombo.IsEnabled = true; - pcapAutoDetectButton.IsEnabled = true; - pcapRefreshButton.IsEnabled = true; - packetDebugCheckBox.IsEnabled = true; - packetLogCheckBox.IsEnabled = true; - - guitar1Combo.IsEnabled = true; - guitar1IdTextBox.IsEnabled = true; - guitar1IdAutoDetectButton.IsEnabled = true; - - guitar2Combo.IsEnabled = true; - guitar2IdTextBox.IsEnabled = true; - guitar2IdAutoDetectButton.IsEnabled = true; + // vJoy + case 0: + if (CountAvailableVjoyDevices() > 0) + { + PacketParser.ParseMode = ParsingMode.vJoy; + Properties.Settings.Default.controllerDeviceType = (int)ControllerType.vJoy; + } + else + { + // Reset device type selection + // The parse mode and saved setting will get set automatically since setting this fires off this handler again + controllerDeviceTypeCombo.SelectedIndex = -1; + return; + } + break; - drumCombo.IsEnabled = true; - drumIdTextBox.IsEnabled = true; - drumIdAutoDetectButton.IsEnabled = true; + // ViGEmBus + case 1: + PacketParser.ParseMode = ParsingMode.ViGEmBus; + Properties.Settings.Default.controllerDeviceType = (int)ControllerType.VigemBus; + break; - controllerRefreshButton.IsEnabled = true; - controllerAutoAssignLabel.Visibility = Visibility.Hidden; + default: + PacketParser.ParseMode = (ParsingMode)0; + Properties.Settings.Default.controllerDeviceType = (int)ControllerType.None; + break; + } - startButton.IsEnabled = true; - }); + // Save setting + Properties.Settings.Default.controllerDeviceType = controllerDeviceTypeCombo.SelectedIndex; + Properties.Settings.Default.Save(); - // Disable auto-assignment flags - packetGuitar1AutoAssign = false; - packetGuitar2AutoAssign = false; - packetDrumAutoAssign = false; + // Enable start button + SetStartButtonEnabled(); } /// - /// Task function for auto-detecting an instrument ID. + /// Handles the Pcap auto-detect button being clicked. /// /// /// - private bool Read_AutoDetectID() + private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) { - // Assume failure - bool result = false; + MessageBoxResult result; - // Check if a device has been selected - if (pcapSelectedDevice == null) + // Prompt user to unplug their receiver + result = MessageBox.Show( + "Unplug your receiver, then click OK.\n(A 1-second delay will be taken to ensure that it registers as disconnected.)", + "Auto-Detect Receiver", + MessageBoxButton.OKCancel + ); + if (result == MessageBoxResult.Cancel) { - Console.WriteLine("Please select a Pcap device from the Pcap dropdown."); - return false; + return; } - // Retrieve the device list from the local machine - var allDevices = CaptureDeviceList.Instance; + // Wait for a moment before getting the new list to ensure the device is registered + Thread.Sleep(1000); - // Check if the device is still present - bool deviceStillPresent = false; - foreach (ILiveDevice device in allDevices) + // Get the list of devices for when receiver is unplugged + CaptureDeviceList notPlugged = CaptureDeviceList.Instance; + + // Prompt user to plug in their receiver + result = MessageBox.Show( + "Now plug in your receiver, wait a bit for it to register, then click OK.\n(A 1-second delay will be taken to ensure that it registers as connected.)", + "Auto-Detect Receiver", + MessageBoxButton.OKCancel + ); + if (result == MessageBoxResult.Cancel) { - if (device.Name == pcapSelectedDevice.Name) - { - deviceStillPresent = true; - break; - } + return; } - if (!deviceStillPresent) + // Wait for a moment before getting the new list to ensure the device is registered + Thread.Sleep(1000); + + // Get the list of devices for when receiver is plugged in + CaptureDeviceList plugged = CaptureDeviceList.Instance; + + // Get device names for both not plugged and plugged lists + List notPluggedNames = new List(); + List pluggedNames = new List(); + foreach (ILiveDevice oldDevice in notPlugged) + { + notPluggedNames.Add(oldDevice.Name); + } + foreach (ILiveDevice newDevice in plugged) + { + pluggedNames.Add(newDevice.Name); + } + + // Compare the lists and find what notPlugged doesn't contain + List newNames = new List(); + foreach (string pluggedName in pluggedNames) { - uiDispatcher.Invoke((Action)(() => + if (!notPluggedNames.Contains(pluggedName)) { - // Invalidate selected device - pcapSelectedDevice = null; - // Notify user - Console.WriteLine("Pcap device list has changed and the selected device is no longer present. Please re-select your device from the list and try again."); - // Force a refresh - PopulatePcapDropdown(); - })); - return false; + newNames.Add(pluggedName); + } } - // Open the device - pcapSelectedDevice.Open(new DeviceConfiguration() + // Create a list of new devices based on the list of new device names + List newDevices = new List(); + foreach (ILiveDevice newDevice in plugged) + { + if (newNames.Contains(newDevice.Name)) { - Snaplen = 45, // small packets - Mode = DeviceModes.Promiscuous | DeviceModes.MaxResponsiveness, // promiscuous mode with maximum speed - ReadTimeout = DefaultPacketCaptureTimeoutMilliseconds // read timeout + newDevices.Add(newDevice); } - ); + } - // Receive packet - PacketCapture packet; - int attempts = 6; - while (attempts > 0) + // If there's (strictly) one new device, assign it + if (newDevices.Count == 1) { - var status = pcapSelectedDevice.GetNextPacket(out packet); - if (status == GetPacketStatus.PacketRead) + // Assign the new device + pcapSelectedDevice = newDevices.First(); + + // Remember the new device + Properties.Settings.Default.pcapDevice = pcapSelectedDevice.Name; + Properties.Settings.Default.Save(); + } + else + { + // If there's more than one, don't auto-assign any of them + if (newDevices.Count > 1) { - RawCapture raw = packet.GetPacket(); - byte[] data = raw.Data; - // RawCapture cannot be null here, as an instance is always created in the GetPacket function - // if (raw == null) - // { - // return; - // } - - // Debugging (if enabled) - if (packetDebug) - { - string packetHexString = ParsingHelpers.ByteArrayToHexString(data); - Console.WriteLine(raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + packetHexString); - } + MessageBox.Show("Could not auto-assign; more than one new device was detected.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); + } + // If there's no new ones, don't do anything + else if (newDevices.Count == 0) + { + MessageBox.Show("Could not auto-assign; no new devices were detected.", "Auto-Detect Receiver", MessageBoxButton.OK, MessageBoxImage.Warning); + } + } - // Get ID from packet as Hex string - string idString = null; - if (raw.PacketLength == 40 || raw.PacketLength == 36) - { - // String representation: AA BB CC DD EE FF - uint id = (uint)( - data[15] | // FF - (data[14] << 8) | // EE - (data[13] << 16) | // DD - (data[12] << 24) | // CC - (data[11] << 32) | // BB - (data[10] << 40) // AA - ); - - idString = Convert.ToString(id, 16).ToUpperInvariant(); - } + // Refresh the dropdown + PopulatePcapDropdown(); + } - // Check assignment flags and packet length - if (packetGuitar1AutoAssign && data.Length == 40) - { - // Update UI (assigns instrument ID) - uiDispatcher.Invoke((Action)(() => - { - guitar1IdTextBox.Text = idString; - })); + /// + /// Event handler for AppDomain.CurrentDomain.UnhandledException. + /// + /// + /// Logs the exception info to a file and prompts the user with the exception message. + /// + public static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) + { + // The unhandled exception + Exception unhandledException = args.ExceptionObject as Exception; - result = true; - } - else if (packetGuitar2AutoAssign && data.Length == 40) - { - // Update UI (assigns instrument ID) - uiDispatcher.Invoke((Action)(() => - { - guitar2IdTextBox.Text = idString; - })); + // MessageBox message + StringBuilder message = new StringBuilder(); + message.AppendLine("An unhandled error has occured:"); + message.AppendLine(); + message.AppendLine(unhandledException.GetFirstLine()); + message.AppendLine(); - result = true; - } - else if (packetDrumAutoAssign && data.Length == 36) - { - // Update UI (assigns instrument ID) - uiDispatcher.Invoke((Action)(() => - { - drumIdTextBox.Text = idString; - })); + // Create log if it hasn't been created yet + Logging.CreateMainLog(); + // Use an alternate message if log couldn't be created + if (Logging.MainLogExists) + { + // Log exception + Logging.LogLine("-------------------"); + Logging.LogLine("UNHANDLED EXCEPTION"); + Logging.LogLine("-------------------"); + Logging.LogException(unhandledException); - result = true; - } - break; + // Complete the message buffer + message.AppendLine("A log of the error has been created, do you want to open it?"); + + // Display message + MessageBoxResult result = MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.YesNo, MessageBoxImage.Error); + // If user requested to, open the log + if (result == MessageBoxResult.Yes) + { + Process.Start(Logging.LogFolderPath); } + } + else + { + // Complete the message buffer + message.AppendLine("An error log was unable to be created."); - // Short pause before retry - Thread.Sleep(333); - attempts--; + // Display message + MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.OK, MessageBoxImage.Error); } - - // Close device - pcapSelectedDevice.Close(); - return result; + // Close the log files + Logging.CloseAll(); + // Save settings + Properties.Settings.Default.Save(); + + // Close program + MessageBox.Show("The program will now shut down.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + Application.Current.Shutdown(); } } } diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs index e1a2c8d..cbc549d 100644 --- a/Properties/Settings.Designer.cs +++ b/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace RB4InstrumentMapper.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.10.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.1.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -26,108 +26,48 @@ public static Settings Default { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("")] - public string currentPcapSelection { + public string pcapDevice { get { - return ((string)(this["currentPcapSelection"])); + return ((string)(this["pcapDevice"])); } set { - this["currentPcapSelection"] = value; + this["pcapDevice"] = value; } } [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string currentGuitar1Selection { - get { - return ((string)(this["currentGuitar1Selection"])); - } - set { - this["currentGuitar1Selection"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string currentDrumSelection { - get { - return ((string)(this["currentDrumSelection"])); - } - set { - this["currentDrumSelection"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string currentPacketDebugState { - get { - return ((string)(this["currentPacketDebugState"])); - } - set { - this["currentPacketDebugState"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string currentGuitar2Selection { - get { - return ((string)(this["currentGuitar2Selection"])); - } - set { - this["currentGuitar2Selection"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string currentGuitar1Id { - get { - return ((string)(this["currentGuitar1Id"])); - } - set { - this["currentGuitar1Id"] = value; - } - } - - [global::System.Configuration.UserScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string currentGuitar2Id { + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool packetDebug { get { - return ((string)(this["currentGuitar2Id"])); + return ((bool)(this["packetDebug"])); } set { - this["currentGuitar2Id"] = value; + this["packetDebug"] = value; } } [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string currentDrumId { + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool packetDebugLog { get { - return ((string)(this["currentDrumId"])); + return ((bool)(this["packetDebugLog"])); } set { - this["currentDrumId"] = value; + this["packetDebugLog"] = value; } } [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.DefaultSettingValueAttribute("")] - public string currentPacketDebugLogState { + [global::System.Configuration.DefaultSettingValueAttribute("-1")] + public int controllerDeviceType { get { - return ((string)(this["currentPacketDebugLogState"])); + return ((int)(this["controllerDeviceType"])); } set { - this["currentPacketDebugLogState"] = value; + this["controllerDeviceType"] = value; } } } diff --git a/Properties/Settings.settings b/Properties/Settings.settings index 3ec0513..c049988 100644 --- a/Properties/Settings.settings +++ b/Properties/Settings.settings @@ -2,32 +2,17 @@ - + - - - - - + + False - - + + False - - - - - - - - - - - - - - + + -1 \ No newline at end of file From 2c25eaa0f7df6f91ea7e243a244d37c189e58172 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 9 May 2022 15:18:40 -0600 Subject: [PATCH 027/437] Fix device ID recognition --- PacketParsing/PacketParser.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/PacketParsing/PacketParser.cs b/PacketParsing/PacketParser.cs index b807e86..26399dd 100644 --- a/PacketParsing/PacketParser.cs +++ b/PacketParsing/PacketParser.cs @@ -47,14 +47,21 @@ public static void HandlePcapPacket(ReadOnlySpan data) } // Get device ID - ulong deviceId = (ulong)( + // Have to do it in chunks to avoid bit shift wraparounds and sign extension weirdness from casting + ulong deviceIdLow = (ulong)( data[HeaderOffset.DeviceId + 5] | (data[HeaderOffset.DeviceId + 4] << 8) | (data[HeaderOffset.DeviceId + 3] << 16) | - (data[HeaderOffset.DeviceId + 2] << 24) | - (data[HeaderOffset.DeviceId + 1] << 32) | - (data[HeaderOffset.DeviceId] << 40) | - 0x0000000000000000 // Ensures that no garbage data gets through to the final number + (data[HeaderOffset.DeviceId + 2] << 24) + ); + ulong deviceIdHigh = (ulong)( + data[HeaderOffset.DeviceId + 1] | + (data[HeaderOffset.DeviceId] << 8) + ); + + ulong deviceId = ( + deviceIdLow | + (deviceIdHigh << 32) ); // Check if ID has been encountered yet From 16a6d8ad6f702c58ccbd743e4d1b8d56fceb3ffb Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 9 May 2022 15:20:54 -0600 Subject: [PATCH 028/437] Fix packet count not incrementing --- MainWindow/MainWindow.xaml.cs | 2 +- PacketParsing/PacketParser.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index f4858fb..b7569ae 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -477,7 +477,7 @@ private void OnPacketArrival(object sender, PacketCapture packet) { try { - PacketParser.HandlePcapPacket(packet.Data); + PacketParser.HandlePcapPacket(packet.Data, ref processedPacketCount); } catch (Exception ex) { diff --git a/PacketParsing/PacketParser.cs b/PacketParsing/PacketParser.cs index 26399dd..090ff81 100644 --- a/PacketParsing/PacketParser.cs +++ b/PacketParsing/PacketParser.cs @@ -38,7 +38,7 @@ public static class PacketParser /// /// Handles a received Pcap packet. /// - public static void HandlePcapPacket(ReadOnlySpan data) + public static void HandlePcapPacket(ReadOnlySpan data, ref ulong processedCount) { // Packet must be at least 30 bytes long if (data.Length < (Length.ReceiverHeader + Length.CommandHeader)) @@ -91,6 +91,7 @@ public static void HandlePcapPacket(ReadOnlySpan data) // Strip off receiver header and send the data to be parsed pcapIds[deviceId].ParseCommand(data.Slice(Length.ReceiverHeader)); + processedCount++; } // TODO: Add libusb support From 3aea093c19fa9a656c55e2546220be057ff60af3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 10 May 2022 00:24:12 -0600 Subject: [PATCH 029/437] Fix threading issue when stopping capture due to unhandled exception --- MainWindow/MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index b7569ae..f6f6a0b 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -485,7 +485,7 @@ private void OnPacketArrival(object sender, PacketCapture packet) Logging.LogException(ex); // Stop capture - StopCapture(); + uiDispatcher.Invoke(StopCapture); return; } From 36d74771c86f5e3a6d51120734fe30af50b92d16 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 10 May 2022 01:01:12 -0600 Subject: [PATCH 030/437] Update some logging stuff Renamed functions to better indicate which log they write to Don't create packet log if it doesn't exist, it gets created on capture start if requested by the user Add Packet_Write function for using packetLog.Write Don't log ThreadAbortExceptions that happen during packet capture --- LogUtils.cs | 18 ++++++++++++------ MainWindow/MainWindow.xaml.cs | 21 +++++++++++---------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/LogUtils.cs b/LogUtils.cs index 32cdfa8..20355ab 100644 --- a/LogUtils.cs +++ b/LogUtils.cs @@ -144,7 +144,7 @@ public static bool CreatePacketLog() /// /// Writes a line to the log file. /// - public static void LogLine(string text) + public static void Main_WriteLine(string text) { // Create log file if it hasn't been made yet CreateMainLog(); @@ -155,7 +155,7 @@ public static void LogLine(string text) /// /// Writes an exception, and any additonal info, to the log. /// - public static void LogException(Exception ex, string addtlInfo = null) + public static void Main_WriteException(Exception ex, string addtlInfo = null) { // Create log file if it hasn't been made yet CreateMainLog(); @@ -163,12 +163,18 @@ public static void LogException(Exception ex, string addtlInfo = null) mainLog?.WriteException(ex, addtlInfo); } - public static void LogPacket(string packetLine) + public static void Packet_WriteLine(string text) { - // Create log file if it hasn't been made yet - CreatePacketLog(); + // Don't create log file if it hasn't been made yet + // Packet log should be created manually + packetLog?.WriteLine(text); + } - packetLog?.WriteLine(packetLine); + public static void Packet_Write(string text) + { + // Don't create log file if it hasn't been made yet + // Packet log should be created manually + packetLog?.Write(text); } /// diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index f6f6a0b..f573dc0 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -479,10 +479,15 @@ private void OnPacketArrival(object sender, PacketCapture packet) { PacketParser.HandlePcapPacket(packet.Data, ref processedPacketCount); } + catch (ThreadAbortException) + { + // Don't log ThreadAbortExceptions, just return + return; + } catch (Exception ex) { Console.WriteLine($"Error while handling packet: {ex.GetFirstLine()}"); - Logging.LogException(ex); + Logging.Main_WriteException(ex, "Context: Unhandled error during packet handling"); // Stop capture uiDispatcher.Invoke(StopCapture); @@ -495,11 +500,7 @@ private void OnPacketArrival(object sender, PacketCapture packet) RawCapture raw = packet.GetPacket(); string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; Console.WriteLine(packetLogString); - - if (packetDebugLog) - { - Logging.LogPacket(packetLogString); - } + Logging.Packet_WriteLine(packetLogString); } // Status reporting (slow) @@ -813,10 +814,10 @@ public static void OnUnhandledException(object sender, UnhandledExceptionEventAr if (Logging.MainLogExists) { // Log exception - Logging.LogLine("-------------------"); - Logging.LogLine("UNHANDLED EXCEPTION"); - Logging.LogLine("-------------------"); - Logging.LogException(unhandledException); + Logging.Main_WriteLine("-------------------"); + Logging.Main_WriteLine("UNHANDLED EXCEPTION"); + Logging.Main_WriteLine("-------------------"); + Logging.Main_WriteException(unhandledException); // Complete the message buffer message.AppendLine("A log of the error has been created, do you want to open it?"); From 937248b154dfe536a0716e5a49b9f8ff8d10dda4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 10 May 2022 14:56:20 -0600 Subject: [PATCH 031/437] Add intermediary packet parsing logging --- MainWindow/MainWindow.xaml.cs | 2 + PacketParsing/PacketParser.cs | 5 +++ PacketParsing/ParsingUtils.cs | 9 ++++ PacketParsing/VigemMapper.cs | 71 ++++++++++++++++++++++--------- PacketParsing/VjoyMapper.cs | 80 ++++++++++++++++++++++++++++------- PacketParsing/XboxDevice.cs | 7 +++ 6 files changed, 140 insertions(+), 34 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index f573dc0..7aa63f6 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -583,6 +583,7 @@ private void startButton_Click(object sender, RoutedEventArgs e) private void packetDebugCheckBox_Checked(object sender, RoutedEventArgs e) { packetDebug = true; + PacketParser.PacketDebug = true; packetLogCheckBox.IsEnabled = true; packetDebugLog = packetLogCheckBox.IsChecked.GetValueOrDefault(); @@ -599,6 +600,7 @@ private void packetDebugCheckBox_Checked(object sender, RoutedEventArgs e) private void packetDebugCheckBox_Unchecked(object sender, RoutedEventArgs e) { packetDebug = false; + PacketParser.PacketDebug = false; packetLogCheckBox.IsEnabled = false; packetDebugLog = false; diff --git a/PacketParsing/PacketParser.cs b/PacketParsing/PacketParser.cs index 090ff81..ed4205f 100644 --- a/PacketParsing/PacketParser.cs +++ b/PacketParsing/PacketParser.cs @@ -30,6 +30,11 @@ public static class PacketParser /// public static ParsingMode ParseMode { get; set; } = (ParsingMode)0; + /// + /// Gets or sets the current parsing mode. + /// + public static bool PacketDebug { get; set; } = false; + /// /// Whether or not new devices can be added. /// diff --git a/PacketParsing/ParsingUtils.cs b/PacketParsing/ParsingUtils.cs index 5d097ff..9a83576 100644 --- a/PacketParsing/ParsingUtils.cs +++ b/PacketParsing/ParsingUtils.cs @@ -78,5 +78,14 @@ public static short GetInt16BE(this ReadOnlySpan span, int index) { return (short)(span[index] << 8 | span[index + 1]); } + + /// + /// Converts a ReadOnlySpan to a string. + /// + public static string ToHexString(this ReadOnlySpan span) + { + string hexString = ParsingHelpers.ByteArrayToHexString(span.ToArray()); + return hexString; + } } } diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 993a3b5..37ad2c3 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -99,6 +99,13 @@ public void ParseInput(ReadOnlySpan data, byte length) // Send data device.SubmitReport(); + + if (PacketParser.PacketDebug) + { + string debugData = $", Input: {data.ToHexString()}"; + Console.WriteLine(debugData); + Logging.Packet_WriteLine(debugData); + } } /// @@ -106,6 +113,13 @@ public void ParseInput(ReadOnlySpan data, byte length) /// private void ParseCoreButtons(ushort buttons) { + if (PacketParser.PacketDebug) + { + string debugData = $"Buttons: {buttons.ToString("X4")}"; + Console.Write(debugData); + Logging.Packet_Write(debugData); + } + // Menu device.SetButtonState(Xbox360Button.Start, (buttons | GamepadButton.Menu) != 0); // Options @@ -171,12 +185,24 @@ private void ParseGuitar(ReadOnlySpan data) device.SetButtonState(Xbox360Button.X, (frets | GuitarFret.Blue) != 0); device.SetButtonState(Xbox360Button.LeftShoulder, (frets | GuitarFret.Orange) != 0); + // Axes + short whammy = data[GuitarOffset.WhammyBar].ScaleToInt16(); + short tilt = data[GuitarOffset.Tilt].ScaleToInt16(); + byte pickup = data[GuitarOffset.PickupSwitch]; + // Whammy - device.SetAxisValue(Xbox360Axis.RightThumbX, data[GuitarOffset.WhammyBar].ScaleToInt16()); + device.SetAxisValue(Xbox360Axis.RightThumbX, whammy); // Tilt - device.SetAxisValue(Xbox360Axis.RightThumbY, data[GuitarOffset.Tilt].ScaleToInt16()); + device.SetAxisValue(Xbox360Axis.RightThumbY, tilt); // Pickup Switch - device.SetSliderValue(Xbox360Slider.LeftTrigger, data[GuitarOffset.PickupSwitch]); + device.SetSliderValue(Xbox360Slider.LeftTrigger, pickup); + + if (PacketParser.PacketDebug) + { + string debugData = $", Frets: {frets.ToString("X2")}, Whammy: {whammy.ToString("X4")}, Tilt: {tilt.ToString("X4")}, Pickup Switch: {pickup.ToString("X2")}"; + Console.WriteLine(debugData); + Logging.Packet_WriteLine(debugData); + } } /// @@ -211,22 +237,15 @@ private void ParseDrums(ReadOnlySpan data) (yellowCym | blueCym | greenCym) != 0); // Velocities - // device.SetAxisValue( - // Xbox360Axis.LeftThumbX, - // ByteToVelocity(redPad) - // ); - // device.SetAxisValue( - // Xbox360Axis.LeftThumbY, - // ByteToVelocityNegative((byte)(yellowPad | yellowCym)) - // ); - // device.SetAxisValue( - // Xbox360Axis.RightThumbX, - // ByteToVelocity((byte)(bluePad | blueCym)) - // ); - // device.SetAxisValue( - // Xbox360Axis.RightThumbY, - // ByteToVelocityNegative((byte)(greenPad | greenCym)) - // ); + // short redVel = ByteToVelocity(redPad); + // short yellowVel = ByteToVelocityNegative((byte)(yellowPad | yellowCym)); + // short blueVel = ByteToVelocity((byte)(bluePad | blueCym)); + // short greenVel = ByteToVelocityNegative((byte)(greenPad | greenCym)); + + // device.SetAxisValue(Xbox360Axis.LeftThumbX, redVel); + // device.SetAxisValue(Xbox360Axis.LeftThumbY, yellowVel); + // device.SetAxisValue(Xbox360Axis.RightThumbX, blueVel); + // device.SetAxisValue(Xbox360Axis.RightThumbY, greenVel); /// /// Scales a byte to a drums velocity value. @@ -255,6 +274,13 @@ private void ParseDrums(ReadOnlySpan data) // ((~value.ScaleToUInt16()) >> 1) | 0x8000 // ); // } + + if (PacketParser.PacketDebug) + { + string debugData = $", Pads: Red: {redPad.ToString("X2")}, Yellow: {yellowPad.ToString("X2")}, Blue: {bluePad.ToString("X2")}, Green: {greenPad.ToString("X2")},\nCymbals: Yellow: {yellowCym.ToString("X2")}, Blue: {blueCym.ToString("X2")}, Green: {greenCym.ToString("X2")}"; // ,\nVelocities: Red: {redVel.ToString("X4)}, Yellow: {yellowVel.ToString("X4)}, Blue: {blueVel.ToString("X4)}, Green: {greenVel.ToString("X4)} + Console.WriteLine(debugData); + Logging.Packet_WriteLine(debugData); + } } /// @@ -270,6 +296,13 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length) device.SetButtonState(Xbox360Button.Guide, data[KeycodeOffset.PressedState] != 0); device.SubmitReport(); } + + if (PacketParser.PacketDebug) + { + string debugData = $", Virtual key: {data.ToHexString()}"; + Console.WriteLine(debugData); + Logging.Packet_WriteLine(debugData); + } } /// diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index 41e39f9..c556b71 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -58,6 +58,13 @@ public void ParseInput(ReadOnlySpan data, byte length) // Send data VjoyStatic.Client.UpdateVJD(deviceId, ref state); + + if (PacketParser.PacketDebug) + { + string debugData = $", Input: {data.ToHexString()}"; + Console.WriteLine(debugData); + Logging.Packet_WriteLine(debugData); + } } /// @@ -65,6 +72,13 @@ public void ParseInput(ReadOnlySpan data, byte length) /// private void ParseCoreButtons(ushort buttons) { + if (PacketParser.PacketDebug) + { + string debugData = $"Buttons: {buttons.ToString("X4")}"; + Console.Write(debugData); + Logging.Packet_Write(debugData); + } + // Menu if ((buttons | GamepadButton.Menu) != 0) { @@ -155,24 +169,38 @@ private void ParseGuitar(ReadOnlySpan data) ParseCoreButtons(data.GetUInt16BE(GuitarOffset.Buttons)); // Frets - // The fret data aligns with how we want it to be set in the vJoy device, so it can be mapped directly - state.Buttons |= data[GuitarOffset.UpperFrets]; + byte frets = data[GuitarOffset.UpperFrets]; // Lower frets are mapped on top of the upper frets to allow both sets to be used in-game - state.Buttons |= data[GuitarOffset.LowerFrets]; + frets |= data[GuitarOffset.LowerFrets]; + + // The fret data aligns with how we want it to be set in the vJoy device, so it can be mapped directly + state.Buttons |= frets; + + // Axes + int whammy = data[GuitarOffset.WhammyBar].ScaleToInt32(); + int tilt = data[GuitarOffset.Tilt].ScaleToInt32(); + int pickup = data[GuitarOffset.PickupSwitch].ScaleToInt32(); // Whammy // Value ranges from 0 (not pressed) to 255 (fully pressed) - state.AxisY = data[GuitarOffset.WhammyBar].ScaleToInt32(); + state.AxisY = whammy; // Tilt // Value ranges from 0 to 255 // It seems to have a threshold of around 0x70 though, // after a certain point values will get floored to 0 - state.AxisZ = data[GuitarOffset.Tilt].ScaleToInt32(); + state.AxisZ = tilt; // Pickup switch // Reported values are 0x00, 0x10, 0x20, 0x30, and 0x40 (ranges from 0 to 64) - state.AxisX = data[GuitarOffset.PickupSwitch].ScaleToInt32(); + state.AxisX = pickup; + + if (PacketParser.PacketDebug) + { + string debugData = $", Frets: {frets.ToString("X2")}, Whammy: {whammy.ToString("X8")}, Tilt: {tilt.ToString("X8")}, Pickup Switch: {pickup.ToString("X8")}"; + Console.WriteLine(debugData); + Logging.Packet_WriteLine(debugData); + } } /// @@ -183,47 +211,55 @@ private void ParseDrums(ReadOnlySpan data) // Buttons ParseCoreButtons(data.GetUInt16BE(DrumOffset.Buttons)); - // Pads + // Pads and cymbals + byte redPad = (byte)(data[DrumOffset.PadVels] >> 4); + byte yellowPad = (byte)(data[DrumOffset.PadVels] | DrumPadVel.Yellow); + byte bluePad = (byte)(data[DrumOffset.PadVels + 1] >> 4); + byte greenPad = (byte)(data[DrumOffset.PadVels + 1] | DrumPadVel.Green); + + byte yellowCym = (byte)(data[DrumOffset.CymbalVels] >> 4); + byte blueCym = (byte)(data[DrumOffset.CymbalVels] | DrumPadVel.Blue); + byte greenCym = (byte)(data[DrumOffset.CymbalVels + 1] >> 4); + // Red pad - if ((data[DrumOffset.PadVels] | DrumPadVel.Red) != 0) + if (redPad != 0) { state.Buttons |= VjoyStatic.Button.One; } // Yellow pad - if ((data[DrumOffset.PadVels] | DrumPadVel.Yellow) != 0) + if (yellowPad != 0) { state.Buttons |= VjoyStatic.Button.Two; } // Blue pad - if ((data[DrumOffset.PadVels] | DrumPadVel.Blue) != 0) + if (bluePad != 0) { state.Buttons |= VjoyStatic.Button.Three; } // Green pad - if ((data[DrumOffset.PadVels] | DrumPadVel.Green) != 0) + if (greenPad != 0) { state.Buttons |= VjoyStatic.Button.Four; } - // Cymbals // Yellow cymbal - if ((data[DrumOffset.CymbalVels] | DrumCymVel.Yellow) != 0) + if (yellowCym != 0) { state.Buttons |= VjoyStatic.Button.Six; } // Blue cymbal - if ((data[DrumOffset.CymbalVels] | DrumCymVel.Blue) != 0) + if (blueCym != 0) { state.Buttons |= VjoyStatic.Button.Seven; } // Green cymbal - if ((data[DrumOffset.CymbalVels] | DrumCymVel.Green) != 0) + if (greenCym != 0) { state.Buttons |= VjoyStatic.Button.Eight; } @@ -241,6 +277,13 @@ private void ParseDrums(ReadOnlySpan data) { state.Buttons |= VjoyStatic.Button.Nine; } + + if (PacketParser.PacketDebug) + { + string debugData = $", Pads: Red: {redPad.ToString("X2")}, Yellow: {yellowPad.ToString("X2")}, Blue: {bluePad.ToString("X2")}, Green: {greenPad.ToString("X2")}; Cymbals: Yellow: {yellowCym.ToString("X2")}, Blue: {blueCym.ToString("X2")}, Green: {greenCym.ToString("X2")}"; + Console.WriteLine(debugData); + Logging.Packet_WriteLine(debugData); + } } /// @@ -257,6 +300,13 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length) state.Buttons |= (data[KeycodeOffset.PressedState] != 0) ? VjoyStatic.Button.Fourteen : 0; VjoyStatic.Client.UpdateVJD(deviceId, ref state); } + + if (PacketParser.PacketDebug) + { + string debugData = $", Virtual key: {data.ToHexString()}"; + Console.WriteLine(debugData); + Logging.Packet_WriteLine(debugData); + } } /// diff --git a/PacketParsing/XboxDevice.cs b/PacketParsing/XboxDevice.cs index 6bd6df2..0836043 100644 --- a/PacketParsing/XboxDevice.cs +++ b/PacketParsing/XboxDevice.cs @@ -45,6 +45,13 @@ public XboxDevice(ParsingMode parseMode) /// public void ParseCommand(ReadOnlySpan commandData) { + if (PacketParser.PacketDebug) + { + string debugData = $"Command: {commandData.ToHexString()}"; + Console.Write(debugData); + Logging.Packet_Write(debugData); + } + switch (commandData[CommandOffset.CommandId]) { case CommandId.Input: From ce17871f2639c54710a1bc8eb90cee44fed16eca Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 10 May 2022 14:57:47 -0600 Subject: [PATCH 032/437] Remove commented-out vJoy safety net in PacketParser The issue it was made for should no longer happen --- PacketParsing/PacketParser.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/PacketParsing/PacketParser.cs b/PacketParsing/PacketParser.cs index ed4205f..01a55a2 100644 --- a/PacketParsing/PacketParser.cs +++ b/PacketParsing/PacketParser.cs @@ -112,13 +112,6 @@ public static void Close() device.Close(); } - // Just in case... - // At least in debug builds, stopping capture can take a while and cause the devices to not get disconnected - // if (VjoyStatic.Client.vJoyEnabled()) - // { - // VjoyStatic.FreeAllDevices(); - // } - // Clear IDs list pcapIds.Clear(); From ed4b5a5a46bb396b1a87201570881e0f9b0266a5 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 10 May 2022 15:25:03 -0600 Subject: [PATCH 033/437] Fix mis-ordered logging --- MainWindow/MainWindow.xaml.cs | 18 +++++++++--------- PacketParsing/VigemMapper.cs | 14 +++++++------- PacketParsing/VjoyMapper.cs | 14 +++++++------- PacketParsing/XboxDevice.cs | 6 ++++++ 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index 7aa63f6..5f69620 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -475,6 +475,15 @@ private void StopCapture() /// The received packet private void OnPacketArrival(object sender, PacketCapture packet) { + // Debugging (if enabled) + if (packetDebug) + { + RawCapture raw = packet.GetPacket(); + string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; + Console.WriteLine(packetLogString); + Logging.Packet_WriteLine(packetLogString); + } + try { PacketParser.HandlePcapPacket(packet.Data, ref processedPacketCount); @@ -494,15 +503,6 @@ private void OnPacketArrival(object sender, PacketCapture packet) return; } - // Debugging (if enabled) - if (packetDebug) - { - RawCapture raw = packet.GetPacket(); - string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; - Console.WriteLine(packetLogString); - Logging.Packet_WriteLine(packetLogString); - } - // Status reporting (slow) if ((processedPacketCount < 10) || ((processedPacketCount < 100) && (processedPacketCount % 10 == 0)) || diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 37ad2c3..6f17f1a 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -72,6 +72,13 @@ public void ParseInput(ReadOnlySpan data, byte length) return; } + if (PacketParser.PacketDebug) + { + string debugData = $", Input: {data.ToHexString()}"; + Console.WriteLine(debugData); + Logging.Packet_WriteLine(debugData); + } + // Reset report device.ResetReport(); @@ -99,13 +106,6 @@ public void ParseInput(ReadOnlySpan data, byte length) // Send data device.SubmitReport(); - - if (PacketParser.PacketDebug) - { - string debugData = $", Input: {data.ToHexString()}"; - Console.WriteLine(debugData); - Logging.Packet_WriteLine(debugData); - } } /// diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index c556b71..e70c3e3 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -31,6 +31,13 @@ public VjoyMapper() /// public void ParseInput(ReadOnlySpan data, byte length) { + if (PacketParser.PacketDebug) + { + string debugData = $", Input: {data.ToHexString()}"; + Console.WriteLine(debugData); + Logging.Packet_WriteLine(debugData); + } + // Reset report state.ResetState(); @@ -58,13 +65,6 @@ public void ParseInput(ReadOnlySpan data, byte length) // Send data VjoyStatic.Client.UpdateVJD(deviceId, ref state); - - if (PacketParser.PacketDebug) - { - string debugData = $", Input: {data.ToHexString()}"; - Console.WriteLine(debugData); - Logging.Packet_WriteLine(debugData); - } } /// diff --git a/PacketParsing/XboxDevice.cs b/PacketParsing/XboxDevice.cs index 0836043..5c4a534 100644 --- a/PacketParsing/XboxDevice.cs +++ b/PacketParsing/XboxDevice.cs @@ -66,6 +66,12 @@ public void ParseCommand(ReadOnlySpan commandData) default: // Don't do anything with unrecognized command IDs + if (PacketParser.PacketDebug) + { + // Finish off debug line; normally this is done in the input parsing + Console.Write("\n"); + Logging.Packet_Write("\n"); + } break; } } From 0c881d41b65656aecb701cbe5aea33b4e62d1a16 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 10 May 2022 21:07:30 -0600 Subject: [PATCH 034/437] This took *way* too long to realize lol --- PacketParsing/VigemMapper.cs | 44 ++++++++++++++++++------------------ PacketParsing/VjoyMapper.cs | 26 ++++++++++----------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 6f17f1a..a4daac2 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -121,15 +121,15 @@ private void ParseCoreButtons(ushort buttons) } // Menu - device.SetButtonState(Xbox360Button.Start, (buttons | GamepadButton.Menu) != 0); + device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); // Options - device.SetButtonState(Xbox360Button.Back, (buttons | GamepadButton.Options) != 0); + device.SetButtonState(Xbox360Button.Back, (buttons & GamepadButton.Options) != 0); // Dpad - device.SetButtonState(Xbox360Button.Up, (buttons | GamepadButton.DpadUp) != 0); - device.SetButtonState(Xbox360Button.Down, (buttons | GamepadButton.DpadDown) != 0); - device.SetButtonState(Xbox360Button.Left, (buttons | GamepadButton.DpadLeft) != 0); - device.SetButtonState(Xbox360Button.Right, (buttons | GamepadButton.DpadRight) != 0); + device.SetButtonState(Xbox360Button.Up, (buttons & GamepadButton.DpadUp) != 0); + device.SetButtonState(Xbox360Button.Down, (buttons & GamepadButton.DpadDown) != 0); + device.SetButtonState(Xbox360Button.Left, (buttons & GamepadButton.DpadLeft) != 0); + device.SetButtonState(Xbox360Button.Right, (buttons & GamepadButton.DpadRight) != 0); // Other buttons are not mapped here since they may have specific uses } @@ -145,15 +145,15 @@ private void ParseGamepad(ReadOnlySpan data) ushort buttons = data.GetUInt16BE(GamepadOffset.Buttons); ParseCoreButtons(buttons); - device.SetButtonState(Xbox360Button.A, (buttons | GamepadButton.A) != 0); - device.SetButtonState(Xbox360Button.B, (buttons | GamepadButton.B) != 0); - device.SetButtonState(Xbox360Button.X, (buttons | GamepadButton.X) != 0); - device.SetButtonState(Xbox360Button.Y, (buttons | GamepadButton.Y) != 0); + device.SetButtonState(Xbox360Button.A, (buttons & GamepadButton.A) != 0); + device.SetButtonState(Xbox360Button.B, (buttons & GamepadButton.B) != 0); + device.SetButtonState(Xbox360Button.X, (buttons & GamepadButton.X) != 0); + device.SetButtonState(Xbox360Button.Y, (buttons & GamepadButton.Y) != 0); - device.SetButtonState(Xbox360Button.LeftShoulder, (buttons | GamepadButton.LeftBumper) != 0); - device.SetButtonState(Xbox360Button.RightShoulder, (buttons | GamepadButton.RightBumper) != 0); - device.SetButtonState(Xbox360Button.LeftThumb, (buttons | GamepadButton.LeftStickPress) != 0); - device.SetButtonState(Xbox360Button.RightThumb, (buttons | GamepadButton.RightStickPress) != 0); + device.SetButtonState(Xbox360Button.LeftShoulder, (buttons & GamepadButton.LeftBumper) != 0); + device.SetButtonState(Xbox360Button.RightShoulder, (buttons & GamepadButton.RightBumper) != 0); + device.SetButtonState(Xbox360Button.LeftThumb, (buttons & GamepadButton.LeftStickPress) != 0); + device.SetButtonState(Xbox360Button.RightThumb, (buttons & GamepadButton.RightStickPress) != 0); // Sticks device.SetAxisValue(Xbox360Axis.LeftThumbX, data.GetInt16LE(GamepadOffset.LeftStickX)); @@ -179,11 +179,11 @@ private void ParseGuitar(ReadOnlySpan data) byte frets = data[GuitarOffset.UpperFrets]; frets |= data[GuitarOffset.LowerFrets]; - device.SetButtonState(Xbox360Button.A, (frets | GuitarFret.Green) != 0); - device.SetButtonState(Xbox360Button.B, (frets | GuitarFret.Red) != 0); - device.SetButtonState(Xbox360Button.Y, (frets | GuitarFret.Yellow) != 0); - device.SetButtonState(Xbox360Button.X, (frets | GuitarFret.Blue) != 0); - device.SetButtonState(Xbox360Button.LeftShoulder, (frets | GuitarFret.Orange) != 0); + device.SetButtonState(Xbox360Button.A, (frets & GuitarFret.Green) != 0); + device.SetButtonState(Xbox360Button.B, (frets & GuitarFret.Red) != 0); + device.SetButtonState(Xbox360Button.Y, (frets & GuitarFret.Yellow) != 0); + device.SetButtonState(Xbox360Button.X, (frets & GuitarFret.Blue) != 0); + device.SetButtonState(Xbox360Button.LeftShoulder, (frets & GuitarFret.Orange) != 0); // Axes short whammy = data[GuitarOffset.WhammyBar].ScaleToInt16(); @@ -215,12 +215,12 @@ private void ParseDrums(ReadOnlySpan data) // Pads and cymbals byte redPad = (byte)(data[DrumOffset.PadVels] >> 4); - byte yellowPad = (byte)(data[DrumOffset.PadVels] | DrumPadVel.Yellow); + byte yellowPad = (byte)(data[DrumOffset.PadVels] & DrumPadVel.Yellow); byte bluePad = (byte)(data[DrumOffset.PadVels + 1] >> 4); - byte greenPad = (byte)(data[DrumOffset.PadVels + 1] | DrumPadVel.Green); + byte greenPad = (byte)(data[DrumOffset.PadVels + 1] & DrumPadVel.Green); byte yellowCym = (byte)(data[DrumOffset.CymbalVels] >> 4); - byte blueCym = (byte)(data[DrumOffset.CymbalVels] | DrumPadVel.Blue); + byte blueCym = (byte)(data[DrumOffset.CymbalVels] & DrumPadVel.Blue); byte greenCym = (byte)(data[DrumOffset.CymbalVels + 1] >> 4); // Color flags diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index e70c3e3..bf50f46 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -80,25 +80,25 @@ private void ParseCoreButtons(ushort buttons) } // Menu - if ((buttons | GamepadButton.Menu) != 0) + if ((buttons & GamepadButton.Menu) != 0) { state.Buttons |= VjoyStatic.Button.Fifteen; } // Options - if ((buttons | GamepadButton.Options) != 0) + if ((buttons & GamepadButton.Options) != 0) { state.Buttons |= VjoyStatic.Button.Sixteen; } // D-pad to POV - if ((buttons | GamepadButton.DpadUp) != 0) + if ((buttons & GamepadButton.DpadUp) != 0) { - if ((buttons | GamepadButton.DpadLeft) != 0) + if ((buttons & GamepadButton.DpadLeft) != 0) { state.bHats = VjoyStatic.PoV.UpLeft; } - else if ((buttons | GamepadButton.DpadRight) != 0) + else if ((buttons & GamepadButton.DpadRight) != 0) { state.bHats = VjoyStatic.PoV.UpRight; } @@ -107,13 +107,13 @@ private void ParseCoreButtons(ushort buttons) state.bHats = VjoyStatic.PoV.Up; } } - else if ((buttons | GamepadButton.DpadDown) != 0) + else if ((buttons & GamepadButton.DpadDown) != 0) { - if ((buttons | GamepadButton.DpadLeft) != 0) + if ((buttons & GamepadButton.DpadLeft) != 0) { state.bHats = VjoyStatic.PoV.DownLeft; } - else if ((buttons | GamepadButton.DpadRight) != 0) + else if ((buttons & GamepadButton.DpadRight) != 0) { state.bHats = VjoyStatic.PoV.DownRight; } @@ -124,11 +124,11 @@ private void ParseCoreButtons(ushort buttons) } else { - if ((buttons | GamepadButton.DpadLeft) != 0) + if ((buttons & GamepadButton.DpadLeft) != 0) { state.bHats = VjoyStatic.PoV.Left; } - else if ((buttons | GamepadButton.DpadRight) != 0) + else if ((buttons & GamepadButton.DpadRight) != 0) { state.bHats = VjoyStatic.PoV.Right; } @@ -213,12 +213,12 @@ private void ParseDrums(ReadOnlySpan data) // Pads and cymbals byte redPad = (byte)(data[DrumOffset.PadVels] >> 4); - byte yellowPad = (byte)(data[DrumOffset.PadVels] | DrumPadVel.Yellow); + byte yellowPad = (byte)(data[DrumOffset.PadVels] & DrumPadVel.Yellow); byte bluePad = (byte)(data[DrumOffset.PadVels + 1] >> 4); - byte greenPad = (byte)(data[DrumOffset.PadVels + 1] | DrumPadVel.Green); + byte greenPad = (byte)(data[DrumOffset.PadVels + 1] & DrumPadVel.Green); byte yellowCym = (byte)(data[DrumOffset.CymbalVels] >> 4); - byte blueCym = (byte)(data[DrumOffset.CymbalVels] | DrumPadVel.Blue); + byte blueCym = (byte)(data[DrumOffset.CymbalVels] & DrumPadVel.Blue); byte greenCym = (byte)(data[DrumOffset.CymbalVels + 1] >> 4); // Red pad From a53eabf34a7113fc52ac38d98ccc6abd34459921 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 10 May 2022 21:21:17 -0600 Subject: [PATCH 035/437] Revert intermediary logging commits The logging doesn't serve a purpose anymore --- MainWindow/MainWindow.xaml.cs | 20 ++++----- PacketParsing/PacketParser.cs | 5 --- PacketParsing/ParsingUtils.cs | 9 ---- PacketParsing/VigemMapper.cs | 71 +++++++++---------------------- PacketParsing/VjoyMapper.cs | 80 +++++++---------------------------- PacketParsing/XboxDevice.cs | 13 ------ 6 files changed, 43 insertions(+), 155 deletions(-) diff --git a/MainWindow/MainWindow.xaml.cs b/MainWindow/MainWindow.xaml.cs index 5f69620..f573dc0 100644 --- a/MainWindow/MainWindow.xaml.cs +++ b/MainWindow/MainWindow.xaml.cs @@ -475,15 +475,6 @@ private void StopCapture() /// The received packet private void OnPacketArrival(object sender, PacketCapture packet) { - // Debugging (if enabled) - if (packetDebug) - { - RawCapture raw = packet.GetPacket(); - string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; - Console.WriteLine(packetLogString); - Logging.Packet_WriteLine(packetLogString); - } - try { PacketParser.HandlePcapPacket(packet.Data, ref processedPacketCount); @@ -503,6 +494,15 @@ private void OnPacketArrival(object sender, PacketCapture packet) return; } + // Debugging (if enabled) + if (packetDebug) + { + RawCapture raw = packet.GetPacket(); + string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; + Console.WriteLine(packetLogString); + Logging.Packet_WriteLine(packetLogString); + } + // Status reporting (slow) if ((processedPacketCount < 10) || ((processedPacketCount < 100) && (processedPacketCount % 10 == 0)) || @@ -583,7 +583,6 @@ private void startButton_Click(object sender, RoutedEventArgs e) private void packetDebugCheckBox_Checked(object sender, RoutedEventArgs e) { packetDebug = true; - PacketParser.PacketDebug = true; packetLogCheckBox.IsEnabled = true; packetDebugLog = packetLogCheckBox.IsChecked.GetValueOrDefault(); @@ -600,7 +599,6 @@ private void packetDebugCheckBox_Checked(object sender, RoutedEventArgs e) private void packetDebugCheckBox_Unchecked(object sender, RoutedEventArgs e) { packetDebug = false; - PacketParser.PacketDebug = false; packetLogCheckBox.IsEnabled = false; packetDebugLog = false; diff --git a/PacketParsing/PacketParser.cs b/PacketParsing/PacketParser.cs index 01a55a2..18b9c79 100644 --- a/PacketParsing/PacketParser.cs +++ b/PacketParsing/PacketParser.cs @@ -30,11 +30,6 @@ public static class PacketParser /// public static ParsingMode ParseMode { get; set; } = (ParsingMode)0; - /// - /// Gets or sets the current parsing mode. - /// - public static bool PacketDebug { get; set; } = false; - /// /// Whether or not new devices can be added. /// diff --git a/PacketParsing/ParsingUtils.cs b/PacketParsing/ParsingUtils.cs index 9a83576..5d097ff 100644 --- a/PacketParsing/ParsingUtils.cs +++ b/PacketParsing/ParsingUtils.cs @@ -78,14 +78,5 @@ public static short GetInt16BE(this ReadOnlySpan span, int index) { return (short)(span[index] << 8 | span[index + 1]); } - - /// - /// Converts a ReadOnlySpan to a string. - /// - public static string ToHexString(this ReadOnlySpan span) - { - string hexString = ParsingHelpers.ByteArrayToHexString(span.ToArray()); - return hexString; - } } } diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index a4daac2..28d2fa6 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -72,13 +72,6 @@ public void ParseInput(ReadOnlySpan data, byte length) return; } - if (PacketParser.PacketDebug) - { - string debugData = $", Input: {data.ToHexString()}"; - Console.WriteLine(debugData); - Logging.Packet_WriteLine(debugData); - } - // Reset report device.ResetReport(); @@ -113,13 +106,6 @@ public void ParseInput(ReadOnlySpan data, byte length) /// private void ParseCoreButtons(ushort buttons) { - if (PacketParser.PacketDebug) - { - string debugData = $"Buttons: {buttons.ToString("X4")}"; - Console.Write(debugData); - Logging.Packet_Write(debugData); - } - // Menu device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); // Options @@ -185,24 +171,12 @@ private void ParseGuitar(ReadOnlySpan data) device.SetButtonState(Xbox360Button.X, (frets & GuitarFret.Blue) != 0); device.SetButtonState(Xbox360Button.LeftShoulder, (frets & GuitarFret.Orange) != 0); - // Axes - short whammy = data[GuitarOffset.WhammyBar].ScaleToInt16(); - short tilt = data[GuitarOffset.Tilt].ScaleToInt16(); - byte pickup = data[GuitarOffset.PickupSwitch]; - // Whammy - device.SetAxisValue(Xbox360Axis.RightThumbX, whammy); + device.SetAxisValue(Xbox360Axis.RightThumbX, data[GuitarOffset.WhammyBar].ScaleToInt16()); // Tilt - device.SetAxisValue(Xbox360Axis.RightThumbY, tilt); + device.SetAxisValue(Xbox360Axis.RightThumbY, data[GuitarOffset.Tilt].ScaleToInt16()); // Pickup Switch - device.SetSliderValue(Xbox360Slider.LeftTrigger, pickup); - - if (PacketParser.PacketDebug) - { - string debugData = $", Frets: {frets.ToString("X2")}, Whammy: {whammy.ToString("X4")}, Tilt: {tilt.ToString("X4")}, Pickup Switch: {pickup.ToString("X2")}"; - Console.WriteLine(debugData); - Logging.Packet_WriteLine(debugData); - } + device.SetSliderValue(Xbox360Slider.LeftTrigger, data[GuitarOffset.PickupSwitch]); } /// @@ -237,15 +211,22 @@ private void ParseDrums(ReadOnlySpan data) (yellowCym | blueCym | greenCym) != 0); // Velocities - // short redVel = ByteToVelocity(redPad); - // short yellowVel = ByteToVelocityNegative((byte)(yellowPad | yellowCym)); - // short blueVel = ByteToVelocity((byte)(bluePad | blueCym)); - // short greenVel = ByteToVelocityNegative((byte)(greenPad | greenCym)); - - // device.SetAxisValue(Xbox360Axis.LeftThumbX, redVel); - // device.SetAxisValue(Xbox360Axis.LeftThumbY, yellowVel); - // device.SetAxisValue(Xbox360Axis.RightThumbX, blueVel); - // device.SetAxisValue(Xbox360Axis.RightThumbY, greenVel); + // device.SetAxisValue( + // Xbox360Axis.LeftThumbX, + // ByteToVelocity(redPad) + // ); + // device.SetAxisValue( + // Xbox360Axis.LeftThumbY, + // ByteToVelocityNegative((byte)(yellowPad | yellowCym)) + // ); + // device.SetAxisValue( + // Xbox360Axis.RightThumbX, + // ByteToVelocity((byte)(bluePad | blueCym)) + // ); + // device.SetAxisValue( + // Xbox360Axis.RightThumbY, + // ByteToVelocityNegative((byte)(greenPad | greenCym)) + // ); /// /// Scales a byte to a drums velocity value. @@ -274,13 +255,6 @@ private void ParseDrums(ReadOnlySpan data) // ((~value.ScaleToUInt16()) >> 1) | 0x8000 // ); // } - - if (PacketParser.PacketDebug) - { - string debugData = $", Pads: Red: {redPad.ToString("X2")}, Yellow: {yellowPad.ToString("X2")}, Blue: {bluePad.ToString("X2")}, Green: {greenPad.ToString("X2")},\nCymbals: Yellow: {yellowCym.ToString("X2")}, Blue: {blueCym.ToString("X2")}, Green: {greenCym.ToString("X2")}"; // ,\nVelocities: Red: {redVel.ToString("X4)}, Yellow: {yellowVel.ToString("X4)}, Blue: {blueVel.ToString("X4)}, Green: {greenVel.ToString("X4)} - Console.WriteLine(debugData); - Logging.Packet_WriteLine(debugData); - } } /// @@ -296,13 +270,6 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length) device.SetButtonState(Xbox360Button.Guide, data[KeycodeOffset.PressedState] != 0); device.SubmitReport(); } - - if (PacketParser.PacketDebug) - { - string debugData = $", Virtual key: {data.ToHexString()}"; - Console.WriteLine(debugData); - Logging.Packet_WriteLine(debugData); - } } /// diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index bf50f46..68de2ed 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -31,13 +31,6 @@ public VjoyMapper() /// public void ParseInput(ReadOnlySpan data, byte length) { - if (PacketParser.PacketDebug) - { - string debugData = $", Input: {data.ToHexString()}"; - Console.WriteLine(debugData); - Logging.Packet_WriteLine(debugData); - } - // Reset report state.ResetState(); @@ -72,13 +65,6 @@ public void ParseInput(ReadOnlySpan data, byte length) /// private void ParseCoreButtons(ushort buttons) { - if (PacketParser.PacketDebug) - { - string debugData = $"Buttons: {buttons.ToString("X4")}"; - Console.Write(debugData); - Logging.Packet_Write(debugData); - } - // Menu if ((buttons & GamepadButton.Menu) != 0) { @@ -169,38 +155,24 @@ private void ParseGuitar(ReadOnlySpan data) ParseCoreButtons(data.GetUInt16BE(GuitarOffset.Buttons)); // Frets - byte frets = data[GuitarOffset.UpperFrets]; - // Lower frets are mapped on top of the upper frets to allow both sets to be used in-game - frets |= data[GuitarOffset.LowerFrets]; - // The fret data aligns with how we want it to be set in the vJoy device, so it can be mapped directly - state.Buttons |= frets; - - // Axes - int whammy = data[GuitarOffset.WhammyBar].ScaleToInt32(); - int tilt = data[GuitarOffset.Tilt].ScaleToInt32(); - int pickup = data[GuitarOffset.PickupSwitch].ScaleToInt32(); + state.Buttons |= data[GuitarOffset.UpperFrets]; + // Lower frets are mapped on top of the upper frets to allow both sets to be used in-game + state.Buttons |= data[GuitarOffset.LowerFrets]; // Whammy // Value ranges from 0 (not pressed) to 255 (fully pressed) - state.AxisY = whammy; + state.AxisY = data[GuitarOffset.WhammyBar].ScaleToInt32(); // Tilt // Value ranges from 0 to 255 // It seems to have a threshold of around 0x70 though, // after a certain point values will get floored to 0 - state.AxisZ = tilt; + state.AxisZ = data[GuitarOffset.Tilt].ScaleToInt32(); // Pickup switch // Reported values are 0x00, 0x10, 0x20, 0x30, and 0x40 (ranges from 0 to 64) - state.AxisX = pickup; - - if (PacketParser.PacketDebug) - { - string debugData = $", Frets: {frets.ToString("X2")}, Whammy: {whammy.ToString("X8")}, Tilt: {tilt.ToString("X8")}, Pickup Switch: {pickup.ToString("X8")}"; - Console.WriteLine(debugData); - Logging.Packet_WriteLine(debugData); - } + state.AxisX = data[GuitarOffset.PickupSwitch].ScaleToInt32(); } /// @@ -211,55 +183,47 @@ private void ParseDrums(ReadOnlySpan data) // Buttons ParseCoreButtons(data.GetUInt16BE(DrumOffset.Buttons)); - // Pads and cymbals - byte redPad = (byte)(data[DrumOffset.PadVels] >> 4); - byte yellowPad = (byte)(data[DrumOffset.PadVels] & DrumPadVel.Yellow); - byte bluePad = (byte)(data[DrumOffset.PadVels + 1] >> 4); - byte greenPad = (byte)(data[DrumOffset.PadVels + 1] & DrumPadVel.Green); - - byte yellowCym = (byte)(data[DrumOffset.CymbalVels] >> 4); - byte blueCym = (byte)(data[DrumOffset.CymbalVels] & DrumPadVel.Blue); - byte greenCym = (byte)(data[DrumOffset.CymbalVels + 1] >> 4); - + // Pads // Red pad - if (redPad != 0) + if ((data[DrumOffset.PadVels] | DrumPadVel.Red) != 0) { state.Buttons |= VjoyStatic.Button.One; } // Yellow pad - if (yellowPad != 0) + if ((data[DrumOffset.PadVels] | DrumPadVel.Yellow) != 0) { state.Buttons |= VjoyStatic.Button.Two; } // Blue pad - if (bluePad != 0) + if ((data[DrumOffset.PadVels] | DrumPadVel.Blue) != 0) { state.Buttons |= VjoyStatic.Button.Three; } // Green pad - if (greenPad != 0) + if ((data[DrumOffset.PadVels] | DrumPadVel.Green) != 0) { state.Buttons |= VjoyStatic.Button.Four; } + // Cymbals // Yellow cymbal - if (yellowCym != 0) + if ((data[DrumOffset.CymbalVels] | DrumCymVel.Yellow) != 0) { state.Buttons |= VjoyStatic.Button.Six; } // Blue cymbal - if (blueCym != 0) + if ((data[DrumOffset.CymbalVels] | DrumCymVel.Blue) != 0) { state.Buttons |= VjoyStatic.Button.Seven; } // Green cymbal - if (greenCym != 0) + if ((data[DrumOffset.CymbalVels] | DrumCymVel.Green) != 0) { state.Buttons |= VjoyStatic.Button.Eight; } @@ -277,13 +241,6 @@ private void ParseDrums(ReadOnlySpan data) { state.Buttons |= VjoyStatic.Button.Nine; } - - if (PacketParser.PacketDebug) - { - string debugData = $", Pads: Red: {redPad.ToString("X2")}, Yellow: {yellowPad.ToString("X2")}, Blue: {bluePad.ToString("X2")}, Green: {greenPad.ToString("X2")}; Cymbals: Yellow: {yellowCym.ToString("X2")}, Blue: {blueCym.ToString("X2")}, Green: {greenCym.ToString("X2")}"; - Console.WriteLine(debugData); - Logging.Packet_WriteLine(debugData); - } } /// @@ -300,13 +257,6 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length) state.Buttons |= (data[KeycodeOffset.PressedState] != 0) ? VjoyStatic.Button.Fourteen : 0; VjoyStatic.Client.UpdateVJD(deviceId, ref state); } - - if (PacketParser.PacketDebug) - { - string debugData = $", Virtual key: {data.ToHexString()}"; - Console.WriteLine(debugData); - Logging.Packet_WriteLine(debugData); - } } /// diff --git a/PacketParsing/XboxDevice.cs b/PacketParsing/XboxDevice.cs index 5c4a534..6bd6df2 100644 --- a/PacketParsing/XboxDevice.cs +++ b/PacketParsing/XboxDevice.cs @@ -45,13 +45,6 @@ public XboxDevice(ParsingMode parseMode) /// public void ParseCommand(ReadOnlySpan commandData) { - if (PacketParser.PacketDebug) - { - string debugData = $"Command: {commandData.ToHexString()}"; - Console.Write(debugData); - Logging.Packet_Write(debugData); - } - switch (commandData[CommandOffset.CommandId]) { case CommandId.Input: @@ -66,12 +59,6 @@ public void ParseCommand(ReadOnlySpan commandData) default: // Don't do anything with unrecognized command IDs - if (PacketParser.PacketDebug) - { - // Finish off debug line; normally this is done in the input parsing - Console.Write("\n"); - Logging.Packet_Write("\n"); - } break; } } From 94d572437f8fa05890788b9b31a91e63ac740d77 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 May 2022 02:53:07 -0600 Subject: [PATCH 036/437] Whoops, missed a few --- PacketParsing/VjoyMapper.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index 68de2ed..d0fea75 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -185,25 +185,25 @@ private void ParseDrums(ReadOnlySpan data) // Pads // Red pad - if ((data[DrumOffset.PadVels] | DrumPadVel.Red) != 0) + if ((data[DrumOffset.PadVels] & DrumPadVel.Red) != 0) { state.Buttons |= VjoyStatic.Button.One; } // Yellow pad - if ((data[DrumOffset.PadVels] | DrumPadVel.Yellow) != 0) + if ((data[DrumOffset.PadVels] & DrumPadVel.Yellow) != 0) { state.Buttons |= VjoyStatic.Button.Two; } // Blue pad - if ((data[DrumOffset.PadVels] | DrumPadVel.Blue) != 0) + if ((data[DrumOffset.PadVels] & DrumPadVel.Blue) != 0) { state.Buttons |= VjoyStatic.Button.Three; } // Green pad - if ((data[DrumOffset.PadVels] | DrumPadVel.Green) != 0) + if ((data[DrumOffset.PadVels] & DrumPadVel.Green) != 0) { state.Buttons |= VjoyStatic.Button.Four; } @@ -211,19 +211,19 @@ private void ParseDrums(ReadOnlySpan data) // Cymbals // Yellow cymbal - if ((data[DrumOffset.CymbalVels] | DrumCymVel.Yellow) != 0) + if ((data[DrumOffset.CymbalVels] & DrumCymVel.Yellow) != 0) { state.Buttons |= VjoyStatic.Button.Six; } // Blue cymbal - if ((data[DrumOffset.CymbalVels] | DrumCymVel.Blue) != 0) + if ((data[DrumOffset.CymbalVels] & DrumCymVel.Blue) != 0) { state.Buttons |= VjoyStatic.Button.Seven; } // Green cymbal - if ((data[DrumOffset.CymbalVels] | DrumCymVel.Green) != 0) + if ((data[DrumOffset.CymbalVels] & DrumCymVel.Green) != 0) { state.Buttons |= VjoyStatic.Button.Eight; } @@ -231,13 +231,13 @@ private void ParseDrums(ReadOnlySpan data) // Kick pedals // Kick 1 - if ((data[DrumOffset.Buttons] | DrumButton.KickOne) != 0) + if ((data[DrumOffset.Buttons] & DrumButton.KickOne) != 0) { state.Buttons |= VjoyStatic.Button.Five; } // Kick 2 - if ((data[DrumOffset.Buttons] | DrumButton.KickTwo) != 0) + if ((data[DrumOffset.Buttons] & DrumButton.KickTwo) != 0) { state.Buttons |= VjoyStatic.Button.Nine; } From 256037edf1c618509dec9b5ec083e0a32993ee22 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 May 2022 13:45:21 -0600 Subject: [PATCH 037/437] Remove debug gamepad report parsing I uh, never actually used it lol --- PacketParsing/VigemMapper.cs | 40 ------------------------------------ PacketParsing/VjoyMapper.cs | 26 ----------------------- 2 files changed, 66 deletions(-) diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 28d2fa6..00c252b 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -77,13 +77,6 @@ public void ParseInput(ReadOnlySpan data, byte length) switch (length) { -#if DEBUG - // Gamepad report parsing for debugging purposes - case Length.Input_Gamepad: - ParseGamepad(data); - break; -#endif - case Length.Input_Guitar: ParseGuitar(data); break; @@ -120,39 +113,6 @@ private void ParseCoreButtons(ushort buttons) // Other buttons are not mapped here since they may have specific uses } -#if DEBUG - // Gamepad report parsing for debugging purposes - /// - /// Parses gamepad input data from an input report. - /// - private void ParseGamepad(ReadOnlySpan data) - { - // Buttons - ushort buttons = data.GetUInt16BE(GamepadOffset.Buttons); - ParseCoreButtons(buttons); - - device.SetButtonState(Xbox360Button.A, (buttons & GamepadButton.A) != 0); - device.SetButtonState(Xbox360Button.B, (buttons & GamepadButton.B) != 0); - device.SetButtonState(Xbox360Button.X, (buttons & GamepadButton.X) != 0); - device.SetButtonState(Xbox360Button.Y, (buttons & GamepadButton.Y) != 0); - - device.SetButtonState(Xbox360Button.LeftShoulder, (buttons & GamepadButton.LeftBumper) != 0); - device.SetButtonState(Xbox360Button.RightShoulder, (buttons & GamepadButton.RightBumper) != 0); - device.SetButtonState(Xbox360Button.LeftThumb, (buttons & GamepadButton.LeftStickPress) != 0); - device.SetButtonState(Xbox360Button.RightThumb, (buttons & GamepadButton.RightStickPress) != 0); - - // Sticks - device.SetAxisValue(Xbox360Axis.LeftThumbX, data.GetInt16LE(GamepadOffset.LeftStickX)); - device.SetAxisValue(Xbox360Axis.LeftThumbY, data.GetInt16LE(GamepadOffset.LeftStickY)); - device.SetAxisValue(Xbox360Axis.RightThumbX, data.GetInt16LE(GamepadOffset.RightStickX)); - device.SetAxisValue(Xbox360Axis.RightThumbY, data.GetInt16LE(GamepadOffset.RightStickY)); - - // Triggers - device.SetSliderValue(Xbox360Slider.LeftTrigger, (byte)(data.GetInt16LE(GamepadOffset.LeftTrigger) >> 8)); - device.SetSliderValue(Xbox360Slider.RightTrigger, (byte)(data.GetInt16LE(GamepadOffset.RightTrigger) >> 8)); - } -#endif - /// /// Parses guitar input data from an input report. /// diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index d0fea75..c561458 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -37,13 +37,6 @@ public void ParseInput(ReadOnlySpan data, byte length) // Parse the respective device switch (length) { -#if DEBUG - // Gamepad report parsing for debugging purposes - case Length.Input_Gamepad: - ParseGamepad(data); - break; -#endif - case Length.Input_Guitar: ParseGuitar(data); break; @@ -127,25 +120,6 @@ private void ParseCoreButtons(ushort buttons) // Other buttons are not mapped here since they may have specific uses } -#if DEBUG - // Gamepad report parsing for debugging purposes - /// - /// Parses gamepad input data from an input report. - /// - private void ParseGamepad(ReadOnlySpan data) - { - // Buttons - ushort buttons = data.GetUInt16BE(GamepadOffset.Buttons); - ParseCoreButtons(buttons); - - // Left stick - state.AxisX = data.GetInt16LE(GamepadOffset.LeftStickX); - state.AxisY = data.GetInt16LE(GamepadOffset.LeftStickY); - - // Don't map anything else, as there are not enough axes and this is meant for debug purposes only - } -#endif - /// /// Parses guitar input data from an input report. /// From e347fcb5fe9ac4ed81d66339b21ad83f701f8636 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 May 2022 13:47:59 -0600 Subject: [PATCH 038/437] Immediately return in vJoy mapper if an unknown report is encountered --- PacketParsing/VjoyMapper.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index c561458..a8a5b0f 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -44,9 +44,10 @@ public void ParseInput(ReadOnlySpan data, byte length) case Length.Input_Drums: ParseDrums(data); break; - + default: - break; + // Don't parse unknown input reports + return; } // Send data From c8ce5bc089ef557f4afc1f20057ee824030daf5c Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 May 2022 13:48:35 -0600 Subject: [PATCH 039/437] Reset report when closing mappers --- PacketParsing/VigemMapper.cs | 5 +++++ PacketParsing/VjoyMapper.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 00c252b..a85f0fe 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -237,6 +237,11 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length) /// public void Close() { + // Reset report + device.ResetReport(); + device.SubmitReport(); + + // Disconnect device try { device?.Disconnect(); } catch {} device = null; } diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index a8a5b0f..3f4d58c 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -239,6 +239,11 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length) /// public void Close() { + // Reset report + state.ResetState(); + VjoyStatic.Client.UpdateVJD(deviceId, ref state); + + // Free device VjoyStatic.ReleaseDevice(deviceId); } } From 39e9cbb3c6d9adf79538af1476b5fd29c49637ce Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 May 2022 13:50:02 -0600 Subject: [PATCH 040/437] Re-enable ViGEm drums velocity, without additional scaling this time --- PacketParsing/VigemMapper.cs | 74 ++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index a85f0fe..872ec72 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -171,50 +171,52 @@ private void ParseDrums(ReadOnlySpan data) (yellowCym | blueCym | greenCym) != 0); // Velocities - // device.SetAxisValue( - // Xbox360Axis.LeftThumbX, - // ByteToVelocity(redPad) - // ); - // device.SetAxisValue( - // Xbox360Axis.LeftThumbY, - // ByteToVelocityNegative((byte)(yellowPad | yellowCym)) - // ); - // device.SetAxisValue( - // Xbox360Axis.RightThumbX, - // ByteToVelocity((byte)(bluePad | blueCym)) - // ); - // device.SetAxisValue( - // Xbox360Axis.RightThumbY, - // ByteToVelocityNegative((byte)(greenPad | greenCym)) - // ); + device.SetAxisValue( + Xbox360Axis.LeftThumbX, + ByteToVelocity(redPad) + ); + device.SetAxisValue( + Xbox360Axis.LeftThumbY, + ByteToVelocityNegative((byte)(yellowPad | yellowCym)) + ); + device.SetAxisValue( + Xbox360Axis.RightThumbX, + ByteToVelocity((byte)(bluePad | blueCym)) + ); + device.SetAxisValue( + Xbox360Axis.RightThumbY, + ByteToVelocityNegative((byte)(greenPad | greenCym)) + ); /// /// Scales a byte to a drums velocity value. /// - // short ByteToVelocity(byte value) - // { - // // TODO: Figure out if this is necessary - // // Currently, this assumes the max from the kit is 0x04 - // value = (byte)(value * 0x40 - 1); - - // return (short)( - // (~value.ScaleToUInt16()) >> 1 - // ); - // } + short ByteToVelocity(byte value) + { + // TODO: Figure out if this is necessary + // This assumes the max from the kit is 0x04 + // value = (byte)(value * 0x40 - 1); + + return (short)( + // Bitwise invert to flip the value, then shift down one to exclude the sign bit + (~value.ScaleToUInt16()) >> 1 + ); + } /// /// Scales a byte to a negative drums velocity value. /// - // short ByteToVelocityNegative(byte value) - // { - // // TODO: Figure out if this is necessary - // // Currently, this assumes the max from the kit is 0x04 - // value = (byte)(value * 0x40 - 1); - - // return (short)( - // ((~value.ScaleToUInt16()) >> 1) | 0x8000 - // ); - // } + short ByteToVelocityNegative(byte value) + { + // TODO: Figure out if this is necessary + // This assumes the max from the kit is 0x04 + // value = (byte)(value * 0x40 - 1); + + return (short)( + // Bitwise invert to flip the value, then shift down one to exclude the sign bit, then add our own + ((~value.ScaleToUInt16()) >> 1) | 0x8000 + ); + } } /// From d03f9e4d3e69f8af67e7cd49b020bfbcf3adeee1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 May 2022 17:34:02 -0600 Subject: [PATCH 041/437] Don't reset controller state manually during input parsing --- PacketParsing/VigemMapper.cs | 5 ----- PacketParsing/VjoyMapper.cs | 6 ------ 2 files changed, 11 deletions(-) diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 872ec72..5c4b3b0 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -72,9 +72,6 @@ public void ParseInput(ReadOnlySpan data, byte length) return; } - // Reset report - device.ResetReport(); - switch (length) { case Length.Input_Guitar: @@ -227,8 +224,6 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length) // Only respond to the Left Windows keycode, as this is what the guide button reports. if (data[KeycodeOffset.Keycode] == Keycodes.LeftWin) { - // Don't reset the report to preserve other button information - // device.ResetReport(); device.SetButtonState(Xbox360Button.Guide, data[KeycodeOffset.PressedState] != 0); device.SubmitReport(); } diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index 3f4d58c..cd02ff6 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -31,9 +31,6 @@ public VjoyMapper() /// public void ParseInput(ReadOnlySpan data, byte length) { - // Reset report - state.ResetState(); - // Parse the respective device switch (length) { @@ -226,9 +223,6 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length) // Only respond to the Left Windows keycode, as this is what the guide button reports. if (data[KeycodeOffset.Keycode] == Keycodes.LeftWin) { - // Don't reset the state to preserve other button information - // state.ResetState(); - state.Buttons |= (data[KeycodeOffset.PressedState] != 0) ? VjoyStatic.Button.Fourteen : 0; VjoyStatic.Client.UpdateVJD(deviceId, ref state); } From 284d23990fe42955363b64b8c4da355e05e9297d Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 May 2022 17:34:52 -0600 Subject: [PATCH 042/437] Mask off non-fret values in the guitar fret bytes when assigning to vJoy buttons --- PacketParsing/PacketDefinitions.cs | 3 ++- PacketParsing/VjoyMapper.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/PacketParsing/PacketDefinitions.cs b/PacketParsing/PacketDefinitions.cs index 7796050..8464945 100644 --- a/PacketParsing/PacketDefinitions.cs +++ b/PacketParsing/PacketDefinitions.cs @@ -142,7 +142,8 @@ public const byte Red = 0x02, Yellow = 0x04, Blue = 0x08, - Orange = 0x10; + Orange = 0x10, + All = 0x1F; } /// diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index cd02ff6..fe0cac7 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -128,9 +128,9 @@ private void ParseGuitar(ReadOnlySpan data) // Frets // The fret data aligns with how we want it to be set in the vJoy device, so it can be mapped directly - state.Buttons |= data[GuitarOffset.UpperFrets]; + state.Buttons |= (uint)(data[GuitarOffset.UpperFrets] & GuitarFret.All); // Lower frets are mapped on top of the upper frets to allow both sets to be used in-game - state.Buttons |= data[GuitarOffset.LowerFrets]; + state.Buttons |= (uint)(data[GuitarOffset.LowerFrets] & GuitarFret.All); // Whammy // Value ranges from 0 (not pressed) to 255 (fully pressed) From 0a021897de31dce6db9fed50536e70b8a57d9293 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 May 2022 18:01:00 -0600 Subject: [PATCH 043/437] Check fret bits manually in vJoy mapper instead of assigning directly --- PacketParsing/VjoyMapper.cs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index fe0cac7..0336172 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -127,10 +127,33 @@ private void ParseGuitar(ReadOnlySpan data) ParseCoreButtons(data.GetUInt16BE(GuitarOffset.Buttons)); // Frets - // The fret data aligns with how we want it to be set in the vJoy device, so it can be mapped directly - state.Buttons |= (uint)(data[GuitarOffset.UpperFrets] & GuitarFret.All); - // Lower frets are mapped on top of the upper frets to allow both sets to be used in-game - state.Buttons |= (uint)(data[GuitarOffset.LowerFrets] & GuitarFret.All); + byte frets = data[GuitarOffset.UpperFrets]; + frets |= data[GuitarOffset.LowerFrets]; + + if ((frets & GuitarFret.Green) != 0) + { + state.Buttons = VjoyStatic.Button.One; + } + + if ((frets & GuitarFret.Red) != 0) + { + state.Buttons = VjoyStatic.Button.Two; + } + + if ((frets & GuitarFret.Yellow) != 0) + { + state.Buttons = VjoyStatic.Button.Three; + } + + if ((frets & GuitarFret.Blue) != 0) + { + state.Buttons = VjoyStatic.Button.Four; + } + + if ((frets & GuitarFret.Orange) != 0) + { + state.Buttons = VjoyStatic.Button.Five; + } // Whammy // Value ranges from 0 (not pressed) to 255 (fully pressed) From 3b6e431a76d9a7a25bd433e820c9168d51cac18e Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 May 2022 22:25:45 -0600 Subject: [PATCH 044/437] Re-add scaling to ViGEm drums velocity This doesn't make any assumptions about what the potential max is, just copies the lower nibble to the upper nibble --- PacketParsing/VigemMapper.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 5c4b3b0..9b057b6 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -190,9 +190,8 @@ private void ParseDrums(ReadOnlySpan data) /// short ByteToVelocity(byte value) { - // TODO: Figure out if this is necessary - // This assumes the max from the kit is 0x04 - // value = (byte)(value * 0x40 - 1); + // Scale the value to fill the byte + value = (byte)(value * 0x11); return (short)( // Bitwise invert to flip the value, then shift down one to exclude the sign bit @@ -205,9 +204,8 @@ short ByteToVelocity(byte value) /// short ByteToVelocityNegative(byte value) { - // TODO: Figure out if this is necessary - // This assumes the max from the kit is 0x04 - // value = (byte)(value * 0x40 - 1); + // Scale the value to fill the byte + value = (byte)(value * 0x11); return (short)( // Bitwise invert to flip the value, then shift down one to exclude the sign bit, then add our own From 1887a611823939aa6e91847137f09a5101c62e6f Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 11 May 2022 23:30:28 -0600 Subject: [PATCH 045/437] Remove uncertainty in drums pad velocity specs, and add speculated velocity ranges --- Docs/PacketFormats.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Docs/PacketFormats.md b/Docs/PacketFormats.md index 373ca18..7141d94 100644 --- a/Docs/PacketFormats.md +++ b/Docs/PacketFormats.md @@ -202,15 +202,17 @@ Bytes: - Bit 14 (`0x4000`) - Blue Pad (equivalent to X Button) - Bit 15 (`0x8000`) - Yellow Pad (equivalent to Y Button) - Bytes 32-33 - Pad velocities - - Bits 0-3 (`0x000F`) - Green Pad? - - Bits 4-7 (`0x00F0`) - Blue Pad? - - Bits 8-11 (`0x0F00`) - Yellow Pad? - - Bits 12-15 (`0xF000`) - Red Pad? + - Bits 0-3 (`0x000F`) - Green Pad + - Bits 4-7 (`0x00F0`) - Blue Pad + - Bits 8-11 (`0x0F00`) - Yellow Pad + - Bits 12-15 (`0xF000`) - Red Pad + - Seem to range from 0-7 - Bytes 34-35 - Cymbal velocities - Bits 0-3 (`0x000F`) - Unused? - Bits 4-7 (`0x00F0`) - Green Cymbal - Bits 8-11 (`0x0F00`) - Blue Cymbal - Bits 12-15 (`0xF000`) - Yellow Cymbal + - Seem to range from 0-7 ### Drum Packet Samples From b751e7fba89059d50d3a3b204a62a431c6438e82 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 12 May 2022 13:10:40 -0600 Subject: [PATCH 046/437] Check the sequence count of packets and ignore duplicates --- PacketParsing/IDeviceMapper.cs | 4 ++-- PacketParsing/VigemMapper.cs | 27 +++++++++++++++++++++++++-- PacketParsing/VjoyMapper.cs | 27 +++++++++++++++++++++++++-- PacketParsing/XboxDevice.cs | 4 ++-- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/PacketParsing/IDeviceMapper.cs b/PacketParsing/IDeviceMapper.cs index 7f6cf2a..9028c08 100644 --- a/PacketParsing/IDeviceMapper.cs +++ b/PacketParsing/IDeviceMapper.cs @@ -10,12 +10,12 @@ interface IDeviceMapper /// /// Parses an input packet. /// - void ParseInput(ReadOnlySpan data, byte length); + void ParseInput(ReadOnlySpan data, byte length, byte sequenceCount); /// /// Parses a virtual keycode packet. /// - void ParseVirtualKey(ReadOnlySpan data, byte length); + void ParseVirtualKey(ReadOnlySpan data, byte length, byte sequenceCount); /// /// Performs cleanup for the mapper. diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 9b057b6..448fce8 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -17,6 +17,9 @@ class VigemMapper : IDeviceMapper /// private bool deviceConnected = false; + private int prevInputSeqCount = -1; + private int prevVirtualKeySeqCount = -1; + /// /// Creates a new VigemMapper. /// @@ -64,8 +67,18 @@ void ReceiveUserIndex(object sender, Xbox360FeedbackReceivedEventArgs args) /// /// Parses an input report. /// - public void ParseInput(ReadOnlySpan data, byte length) + public void ParseInput(ReadOnlySpan data, byte length, byte sequenceCount) { + // Don't parse the same report twice + if (sequenceCount == prevInputSeqCount) + { + return; + } + else + { + prevInputSeqCount = sequenceCount; + } + if (!deviceConnected) { // Device has not connected yet @@ -217,8 +230,18 @@ short ByteToVelocityNegative(byte value) /// /// Parses a virtual key report. /// - public void ParseVirtualKey(ReadOnlySpan data, byte length) + public void ParseVirtualKey(ReadOnlySpan data, byte length, byte sequenceCount) { + // Don't parse the same report twice + if (sequenceCount == prevVirtualKeySeqCount) + { + return; + } + else + { + prevVirtualKeySeqCount = sequenceCount; + } + // Only respond to the Left Windows keycode, as this is what the guide button reports. if (data[KeycodeOffset.Keycode] == Keycodes.LeftWin) { diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index 0336172..db62cbb 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -8,6 +8,9 @@ class VjoyMapper : IDeviceMapper private vJoy.JoystickState state = new vJoy.JoystickState(); private uint deviceId = 0; + private int prevInputSeqCount = -1; + private int prevVirtualKeySeqCount = -1; + /// /// Creates a new VjoyMapper. /// @@ -29,8 +32,18 @@ public VjoyMapper() /// /// Parses an input report. /// - public void ParseInput(ReadOnlySpan data, byte length) + public void ParseInput(ReadOnlySpan data, byte length, byte sequenceCount) { + // Don't parse the same report twice + if (sequenceCount == prevInputSeqCount) + { + return; + } + else + { + prevInputSeqCount = sequenceCount; + } + // Parse the respective device switch (length) { @@ -241,8 +254,18 @@ private void ParseDrums(ReadOnlySpan data) /// /// Parses a virtual key report. /// - public void ParseVirtualKey(ReadOnlySpan data, byte length) + public void ParseVirtualKey(ReadOnlySpan data, byte length, byte sequenceCount) { + // Don't parse the same report twice + if (sequenceCount == prevVirtualKeySeqCount) + { + return; + } + else + { + prevVirtualKeySeqCount = sequenceCount; + } + // Only respond to the Left Windows keycode, as this is what the guide button reports. if (data[KeycodeOffset.Keycode] == Keycodes.LeftWin) { diff --git a/PacketParsing/XboxDevice.cs b/PacketParsing/XboxDevice.cs index 6bd6df2..a94ac61 100644 --- a/PacketParsing/XboxDevice.cs +++ b/PacketParsing/XboxDevice.cs @@ -48,13 +48,13 @@ public void ParseCommand(ReadOnlySpan commandData) switch (commandData[CommandOffset.CommandId]) { case CommandId.Input: - deviceMapper.ParseInput(commandData.Slice(Length.CommandHeader), commandData[CommandOffset.DataLength]); + deviceMapper.ParseInput(commandData.Slice(Length.CommandHeader), commandData[CommandOffset.DataLength], commandData[CommandOffset.SequenceCount]); break; // Probably don't actually want to parse the guide button and output it to the device, // so as to not interfere with Windows processes that use it // case CommandId.VirtualKey: - // deviceMapper.ParseVirtualKey(commandData.Slice(Length.CommandHeader), commandData[CommandOffset.DataLength]); + // deviceMapper.ParseVirtualKey(commandData.Slice(Length.CommandHeader), commandData[CommandOffset.DataLength], commandData[CommandOffset.SequenceCount]); // break; default: From b392de0721235730b6cae580d8ed13a8f4c7e5a3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 12 May 2022 14:27:23 -0600 Subject: [PATCH 047/437] Fix vJoy buttons not getting turned off --- PacketParsing/VjoyMapper.cs | 122 +++++++++++------------------------- 1 file changed, 36 insertions(+), 86 deletions(-) diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index db62cbb..846a617 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -1,6 +1,9 @@ using System; +using System.Runtime.CompilerServices; using vJoyInterfaceWrap; +using Button = RB4InstrumentMapper.Parsing.VjoyStatic.Button; + namespace RB4InstrumentMapper.Parsing { class VjoyMapper : IDeviceMapper @@ -64,22 +67,32 @@ public void ParseInput(ReadOnlySpan data, byte length, byte sequenceCount) VjoyStatic.Client.UpdateVJD(deviceId, ref state); } + /// + /// Sets the state of the specified button. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void SetButton(uint button, bool condition) + { + if (condition) + { + state.Buttons |= button; + } + else + { + state.Buttons &= ~button; + } + } + /// /// Parses common button data from an input report. /// private void ParseCoreButtons(ushort buttons) { // Menu - if ((buttons & GamepadButton.Menu) != 0) - { - state.Buttons |= VjoyStatic.Button.Fifteen; - } + SetButton(Button.Fifteen, (buttons & GamepadButton.Menu) != 0); // Options - if ((buttons & GamepadButton.Options) != 0) - { - state.Buttons |= VjoyStatic.Button.Sixteen; - } + SetButton(Button.Sixteen, (buttons & GamepadButton.Options) != 0); // D-pad to POV if ((buttons & GamepadButton.DpadUp) != 0) @@ -143,30 +156,11 @@ private void ParseGuitar(ReadOnlySpan data) byte frets = data[GuitarOffset.UpperFrets]; frets |= data[GuitarOffset.LowerFrets]; - if ((frets & GuitarFret.Green) != 0) - { - state.Buttons = VjoyStatic.Button.One; - } - - if ((frets & GuitarFret.Red) != 0) - { - state.Buttons = VjoyStatic.Button.Two; - } - - if ((frets & GuitarFret.Yellow) != 0) - { - state.Buttons = VjoyStatic.Button.Three; - } - - if ((frets & GuitarFret.Blue) != 0) - { - state.Buttons = VjoyStatic.Button.Four; - } - - if ((frets & GuitarFret.Orange) != 0) - { - state.Buttons = VjoyStatic.Button.Five; - } + SetButton(Button.One, (frets & GuitarFret.Green) != 0); + SetButton(Button.Two, (frets & GuitarFret.Red) != 0); + SetButton(Button.Three, (frets & GuitarFret.Yellow) != 0); + SetButton(Button.Four, (frets & GuitarFret.Blue) != 0); + SetButton(Button.Five, (frets & GuitarFret.Orange) != 0); // Whammy // Value ranges from 0 (not pressed) to 255 (fully pressed) @@ -192,63 +186,19 @@ private void ParseDrums(ReadOnlySpan data) ParseCoreButtons(data.GetUInt16BE(DrumOffset.Buttons)); // Pads - // Red pad - if ((data[DrumOffset.PadVels] & DrumPadVel.Red) != 0) - { - state.Buttons |= VjoyStatic.Button.One; - } - - // Yellow pad - if ((data[DrumOffset.PadVels] & DrumPadVel.Yellow) != 0) - { - state.Buttons |= VjoyStatic.Button.Two; - } - - // Blue pad - if ((data[DrumOffset.PadVels] & DrumPadVel.Blue) != 0) - { - state.Buttons |= VjoyStatic.Button.Three; - } - - // Green pad - if ((data[DrumOffset.PadVels] & DrumPadVel.Green) != 0) - { - state.Buttons |= VjoyStatic.Button.Four; - } - + SetButton(Button.One, (data[DrumOffset.PadVels] & DrumPadVel.Red) != 0); + SetButton(Button.Two, (data[DrumOffset.PadVels] & DrumPadVel.Yellow) != 0); + SetButton(Button.Three, (data[DrumOffset.PadVels] & DrumPadVel.Blue) != 0); + SetButton(Button.Four, (data[DrumOffset.PadVels] & DrumPadVel.Green) != 0); // Cymbals - // Yellow cymbal - if ((data[DrumOffset.CymbalVels] & DrumCymVel.Yellow) != 0) - { - state.Buttons |= VjoyStatic.Button.Six; - } - - // Blue cymbal - if ((data[DrumOffset.CymbalVels] & DrumCymVel.Blue) != 0) - { - state.Buttons |= VjoyStatic.Button.Seven; - } - - // Green cymbal - if ((data[DrumOffset.CymbalVels] & DrumCymVel.Green) != 0) - { - state.Buttons |= VjoyStatic.Button.Eight; - } - + SetButton(Button.Six, (data[DrumOffset.CymbalVels] & DrumCymVel.Yellow) != 0); + SetButton(Button.Seven, (data[DrumOffset.CymbalVels] & DrumCymVel.Blue) != 0); + SetButton(Button.Eight, (data[DrumOffset.CymbalVels] & DrumCymVel.Green) != 0); // Kick pedals - // Kick 1 - if ((data[DrumOffset.Buttons] & DrumButton.KickOne) != 0) - { - state.Buttons |= VjoyStatic.Button.Five; - } - - // Kick 2 - if ((data[DrumOffset.Buttons] & DrumButton.KickTwo) != 0) - { - state.Buttons |= VjoyStatic.Button.Nine; - } + SetButton(Button.Five, (data[DrumOffset.Buttons] & DrumButton.KickOne) != 0); + SetButton(Button.Nine, (data[DrumOffset.Buttons] & DrumButton.KickTwo) != 0); } /// @@ -269,7 +219,7 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length, byte sequenceC // Only respond to the Left Windows keycode, as this is what the guide button reports. if (data[KeycodeOffset.Keycode] == Keycodes.LeftWin) { - state.Buttons |= (data[KeycodeOffset.PressedState] != 0) ? VjoyStatic.Button.Fourteen : 0; + SetButton(Button.Fourteen, data[KeycodeOffset.PressedState] != 0); VjoyStatic.Client.UpdateVJD(deviceId, ref state); } } From a93b46c0e0c9f08cb0a22f5786e11d995de4bd4a Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 12 May 2022 14:31:49 -0600 Subject: [PATCH 048/437] Add .vscode folder to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 0f947f2..bb167b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# VS Code configuration folder +.vscode/ + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## From 4ad9181fb649495fa902eadae8667749bd5e26f1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 12 May 2022 16:03:18 -0600 Subject: [PATCH 049/437] Fix potential NullReferenceException in VigemMapper --- PacketParsing/VigemMapper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 448fce8..49300dc 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -256,8 +256,8 @@ public void ParseVirtualKey(ReadOnlySpan data, byte length, byte sequenceC public void Close() { // Reset report - device.ResetReport(); - device.SubmitReport(); + try { device.ResetReport(); } catch {} + try { device.SubmitReport(); } catch {} // Disconnect device try { device?.Disconnect(); } catch {} From c46f11962ac242ec93b6443852053f8e8c5315c7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 24 May 2022 02:33:25 -0600 Subject: [PATCH 050/437] Change installer project's platform to x64 --- Installer/RB4InstrumentMapperInstaller.wixproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/RB4InstrumentMapperInstaller.wixproj b/Installer/RB4InstrumentMapperInstaller.wixproj index 3be620a..53cfcde 100644 --- a/Installer/RB4InstrumentMapperInstaller.wixproj +++ b/Installer/RB4InstrumentMapperInstaller.wixproj @@ -2,7 +2,7 @@ Debug - x86 + x64 3.10 047562bb-6d63-4259-8e2e-0a5e834190b1 2.0 @@ -10,12 +10,12 @@ Package RB4InstrumentMapperInstaller - + bin\$(Configuration)\ obj\$(Configuration)\ Debug - + bin\$(Configuration)\ obj\$(Configuration)\ From 98066af6d100a6d357600547e213882dd9a2365f Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 24 May 2022 02:39:12 -0600 Subject: [PATCH 051/437] Disable solution properties for installer project outside of solution builds Fixes a warning from wix2010.targets --- Installer/RB4InstrumentMapperInstaller.wixproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Installer/RB4InstrumentMapperInstaller.wixproj b/Installer/RB4InstrumentMapperInstaller.wixproj index 53cfcde..c5a2320 100644 --- a/Installer/RB4InstrumentMapperInstaller.wixproj +++ b/Installer/RB4InstrumentMapperInstaller.wixproj @@ -19,6 +19,9 @@ bin\$(Configuration)\ obj\$(Configuration)\ + + false + From cc162e4421d5b65931f9e1bc36f016563f78f254 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 24 May 2022 02:47:06 -0600 Subject: [PATCH 052/437] Move program install location from Program Files (x86) to Program Files --- Installer/Product.wxs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 19aca14..049bef7 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -31,7 +31,7 @@ - + From 73b445bc08c6c310157d3bd54aebdc8d39698059 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 24 May 2022 02:47:45 -0600 Subject: [PATCH 053/437] Update installer file inclusion list --- Installer/Product.wxs | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 049bef7..3e6a855 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -44,25 +44,33 @@ - - - - - - - - - - - + + - - + + + + + + + + + + + + + + + + + + + From a585a1a693f1be51daef4be4ca06d9eeaa50e174 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 24 May 2022 02:49:26 -0600 Subject: [PATCH 054/437] Add start menu shortcut to installer, and add uninstall condition for desktop shortcut --- Installer/Product.wxs | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 3e6a855..39c2356 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -26,6 +26,7 @@ + @@ -35,6 +36,9 @@ + + + @@ -81,6 +85,7 @@ + - - + Advertise="no" /> + + + + + + From ff21ca1188e85eae27f8ebbf06a06f304a131d27 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 24 May 2022 02:51:14 -0600 Subject: [PATCH 055/437] Add application icon Previously it was just the installer and shortcut that had it --- RB4InstrumentMapper.csproj | 1 + icon.ico | Bin 0 -> 90022 bytes 2 files changed, 1 insertion(+) create mode 100644 icon.ico diff --git a/RB4InstrumentMapper.csproj b/RB4InstrumentMapper.csproj index ae1302d..1a5fd56 100644 --- a/RB4InstrumentMapper.csproj +++ b/RB4InstrumentMapper.csproj @@ -8,6 +8,7 @@ WinExe RB4InstrumentMapper RB4InstrumentMapper + icon.ico v4.7.2 512 {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..16cc380d8f9cf75c35ce21dd597a0a19a2d1ac2e GIT binary patch literal 90022 zcmeEv2Uu2D)~@NrBx>y4SSf-u#ol}Gz4rzxf*^=A0qF=x5fHFYRIq{tRBUv6i7_Uo zOs1tw=KpW`?|*0Jz3(r?;aA3*#;5v1zd97W$;`8MCb?StVexpu(9_9X(Z`Y~Qf6g0q^!SV`8!xKs z^0T6U0P4JmwleoW-eIfDHjK@c&6DlF+3Kw=%$y~{t$=t~tzVjkrRNL=dllPx> z*w=b&PHbn`{s-GjhxyvP|9~|czx|tOtFHVFt8@QezP~beGKVskGN<)qj%yokHGLoZ z^^Ww7O>p*Cw({e6;pb~OcfPrQg4>aN_@|sfY}zT?r(eb>{e`+_U2HFBmv#B3iX*fi9i9!1ZwcvwT zg$E_DAI0y!5;`w1*P843Z?MwZ2A|fikGk)_kA|OogtpBaqN9R+Qw4j0|7R_l7Ppp7 z6-{vU*oOGmgcexk{0cX3f3$4g9F0Ex6xMc*u&N#nD4mBASd4nnIKc0G$o9(Dyud#o z_m9}@?1ILP8sbC3UTYtHKs=~d7pB~~t9ibEH# zW8G`O59UXmRO`QaQ{ty_H_9-Dy0ES8Z;cjwSW|hfqUTFspCNPqs@sO#UlF%=9}M(6 z;e!u8zz2l?2k*V7+WS1d|NgtA2k)WYM|IJdYpO+)#%R^N30g~QMm%WV80|<4W=@}q z{h9f&&HELO`MS>ZkL|U9{VL*L_}SYS zGinGvZTKnb3GM&*W7Mr%7atNA-X|^y4rurw$GW5m^*;UxjTcq-hK+@+|0azZp?9A?@H&3AChx2@9C)2zFR*dS z`x~}-Z%2E|bDt8H4H`E>ee!^M4cWMM1Rp;6%U#wHfZ1eGqk7fFl1mq zgdNI+$Yb)IXK5QwKg5dov#JF@r*5%0`V?BXY{`B#Lz5;=Fk-|Ac&A@`rhZh==jX-& zzU#H2`P#UI@B8lvLB|g5(11K$nlAi>7JN$jS-v$SoluQ`Ysl7^&oyh-6fLQ9$y)pD zv(M0>eS6|Se*`9<#M)=Tf#pZ53#@y7IY9gVGvh#sjwf=y_Kw%$C_3BB8I#e3uxp@be_8mKhkIq`f(wmH znyfW}Tbov`(4mA51Q7?SBMWRj|1AcNo``qec?WO2@dn;}<1M`X*4yYYXarnSbDs&1 z#O^a_%p^1?{g)>F8a;Xpwx?gOjk$NNu5PHK-@j73PI=s0OZ#n5QaVgcx{~iVQSdLn z_I1~PBf_@{*O9FE@_3-p1Gz`=pb_tp`IfcUwsk9v8a@OGr*fXE8%lqicBLwL^UXKk zRPcY}jW_Y;n{T0By$0ygMGvb3_h1d}JLShx(Ye_ESNX;k(-HPt^nkvU1DC9{g5Bva zA@=Qxo~!0Hfu*t!{Oe!;sw1r;9Z#SQ}#n_TGE%n z0|%o2;NftNPQ^Ov3EI8SaTJ)=6WIUrpZ~1$kAM8*2|RdR>As9LH1j4#j~s%=gtfp< z!vQ^gJ*-{59D8@~!qJ2K;p@E(6DN#^-DU?kZ}Y%L`%UOZUMg@`yuSqQ@{UrtbM4Fe zm)2D10O!3k^$Wk?P^{0bTo2!#^CviX1)_PUu6XBpa--iLKz+SLd>%`MQKvcdFu%dk1+3f5fu z8DckH#kPiiL*Z3c)$?~MJ|OUyR-O)Up2e5*TG4%hy$$U%QM=eYuuHxq>ey}I{+ zrA2piA21R_#!SJ?c}uZ@_Tu1>2zbP%VP?chERQ=2$3wXYK64xU3ctf@(u`7E$^E;) zc9_Nkbm7nI;=>i6(o@I#D*m2S_$`(#U4&-Mo1qb5BdufSPS~|G2oLWR;G55j@FkC5 z-Y!4^;UBs)6z*Q0u-UjCUOt}KV)(;JW)5wzzsBU`GTZcg2wwL}BEJG3eL3AGUjWqko@%=reFA z7I{X&Dua3gpOf{w>*xj2`ax(w+lAPG#BNbnY@4)S2|qB<(?fT2bC{EsSz7c!pPs$d zyZZL(18Kc_^h6Ky?!~PK@q}aT*RMZ}`wm5K+LmU_Ukd-zw*|HW<99S1(BuGK6Tbf% zuKm9wG3^-TKBH3DH=&$nW3v_oIcM?c^Q-vsZXxbo&%urSY_{CU-T@I7h182%-xhQZ%Y0m@NdpzyH+i+aN!~qKft-E*1E7J zJVOp=!_CVFz5$^K3yVc$L;_|^n}@lx=OQ{X3R@kW(9N(bx|#RF0`~|wN2JhCJrKPgK;)13#(SHL_Oldd(aBFzlia zgHF7!Lno!nicZ_MZQIf|XyiC-Kl?z&ATU;PfVTc$XTHDt@&hcKKN~HmtIN7?%+|ZN zCC;5Xf^Y6!Bi%2;9m2lw%30hjxQN2bXHl4wg>#w55lueeh{dSaVLAlH2eq9gWTBk6zl?v_}+dVdeMqPn7Cs?xOX$?s^BkpAm_$BHzTj;)4MnN zQf3@JaXQw~H}1DV_FBpOVn5k_`44d1vIU(xwkuxygh^Ab z(@h)KDZ2mN{cHG&wEuR|W!x^jLOCFpEe}QH0Yz7`ke!u*i0}~lSGU2R@OQAcrCr4s z?YS5L|1bXQ zfB$d%^{;=0i;D|ZuU?HT(t`sB4k(+Dk&%%)_MvXrf5-@onmiNM8yyf3y&towkF48& z0ikE_LhO*j_lxDjva&`AtQ8F?(fK5{?3u^-qMi|XFZSYn*i+xr*Y8M~pZYM@eM2^h zlSxgE$G3#Nv~Soxr~G&4YA$&|K0d!$fX{Cf;9Aak(t<2xojQ&H(*K>56&BB*MH_2# zv}@BE>o#oQ^&rB32b^8DVDH{oT)un;XV0c1FgO5x`}I@spEYORGtqzDIT6`#^R6^3 zS-Kiam#v4rlP|Vx3&*y={g^y=E#7|r6TC~k;bX%8ed>?SPFwKv&p*N6-ye%ei^N{N zcI{gB=`6C&o`dzCbC?-OfI>XD zRe+m$*@_liICmOnPannE^fcI5ts;;7j5MVKW=x+BJ6l_9SZ57uUbop`g(ZvT(e5@} z+2UJK-t_eGdoG`Vh6fVYxx{BTW-nNRnTu>NZ>=8|Zcc=idpwMK4dnWN15N3>Z1B!U zl=GTm*zn=_q+UJR6W*k3(i(R5n~|BBiS$!vYSa&{KRQPx2l8CvizUXiDtviie;3^~ zF#j(IPd$S*YuCV}n<1JrPNO0DbR)uDYpvR}LSTRwZeG1anV+zy-1ik>FYRmoesD97 z@Gm6%uM++|Cr;cgpiD@)pn!70y`l@aN!;19YbVyPS%Jyp$6)Y)zOd|W1~U^Q%2}P2 zPe6FT=zs?LdWbxJ84^!illc)HY~20{7?&ivO*+oyE_QgYROU+U6Kb~=_w&5=@*W&a4yC&nS$+P0qIdtJFoSYretCu-( zzF5vv^j-)4jp?tOJZT*A$k)Fj?7tG&-_9?Ezu+;bVYoOK-OX^GglZWUT^-HRQ_YTbq>VgQoVtY3Qx;;@at};d zxCztduZM%P55`ZJMqRVm58bg{JM^G_Z`{2%LJnmq|AG6_LYz5s21kykKa)S98ZZ%< zi@oQS`FZ?v{)o7JiL}>^q7SZ(;`60;SIh%{`&0~`2LG)xIvxqI`xCEZs*}n!3EsPJBQCn7vz06@-E=i z;Ush>e;7S_1X3>EEVK3n_K}q9=Pg-@u~TQOxXtc^M#8wy5OnQ57`^)rQ1W`Oo;|3i zo1rV^@h;>uZ5W5DZ=jEl>Ni72eM?N6HiNOK(`nPNrfeSzQ!@)#_UMjI^hNg`JQ@qr zZkO8oJ!lg;efl)gj-7zS3ReTJ&vK8z-m{k9S>^XPkeeXkvClMST!HxP+qQ0v&!|&$pw8Wqeq{ZQohe^*MB8?4`1fXLL3=}8`Uc*9 z^KJSXn)eGn(i$^o&VhScZkca-ow$mVCr={v$nh5ee}Vl= zr~P|!A5w?2qK?&G`EtdkuM7KP_{;ilf^OZ65E|@5*)9kBc1KVyoQTBuFx)B1eiHU2 z>tE>wCBMHR9^50GZ{?mLzT{F@xXAT?3SZv1Kpe_pyMk|c{d#UD{I zo1d(|>EY?Hb##Trz!B)u)r7XB#!7}4JB`Gke)PeI__S_)#mk#9&aiEpmM|oI8-G$C zAG}T9;Ky|-=e0&Z>Tlb&Im3`Kj7{n@?suI%+_Jtx!i8H{qa$x@XADS2Mg|TYK1Tju z>+Aoe!#*JI&v0>bp=?zQd-3HrW)s~`;I6@5^!(w&hG2jEE@j^r8SZp?DwZ!@fbnC8 zBYsyPdH4m||BC6p*aDs4M2HcPHun^0!#M=R8(N9);Z&SC|hNfo>+I)Ek=@$DV!iF+OJ8p~Rilr!DUj z_4@A|~izJX6@I&d;*Vj(E5QNE0r!iW_zE78FBIlvBW4%hwP*p>QqGGx zegPpxKR(y|irr6ieew10$^RB>h!b;m9)ra3S_~XXyPZBhBo0Vys>CFU&i~$9Z{d9& z#qMXd-WC$)BY1WEio~Qp);D%!-BIvAapDBhZvFYW`uDovMqvJ*v2^)jv}ncn7Q($H z?dv@)dtmmwg;-^^0h6Z9MAxo{=s@{v-`?H0N?U#&X5I&4AgiHpjPvbL7> zU$I|G9GT!mAbp5KZ2cIo+z4jkhos!lh4MiM#w@gK+Kl5EhMng>SNBI|e_keb%#(4z zVgp>aegpi9?f1V~9N2m8E`|>2uXNlFl#AC=*H6jHSL;q}SYiW+OgoF|)23qD^eG4n z4TK+IKW6M$43Iboww}HFV(H=q$T*w`HzymUCB~Aj7pb+ct$&gE#3uBJ@}BU48%0+T zzdIVsmo3JU6{`>xw-eIIC1kdb8PsA-q zK2BWvMOadY>e;IoMvWeg0fPo%(v&F}F=`a?U;y7S2uZZpekHmfdAYz@@qWTx===R! zxr*L@bGHCT5ADN}B@5{T@Pq%ceE5;iT9=dSU&L4|dgtbp%gUGImv9)f7A`A=zonTu zR;*o*4VQmHVAj1dFdiFnOu^s8!~_AMk*}=p6^-Xvuf<>c3+=5Nv5T=B;@he0`6R|I zNG|W-Aw#+52NC{*F=FIM3>Yv_(SeQY*5KTkWB6Lpcd-E%!(Z${vfjUcaGf$DsBk$D5p8k1XD|DN}{!~n&_#K4L18EZ~_j=_xgG-F(d(0~!-0YQvy56mnJ z{|RBol|SIfkt5g{9rv4tzr^rm=3d9fr^Hmr`cKHXj7gKHpeJJ`#*7<}x%1|u|A0Xl zF?N$>CdZr zR#&zy-v`OfPrG~*D;fLHoBp@q%z+rd^*@mGe(>NSh>hQeos`ifPbd1)1K2t^A}%Ue z@%lgL;DGRjtxmSs$K1YZ#>KtxdELOVWu3o({fYbGeykY&q7Usll8r(A2B4QkcgnP@ zkbN|7?gOmfu%32Q!hZNLELyw>Lxv5*kRd~` zJAtq#{m|~0V-)3yh4bez7UVF#`{F7a^?%~QrOd;O_3%;hllomX0}RA>)af6Q_upbHKqzgvF~_rCo3$_V zCAMVo;iV*}z?*&{-8mG0(pvxB@bmM+rl2?koV<;Q^IxjiO>62X$!RG#dHOg+e%5{N z-{rR#>)&i~+lIcR^@E2E#oW2`FqE`^$gmOE6&sJRO6)SSrVbrGN_)CzvG0fL|7uPq z?P=7r3jg$4&6UJ>9;OfHz`+C9o>3P5o|k`sQ+Og4%%4x2=Lqztont^h%DN*bVVPGH zA{oba`b-9bX$$&yfw?vg(F5G6n+>F_FLHj*-hD8Xa^4{F`<+qI*irD8O2;hge`iD_ z;$lMa4e9^4wDs?f3?{!!gYaAJ8?VDr;=vB29>Jm1L#6N+p11Mz11x1M+FI)slx@AR zGcu5I;~uczY>iQ)=vy>0W9+BCii_}|{`xwMrMCLu&l(;9J3=v&x?Vrpc6!maGmNkw zK-ddie;Vwi4b>wEBR5?OEjIXb)KGxQIgpcn}Ifx_l&j>Rixz_PCqjLqA_ z$f7MwEZWo7-Ie(r6A-q`2ZcAY5xjFd42=w6IbXB5i;frUT9E8!@%MjwI~dC1Eg zPfflRPB}h)?|zIOKc4Z{12L2^A3bI)<}Fx&ps;WR(N9p-`*%?mTCsc)&M{u|RK{WY zhU^i2{9+m1sj7Fp%)iGmciG&+LdA3jhK4des0fh>-k3OZFuIxP!IW{^-RMu2_-!*& z#`T*R@_HAT_3VrlRFJo;}q=0M^qiSY-j9{i-#wi7(=o>Fa#0wo0N~&sLp#{);*Eg z_c39@1m+wp#ujHM96XZ9{C>tdc5lykZbRAuEnwEw0;Yzh#dttJvWW@tz|06nCY>>R z;Ut{SK8k=4Z!}|!;8M>>OqsI~@4ibv^1JWQ?`#6!(`9+X%NkpCzazCZ0+UZ+($uLK zOnYY!%U;U1Ka_f2V93sDt$W>XiNE&1^*=r}M^pFySNYqIwMnC*V{!E8QF!e>fc?n{ zSg>pY%*^!|D`o&QgC4LjHi0>FdrXZ?nd@cBm{en!lOA+8V}6HuC#+aM6Q?g7B;6c= zhRhNC@B`Mi;o6rNnR!cBRGDA;D#tJSPsEY)jB%QYVIxK|?s1~B?e(U-?-v|`K>B4~ zW&d9Ew}mzxOFxA@`}QNU=+8KCESWLkV_{0Tni(6yg6rSBYd2M^>;tapW~^}_d0iHa zdo(xc$~ZaZ0rxjTK!h{%J-r!E@-g0f_kFBbu>!eyIf#jmS9wP-dK}O5e(|^M&bWe^ zvx|K?LhFYU2ZnLo`ve5Dmf!28t3S^et9nNCKe1Kpq5VJg&R=kz@h7X-%_ID~z{1P` zCi<3iqj>rK4PS|W|=HQ@R;rc7W&8bSGhv|_2%4D8t-&Un&J*tE$W9`0Vu zTe*VN?4sAo-lffl_;V5|>nvNoT

ZdiJF~e=x?49mCuTfB2D)Yxn*;A4UI5I*^Lp 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 Date: Tue, 24 May 2022 03:01:41 -0600 Subject: [PATCH 056/437] Increment version number to 2.0.0.0 --- Installer/Product.wxs | 2 +- Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 39c2356..0a1f361 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -4,7 +4,7 @@ Id="*" Name="RB4InstrumentMapper" Language="1033" - Version="1.22.0.0" + Version="2.0.0.0" Manufacturer="Andreas Schiffler" UpgradeCode="94bef546-701f-4571-9828-d4fa39b2ea84"> diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 1d2a3d9..911c44a 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.23.0.0")] -[assembly: AssemblyFileVersion("1.23.0.0")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")] From ce7b114f9140e300ec2cefdaebaa76482ec9e468 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 24 May 2022 04:04:53 -0600 Subject: [PATCH 057/437] Update readme for new version --- Docs/Images/ProgramScreenshot.png | Bin 70271 -> 70271 bytes README.md | 78 +++++++++++++++--------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/Docs/Images/ProgramScreenshot.png b/Docs/Images/ProgramScreenshot.png index 5f0f19402d1376b24e1c1a7e9d7c7d0399f61833..c6cf44af62a4d8fed63882facc9077726edf0e2a 100644 GIT binary patch delta 48962 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 `Devices` > `Bluetooth & other devices` +- Click `Add Bluetooth or other device` and pick the `Everything else` option. ## Installation -1. Install [WinPCap](https://www.winpcap.org/install/default.htm) (recommended as that seems to work best), or [Npcap](https://nmap.org/npcap/#download) in WinPCap compatibility mode. - - Make sure you only install one or the other. Installing both at the same time may cause issues. +1. Install [WinPCap](https://www.winpcap.org/install/default.htm). + - Do not install Npcap, as it doesn't seem to work with Xbox One receivers. 2. Install [USBPCap](https://desowin.org/usbpcap/). 3. Install [ViGEmBus](https://github.com/ViGEm/ViGEmBus/releases/latest) (recommended) or [vJoy](https://github.com/jshafer817/vJoy/releases/latest). - If you installed vJoy, configure it: @@ -41,30 +48,27 @@ Almost all features for guitars and drums are supported. 1. Configure the selected Pcap device: - Click the `Auto-Detect Pcap` button and follow its instructions. - If that doesn't work, then if you installed WinPcap, try installing Npcap instead, or vice versa. -2. Configure the selected controller device for each guitar and drumkit: - - If you installed vJoy: - - Pick one of the vJoy devices that you configured for each instrument you will be using. - - If you installed ViGEmBus: - - Pick the `ViGEmBus Device` item in the dropdown for each instrument you will be using. One emulated Xbox 360 controller will be created for each instrument that has this selected. +2. Select either vJoy or ViGEmBus in the Controller Type dropdown. 3. Connect your instruments if you haven't yet. -4. Assign the instrument ID for each instrument: - - Click the `Auto-Detect ID` button next to each ID field. - - Guitars should auto-detect automatically as they are constantly sending packets. Retry if duplicate IDs were detected (and rejected). - - Drums require an action such as a button press on the instrument you are assigning within 2 seconds after 'Auto-Detect' was clicked. -5. Click the Start button. - - Note: launch *joy.cpl* to check Controller inputs. -6. Map the controls for each instrument in Clone Hero: +4. Click the Start button. Devices will be detected automatically. +5. Map the controls for each instrument in Clone Hero: 1. Press Space on the main menu. - 2. Click the Assign Controller button and do an action on the instrument to be assigned. - 3. Click the slots in the Controller column to map the controls for one of the instruments. - 4. Repeat for Player 2 and 3. + 2. Click the Assign Controller button and press a button on the instrument for it to be assigned. + 3. Click the slots in the Controller column to map each of the controls. + 4. Repeat for each connected device. 5. Click `Done`. -Selections and IDs are saved and should persist across program sessions. +Selections are saved and should persist across program sessions. + +## Packet Logs + +RB4InstrumentMapper is capable of logging packets to a file for debugging purposes. To do so, enable both the `Show Packets (for debugging)` and `Log packets to file` checkboxes, then hit Start. Packet logs get saved to a `RB4InstrumentMapper` > `PacketLogs` folder inside your Documents folder. Make sure to include it when getting help or creating an issue report for packet parsing issues. + +Note that these settings are meant for debugging purposes only, leaving them enabled can reduce the performance of the program somewhat. ## Error Logs -In the case that the program crashes, an error log is saved to a `RB4InstrumentMapper` > `Logs` folder inside your Documents folder. Make sure to include it when creating an issue report for a crash or when getting help for a crash. +In the case that the program crashes, an error log is saved to a `RB4InstrumentMapper` > `Logs` folder inside your Documents folder. Make sure to include it when getting help or creating an issue report for the crash. ## References @@ -75,18 +79,14 @@ Packet Data: - [GuitarSniffer guitar packet logs](https://1drv.ms/f/s!AgQGk0OeTMLwhA-uDO9IQHEHqGhv) - GuitarSniffer guitar packet spreadsheets: [New](https://docs.google.com/spreadsheets/d/1ITZUvRniGpfS_HV_rBpSwlDdGukc3GC1CeOe7SavQBo/edit?usp=sharing), [Old](https://1drv.ms/x/s!AgQGk0OeTMLwg3GBDXFUC3Erj4Wb) -- See [PacketFormats.md](PacketFormats.md) for a breakdown of the known packet data. - -## Build Tools +- See [PacketFormats.md](/Docs/PacketFormats.md) for a breakdown of the known packet data. -Compile was done with Visual Studio 2019 Community edition. +## Building -Installer was created using the following tools: +To build this program, you will need: -- https://wixtoolset.org/ - - https://wixtoolset.org/releases/v3.11.2/stable - - https://marketplace.visualstudio.com/items?itemName=WixToolset.WixToolsetVisualStudio2019Extension - - https://marketplace.visualstudio.com/items?itemName=TomEnglert.Wax +- Visual Studio (or MSBuild + your code editor of choice) for the program +- [WiX Toolset](https://wixtoolset.org/) for the installer ## License From 7a017fbac8cb685e23fb00170398bae6eb9ea529 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 10 Jul 2022 19:42:17 -0600 Subject: [PATCH 058/437] Update drums packet format info Nothing is uncertain now, and yellow and blue pads don't have respective button inputs, only velocities --- Docs/PacketFormats.md | 10 +++++----- PacketParsing/PacketDefinitions.cs | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Docs/PacketFormats.md b/Docs/PacketFormats.md index 7141d94..49d3f37 100644 --- a/Docs/PacketFormats.md +++ b/Docs/PacketFormats.md @@ -191,16 +191,16 @@ Bytes: - Bit 3 (`0x0008`) - D-pad Right - Bit 4 (`0x0010`) - 1st Kick Pedal (equivalent to Left Bumper) - Bit 5 (`0x0020`) - 2nd Kick Pedal (equivalent to Right Bumper) - - Bit 6 (`0x0040`) - Unused? (equivalent to Left Stick Press) - - Bit 7 (`0x0080`) - Unused? (equivalent to Right Stick Press) + - Bit 6 (`0x0040`) - Unused (equivalent to Left Stick Press) + - Bit 7 (`0x0080`) - Unused (equivalent to Right Stick Press) - Bit 8 (`0x0100`) - Sync button? - Bit 9 (`0x0200`) - Unused (undefined?) - Bit 10 (`0x0400`) - Menu Button - Bit 11 (`0x0800`) - Options Button - Bit 12 (`0x1000`) - Green Pad (equivalent to A Button) - Bit 13 (`0x2000`) - Red Pad (equivalent to B Button) - - Bit 14 (`0x4000`) - Blue Pad (equivalent to X Button) - - Bit 15 (`0x8000`) - Yellow Pad (equivalent to Y Button) + - Bit 14 (`0x4000`) - Unused (equivalent to X Button) + - Bit 15 (`0x8000`) - Unused (equivalent to Y Button) - Bytes 32-33 - Pad velocities - Bits 0-3 (`0x000F`) - Green Pad - Bits 4-7 (`0x00F0`) - Blue Pad @@ -208,7 +208,7 @@ Bytes: - Bits 12-15 (`0xF000`) - Red Pad - Seem to range from 0-7 - Bytes 34-35 - Cymbal velocities - - Bits 0-3 (`0x000F`) - Unused? + - Bits 0-3 (`0x000F`) - Unused - Bits 4-7 (`0x00F0`) - Green Cymbal - Bits 8-11 (`0x0F00`) - Blue Cymbal - Bits 12-15 (`0xF000`) - Yellow Cymbal diff --git a/PacketParsing/PacketDefinitions.cs b/PacketParsing/PacketDefinitions.cs index 8464945..0503376 100644 --- a/PacketParsing/PacketDefinitions.cs +++ b/PacketParsing/PacketDefinitions.cs @@ -161,8 +161,6 @@ static class DrumButton { public const ushort RedPad = GamepadButton.B, - YellowPad = GamepadButton.Y, - BluePad = GamepadButton.X, GreenPad = GamepadButton.A, KickOne = GamepadButton.LeftBumper, KickTwo = GamepadButton.RightBumper; From 84ff64309762f27de320f64696579fb17893ee16 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 10 Jul 2022 19:45:15 -0600 Subject: [PATCH 059/437] can't believe these slipped by me lol Add pedal mapping to ViGEm mapper Fix kick pedal mappings in vJoy mapper Fix cymbal/pad overlaps in ViGEm and vJoy mappers --- PacketParsing/VigemMapper.cs | 8 +++++++- PacketParsing/VjoyMapper.cs | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/PacketParsing/VigemMapper.cs b/PacketParsing/VigemMapper.cs index 49300dc..00353ec 100644 --- a/PacketParsing/VigemMapper.cs +++ b/PacketParsing/VigemMapper.cs @@ -164,7 +164,7 @@ private void ParseDrums(ReadOnlySpan data) byte greenPad = (byte)(data[DrumOffset.PadVels + 1] & DrumPadVel.Green); byte yellowCym = (byte)(data[DrumOffset.CymbalVels] >> 4); - byte blueCym = (byte)(data[DrumOffset.CymbalVels] & DrumPadVel.Blue); + byte blueCym = (byte)(data[DrumOffset.CymbalVels] & DrumCymVel.Blue); byte greenCym = (byte)(data[DrumOffset.CymbalVels + 1] >> 4); // Color flags @@ -180,6 +180,12 @@ private void ParseDrums(ReadOnlySpan data) device.SetButtonState(Xbox360Button.RightShoulder, (yellowCym | blueCym | greenCym) != 0); + // Pedals + device.SetButtonState(Xbox360Button.LeftShoulder, + (data[DrumOffset.Buttons + 1] & DrumButton.KickOne) != 0); + device.SetButtonState(Xbox360Button.LeftThumb, + (data[DrumOffset.Buttons + 1] & DrumButton.KickTwo) != 0); + // Velocities device.SetAxisValue( Xbox360Axis.LeftThumbX, diff --git a/PacketParsing/VjoyMapper.cs b/PacketParsing/VjoyMapper.cs index 846a617..11fa8c5 100644 --- a/PacketParsing/VjoyMapper.cs +++ b/PacketParsing/VjoyMapper.cs @@ -188,17 +188,17 @@ private void ParseDrums(ReadOnlySpan data) // Pads SetButton(Button.One, (data[DrumOffset.PadVels] & DrumPadVel.Red) != 0); SetButton(Button.Two, (data[DrumOffset.PadVels] & DrumPadVel.Yellow) != 0); - SetButton(Button.Three, (data[DrumOffset.PadVels] & DrumPadVel.Blue) != 0); - SetButton(Button.Four, (data[DrumOffset.PadVels] & DrumPadVel.Green) != 0); + SetButton(Button.Three, (data[DrumOffset.PadVels + 1] & DrumPadVel.Blue) != 0); + SetButton(Button.Four, (data[DrumOffset.PadVels + 1] & DrumPadVel.Green) != 0); // Cymbals SetButton(Button.Six, (data[DrumOffset.CymbalVels] & DrumCymVel.Yellow) != 0); SetButton(Button.Seven, (data[DrumOffset.CymbalVels] & DrumCymVel.Blue) != 0); - SetButton(Button.Eight, (data[DrumOffset.CymbalVels] & DrumCymVel.Green) != 0); + SetButton(Button.Eight, (data[DrumOffset.CymbalVels + 1] & DrumCymVel.Green) != 0); // Kick pedals - SetButton(Button.Five, (data[DrumOffset.Buttons] & DrumButton.KickOne) != 0); - SetButton(Button.Nine, (data[DrumOffset.Buttons] & DrumButton.KickTwo) != 0); + SetButton(Button.Five, (data[DrumOffset.Buttons + 1] & DrumButton.KickOne) != 0); + SetButton(Button.Nine, (data[DrumOffset.Buttons + 1] & DrumButton.KickTwo) != 0); } ///

From ba0ec557478c16fcc23757002e510f6ee0c4df49 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 10 Jul 2022 19:50:59 -0600 Subject: [PATCH 060/437] Increment version number to 2.0.1 --- Installer/Product.wxs | 2 +- Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 0a1f361..b5da3b5 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -4,7 +4,7 @@ Id="*" Name="RB4InstrumentMapper" Language="1033" - Version="2.0.0.0" + Version="2.0.1.0" Manufacturer="Andreas Schiffler" UpgradeCode="94bef546-701f-4571-9828-d4fa39b2ea84"> diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 911c44a..3d2d808 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] +[assembly: AssemblyVersion("2.0.1.0")] +[assembly: AssemblyFileVersion("2.0.1.0")] From ab7fd99651e9e315156b3e3233bf0987ffc441b8 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 17:01:36 -0600 Subject: [PATCH 061/437] Group program source files into a Program folder --- App.config => Program/App.config | 0 App.xaml => Program/App.xaml | 0 App.xaml.cs => Program/App.xaml.cs | 0 .../Dependencies}/x64/vJoyInterface.dll | Bin .../Dependencies}/x64/vJoyInterfaceWrap.dll | Bin .../Dependencies}/x86/vJoyInterface.dll | Bin .../Dependencies}/x86/vJoyInterfaceWrap.dll | Bin LogUtils.cs => Program/LogUtils.cs | 0 .../MainWindow}/FixedSizeConcurrentQueue.cs | 0 {MainWindow => Program/MainWindow}/MainWindow.xaml | 0 .../MainWindow}/MainWindow.xaml.cs | 0 .../MainWindow}/ParsingHelpers.cs | 0 .../MainWindow}/TextBoxConsole.cs | 0 .../PacketParsing}/IDeviceMapper.cs | 0 .../PacketParsing}/PacketDefinitions.cs | 0 .../PacketParsing}/PacketParser.cs | 0 .../PacketParsing}/ParseException.cs | 0 .../PacketParsing}/ParsingUtils.cs | 0 .../PacketParsing}/VigemMapper.cs | 0 .../PacketParsing}/VigemStatic.cs | 0 .../PacketParsing}/VjoyMapper.cs | 0 .../PacketParsing}/VjoyStatic.cs | 0 .../PacketParsing}/XboxDevice.cs | 0 {Properties => Program/Properties}/AssemblyInfo.cs | 0 .../Properties}/Resources.Designer.cs | 0 {Properties => Program/Properties}/Resources.resx | 0 .../Properties}/Settings.Designer.cs | 0 .../Properties}/Settings.settings | 0 .../RB4InstrumentMapper.csproj | 6 +++--- icon.ico => Program/icon.ico | Bin packages.config => Program/packages.config | 0 RB4InstrumentMapper.sln | 2 +- 32 files changed, 4 insertions(+), 4 deletions(-) rename App.config => Program/App.config (100%) rename App.xaml => Program/App.xaml (100%) rename App.xaml.cs => Program/App.xaml.cs (100%) rename {Dependencies => Program/Dependencies}/x64/vJoyInterface.dll (100%) rename {Dependencies => Program/Dependencies}/x64/vJoyInterfaceWrap.dll (100%) rename {Dependencies => Program/Dependencies}/x86/vJoyInterface.dll (100%) rename {Dependencies => Program/Dependencies}/x86/vJoyInterfaceWrap.dll (100%) rename LogUtils.cs => Program/LogUtils.cs (100%) rename {MainWindow => Program/MainWindow}/FixedSizeConcurrentQueue.cs (100%) rename {MainWindow => Program/MainWindow}/MainWindow.xaml (100%) rename {MainWindow => Program/MainWindow}/MainWindow.xaml.cs (100%) rename {MainWindow => Program/MainWindow}/ParsingHelpers.cs (100%) rename {MainWindow => Program/MainWindow}/TextBoxConsole.cs (100%) rename {PacketParsing => Program/PacketParsing}/IDeviceMapper.cs (100%) rename {PacketParsing => Program/PacketParsing}/PacketDefinitions.cs (100%) rename {PacketParsing => Program/PacketParsing}/PacketParser.cs (100%) rename {PacketParsing => Program/PacketParsing}/ParseException.cs (100%) rename {PacketParsing => Program/PacketParsing}/ParsingUtils.cs (100%) rename {PacketParsing => Program/PacketParsing}/VigemMapper.cs (100%) rename {PacketParsing => Program/PacketParsing}/VigemStatic.cs (100%) rename {PacketParsing => Program/PacketParsing}/VjoyMapper.cs (100%) rename {PacketParsing => Program/PacketParsing}/VjoyStatic.cs (100%) rename {PacketParsing => Program/PacketParsing}/XboxDevice.cs (100%) rename {Properties => Program/Properties}/AssemblyInfo.cs (100%) rename {Properties => Program/Properties}/Resources.Designer.cs (100%) rename {Properties => Program/Properties}/Resources.resx (100%) rename {Properties => Program/Properties}/Settings.Designer.cs (100%) rename {Properties => Program/Properties}/Settings.settings (100%) rename RB4InstrumentMapper.csproj => Program/RB4InstrumentMapper.csproj (97%) rename icon.ico => Program/icon.ico (100%) rename packages.config => Program/packages.config (100%) diff --git a/App.config b/Program/App.config similarity index 100% rename from App.config rename to Program/App.config diff --git a/App.xaml b/Program/App.xaml similarity index 100% rename from App.xaml rename to Program/App.xaml diff --git a/App.xaml.cs b/Program/App.xaml.cs similarity index 100% rename from App.xaml.cs rename to Program/App.xaml.cs diff --git a/Dependencies/x64/vJoyInterface.dll b/Program/Dependencies/x64/vJoyInterface.dll similarity index 100% rename from Dependencies/x64/vJoyInterface.dll rename to Program/Dependencies/x64/vJoyInterface.dll diff --git a/Dependencies/x64/vJoyInterfaceWrap.dll b/Program/Dependencies/x64/vJoyInterfaceWrap.dll similarity index 100% rename from Dependencies/x64/vJoyInterfaceWrap.dll rename to Program/Dependencies/x64/vJoyInterfaceWrap.dll diff --git a/Dependencies/x86/vJoyInterface.dll b/Program/Dependencies/x86/vJoyInterface.dll similarity index 100% rename from Dependencies/x86/vJoyInterface.dll rename to Program/Dependencies/x86/vJoyInterface.dll diff --git a/Dependencies/x86/vJoyInterfaceWrap.dll b/Program/Dependencies/x86/vJoyInterfaceWrap.dll similarity index 100% rename from Dependencies/x86/vJoyInterfaceWrap.dll rename to Program/Dependencies/x86/vJoyInterfaceWrap.dll diff --git a/LogUtils.cs b/Program/LogUtils.cs similarity index 100% rename from LogUtils.cs rename to Program/LogUtils.cs diff --git a/MainWindow/FixedSizeConcurrentQueue.cs b/Program/MainWindow/FixedSizeConcurrentQueue.cs similarity index 100% rename from MainWindow/FixedSizeConcurrentQueue.cs rename to Program/MainWindow/FixedSizeConcurrentQueue.cs diff --git a/MainWindow/MainWindow.xaml b/Program/MainWindow/MainWindow.xaml similarity index 100% rename from MainWindow/MainWindow.xaml rename to Program/MainWindow/MainWindow.xaml diff --git a/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs similarity index 100% rename from MainWindow/MainWindow.xaml.cs rename to Program/MainWindow/MainWindow.xaml.cs diff --git a/MainWindow/ParsingHelpers.cs b/Program/MainWindow/ParsingHelpers.cs similarity index 100% rename from MainWindow/ParsingHelpers.cs rename to Program/MainWindow/ParsingHelpers.cs diff --git a/MainWindow/TextBoxConsole.cs b/Program/MainWindow/TextBoxConsole.cs similarity index 100% rename from MainWindow/TextBoxConsole.cs rename to Program/MainWindow/TextBoxConsole.cs diff --git a/PacketParsing/IDeviceMapper.cs b/Program/PacketParsing/IDeviceMapper.cs similarity index 100% rename from PacketParsing/IDeviceMapper.cs rename to Program/PacketParsing/IDeviceMapper.cs diff --git a/PacketParsing/PacketDefinitions.cs b/Program/PacketParsing/PacketDefinitions.cs similarity index 100% rename from PacketParsing/PacketDefinitions.cs rename to Program/PacketParsing/PacketDefinitions.cs diff --git a/PacketParsing/PacketParser.cs b/Program/PacketParsing/PacketParser.cs similarity index 100% rename from PacketParsing/PacketParser.cs rename to Program/PacketParsing/PacketParser.cs diff --git a/PacketParsing/ParseException.cs b/Program/PacketParsing/ParseException.cs similarity index 100% rename from PacketParsing/ParseException.cs rename to Program/PacketParsing/ParseException.cs diff --git a/PacketParsing/ParsingUtils.cs b/Program/PacketParsing/ParsingUtils.cs similarity index 100% rename from PacketParsing/ParsingUtils.cs rename to Program/PacketParsing/ParsingUtils.cs diff --git a/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs similarity index 100% rename from PacketParsing/VigemMapper.cs rename to Program/PacketParsing/VigemMapper.cs diff --git a/PacketParsing/VigemStatic.cs b/Program/PacketParsing/VigemStatic.cs similarity index 100% rename from PacketParsing/VigemStatic.cs rename to Program/PacketParsing/VigemStatic.cs diff --git a/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs similarity index 100% rename from PacketParsing/VjoyMapper.cs rename to Program/PacketParsing/VjoyMapper.cs diff --git a/PacketParsing/VjoyStatic.cs b/Program/PacketParsing/VjoyStatic.cs similarity index 100% rename from PacketParsing/VjoyStatic.cs rename to Program/PacketParsing/VjoyStatic.cs diff --git a/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs similarity index 100% rename from PacketParsing/XboxDevice.cs rename to Program/PacketParsing/XboxDevice.cs diff --git a/Properties/AssemblyInfo.cs b/Program/Properties/AssemblyInfo.cs similarity index 100% rename from Properties/AssemblyInfo.cs rename to Program/Properties/AssemblyInfo.cs diff --git a/Properties/Resources.Designer.cs b/Program/Properties/Resources.Designer.cs similarity index 100% rename from Properties/Resources.Designer.cs rename to Program/Properties/Resources.Designer.cs diff --git a/Properties/Resources.resx b/Program/Properties/Resources.resx similarity index 100% rename from Properties/Resources.resx rename to Program/Properties/Resources.resx diff --git a/Properties/Settings.Designer.cs b/Program/Properties/Settings.Designer.cs similarity index 100% rename from Properties/Settings.Designer.cs rename to Program/Properties/Settings.Designer.cs diff --git a/Properties/Settings.settings b/Program/Properties/Settings.settings similarity index 100% rename from Properties/Settings.settings rename to Program/Properties/Settings.settings diff --git a/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj similarity index 97% rename from RB4InstrumentMapper.csproj rename to Program/RB4InstrumentMapper.csproj index 1a5fd56..4e51985 100644 --- a/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -163,8 +163,8 @@ - - + + @@ -174,6 +174,6 @@ - copy $(SolutionDir)Dependencies\x64\vJoyInterface.dll $(TargetDir). + copy $(ProjectDir)Dependencies\x64\vJoyInterface.dll $(TargetDir). \ No newline at end of file diff --git a/icon.ico b/Program/icon.ico similarity index 100% rename from icon.ico rename to Program/icon.ico diff --git a/packages.config b/Program/packages.config similarity index 100% rename from packages.config rename to Program/packages.config diff --git a/RB4InstrumentMapper.sln b/RB4InstrumentMapper.sln index bdccd7a..8fe25c1 100644 --- a/RB4InstrumentMapper.sln +++ b/RB4InstrumentMapper.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31729.503 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RB4InstrumentMapper", "RB4InstrumentMapper.csproj", "{93041197-1E1B-4084-B1DD-C8363A588C48}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RB4InstrumentMapper", "Program\RB4InstrumentMapper.csproj", "{93041197-1E1B-4084-B1DD-C8363A588C48}" EndProject Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "RB4InstrumentMapperInstaller", "Installer\RB4InstrumentMapperInstaller.wixproj", "{047562BB-6D63-4259-8E2E-0A5E834190B1}" ProjectSection(ProjectDependencies) = postProject From 19e2c37bc56f02220dd3595c3b97c8d1d03b06a4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 17:03:36 -0600 Subject: [PATCH 062/437] Fix ViGEmBus check resetting controller type incorrectly --- Program/MainWindow/MainWindow.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index f573dc0..ca87418 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -163,7 +163,7 @@ private void Window_Loaded(object sender, RoutedEventArgs e) Console.WriteLine("ViGEmBus not found. ViGEmBus selection will be unavailable."); // Reset device type selection if it was set to ViGEmBus - if (deviceType == (int)ControllerType.vJoy) + if (deviceType == (int)ControllerType.VigemBus) { controllerDeviceTypeCombo.SelectedIndex = (int)ControllerType.None; } From 6c08b018369502a516def659ef3528515bb37815 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 17:15:59 -0600 Subject: [PATCH 063/437] Add basic auto-detection for Xbox receivers when refreshing --- Program/MainWindow/MainWindow.xaml.cs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index ca87418..57ec8c1 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -223,6 +223,8 @@ private void PopulatePcapDropdown() // Populate combo and print the list StringBuilder sb = new StringBuilder(); + ILiveDevice xboxReceiver = null; + int xboxIndex = -1; for (int i = 0; i < pcapDeviceList.Count; i++) { ILiveDevice device = pcapDeviceList[i]; @@ -232,6 +234,15 @@ private void PopulatePcapDropdown() sb.Append($"{itemNumber}. "); if (device.Description != null) { + // Check if it's an Xbox receiver for later use + // TODO: Research if there are any other device names to check for, or other methods to detect receivers + // This won't work anymore if the receiver changes device name down the line + if (device.Description == "MT7612US_RL") + { + xboxReceiver = device; + xboxIndex = i; + } + sb.Append(device.Description); sb.Append($" ({device.Name})"); } @@ -258,10 +269,20 @@ private void PopulatePcapDropdown() }); } - // Set selection to nothing if saved device not detected + // Set selection to the detected receiver if saved device not detected if (pcapSelectedDevice == null) { - pcapDeviceCombo.SelectedIndex = -1; + if (xboxReceiver != null) + { + pcapSelectedDevice = xboxReceiver; + pcapDeviceCombo.SelectedIndex = xboxIndex; + } + else + { + pcapDeviceCombo.SelectedIndex = -1; + Console.WriteLine("No Xbox controller receivers detected during refresh! Please ensure you have one connected, and that you are using WinPcap and not Npcap."); + Console.WriteLine("You may need to run through auto-detection or manually select the device from the dropdown as well."); + } } Console.WriteLine($"Discovered {pcapDeviceList.Count} Pcap devices."); From d5c6ffe0cebffb48bbbde1ff4bb9f3b7472cd095 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 18:26:44 -0600 Subject: [PATCH 064/437] Remove Pcap.Net deps steps and link to this repository --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 7e9e1b1..7edcc89 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,6 @@ RB4 drums made by PDP don't seem to work, they turn off after syncing to the rec ![vJoy Configuration Screenshot](/Docs/Images/vJoyConfiguration.png "vJoy Configuration Screenshot") - If you installed ViGEmBus, there's no configuration required. Outputs for guitars and drums will match that of their Xbox 360 counterparts. 4. Restart your PC. -5. Download the latest release from the [Releases tab](https://github.com/ferzkopp/RB4InstrumentMapper/releases/latest) and install it. -6. Install Pcap.Net dependencies - - [Microsoft Visual C++ 2010 Service Pack 1 Redistributable Package MFC Security Update](https://www.microsoft.com/en-us/download/details.aspx?id=26999) - - [Visual C++ Redistributable Packages for Visual Studio 2013 ](https://www.microsoft.com/en-us/download/details.aspx?id=40784) ## Usage @@ -73,10 +69,6 @@ Note that these settings are meant for debugging purposes only, leaving them ena In the case that the program crashes, an error log is saved to a `RB4InstrumentMapper` > `Logs` folder inside your Documents folder. Make sure to include it when getting help or creating an issue report for the crash. -## Other Versions - -A fork of this code with an updated interface, 64bit version and other improvements is available at: https://github.com/TheNathannator/RB4InstrumentMapper - ## References - [GuitarSniffer repository](https://github.com/artman41/guitarsniffer) From 6809e73ee84a0fccc9758135e66ae7f35d6e04d8 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 18:27:37 -0600 Subject: [PATCH 065/437] Update readme to reflect auto-detection of receivers --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7edcc89..2ba8b97 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ RB4 drums made by PDP don't seem to work, they turn off after syncing to the rec ## Usage 1. Configure the selected Pcap device: - - Click the `Auto-Detect Pcap` button and follow its instructions. + - Xbox receivers should be detected automatically by device name. + - If they are not, click the `Auto-Detect Pcap` button and follow its instructions. - If that doesn't work, then if you installed WinPcap, try installing Npcap instead, or vice versa. 2. Select either vJoy or ViGEmBus in the Controller Type dropdown. 3. Connect your instruments if you haven't yet. From eef071c0db12412d5e589d24b6735321ea026864 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 18:34:13 -0600 Subject: [PATCH 066/437] Update program version to v2.1.0 --- Installer/Product.wxs | 2 +- Program/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index b5da3b5..aa2dcb9 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -4,7 +4,7 @@ Id="*" Name="RB4InstrumentMapper" Language="1033" - Version="2.0.1.0" + Version="2.1.0.0" Manufacturer="Andreas Schiffler" UpgradeCode="94bef546-701f-4571-9828-d4fa39b2ea84"> diff --git a/Program/Properties/AssemblyInfo.cs b/Program/Properties/AssemblyInfo.cs index 3d2d808..0d2d445 100644 --- a/Program/Properties/AssemblyInfo.cs +++ b/Program/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.1.0")] -[assembly: AssemblyFileVersion("2.0.1.0")] +[assembly: AssemblyVersion("2.1.0.0")] +[assembly: AssemblyFileVersion("2.1.0.0")] From 7dc6f019116dcbb8ac8863990d0c9cbb2c9f8905 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 18:45:16 -0600 Subject: [PATCH 067/437] I should probably list the device name to look for in the readme, huh? --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2ba8b97..1bf3ce1 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ RB4 drums made by PDP don't seem to work, they turn off after syncing to the rec 1. Configure the selected Pcap device: - Xbox receivers should be detected automatically by device name. - If they are not, click the `Auto-Detect Pcap` button and follow its instructions. + - You can also check the device list yourself for a device called `MT7612US_RL`. - If that doesn't work, then if you installed WinPcap, try installing Npcap instead, or vice versa. 2. Select either vJoy or ViGEmBus in the Controller Type dropdown. 3. Connect your instruments if you haven't yet. From 1db3d4b793508d46c0f10fc5d393ef8a7dd2c064 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 20:12:22 -0600 Subject: [PATCH 068/437] Add drums face button mappings --- Program/PacketParsing/VigemMapper.cs | 8 +++++++- Program/PacketParsing/VjoyMapper.cs | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 00353ec..d013dcb 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -155,7 +155,13 @@ private void ParseGuitar(ReadOnlySpan data) private void ParseDrums(ReadOnlySpan data) { // Buttons - ParseCoreButtons(data.GetUInt16BE(DrumOffset.Buttons)); + ushort buttons = data.GetUInt16BE(DrumOffset.Buttons); + ParseCoreButtons(buttons); + + device.SetButtonState(Xbox360Button.A, (buttons & GamepadButton.A) != 0); + device.SetButtonState(Xbox360Button.B, (buttons & GamepadButton.B) != 0); + device.SetButtonState(Xbox360Button.X, (buttons & GamepadButton.X) != 0); + device.SetButtonState(Xbox360Button.Y, (buttons & GamepadButton.Y) != 0); // Pads and cymbals byte redPad = (byte)(data[DrumOffset.PadVels] >> 4); diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs index 11fa8c5..b5ce5c5 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/VjoyMapper.cs @@ -183,7 +183,13 @@ private void ParseGuitar(ReadOnlySpan data) private void ParseDrums(ReadOnlySpan data) { // Buttons - ParseCoreButtons(data.GetUInt16BE(DrumOffset.Buttons)); + ushort buttons = data.GetUInt16BE(DrumOffset.Buttons); + ParseCoreButtons(buttons); + + SetButton(Button.Four, (buttons & GamepadButton.A) != 0); + SetButton(Button.One, (buttons & GamepadButton.B) != 0); + SetButton(Button.Three, (buttons & GamepadButton.X) != 0); + SetButton(Button.Two, (buttons & GamepadButton.Y) != 0); // Pads SetButton(Button.One, (data[DrumOffset.PadVels] & DrumPadVel.Red) != 0); From afb02f4f510ef6146dc6828e351861390bd5a036 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 20:31:57 -0600 Subject: [PATCH 069/437] Revert "Update program version to v2.1.0" This reverts commit eef071c0db12412d5e589d24b6735321ea026864. --- Installer/Product.wxs | 2 +- Program/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index aa2dcb9..b5da3b5 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -4,7 +4,7 @@ Id="*" Name="RB4InstrumentMapper" Language="1033" - Version="2.1.0.0" + Version="2.0.1.0" Manufacturer="Andreas Schiffler" UpgradeCode="94bef546-701f-4571-9828-d4fa39b2ea84"> diff --git a/Program/Properties/AssemblyInfo.cs b/Program/Properties/AssemblyInfo.cs index 0d2d445..3d2d808 100644 --- a/Program/Properties/AssemblyInfo.cs +++ b/Program/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.1.0.0")] -[assembly: AssemblyFileVersion("2.1.0.0")] +[assembly: AssemblyVersion("2.0.1.0")] +[assembly: AssemblyFileVersion("2.0.1.0")] From a001fd8e32e2fbd95dab4f6f1dd39c503aa596b9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 21:01:13 -0600 Subject: [PATCH 070/437] Increment program version to 2.0.2 --- Installer/Product.wxs | 2 +- Program/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index b5da3b5..6be6c1d 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -4,7 +4,7 @@ Id="*" Name="RB4InstrumentMapper" Language="1033" - Version="2.0.1.0" + Version="2.0.2.0" Manufacturer="Andreas Schiffler" UpgradeCode="94bef546-701f-4571-9828-d4fa39b2ea84"> diff --git a/Program/Properties/AssemblyInfo.cs b/Program/Properties/AssemblyInfo.cs index 3d2d808..e986c76 100644 --- a/Program/Properties/AssemblyInfo.cs +++ b/Program/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.1.0")] -[assembly: AssemblyFileVersion("2.0.1.0")] +[assembly: AssemblyVersion("2.0.2.0")] +[assembly: AssemblyFileVersion("2.0.2.0")] From 55782762f6d943ad18fe1d872387dc457c3e38b5 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 22:00:16 -0600 Subject: [PATCH 071/437] Update installer project to get program files from Program folder --- Installer/Product.wxs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 6be6c1d..4a95c6f 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -44,7 +44,7 @@ - + From 5c4383be047126173214d10a534c3c8840235890 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 23:07:09 -0600 Subject: [PATCH 072/437] Clean up refresh auto-detection for receivers --- Program/MainWindow/MainWindow.xaml.cs | 48 +++++++++++++-------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 57ec8c1..3f4c4d8 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -198,6 +198,24 @@ private void Window_Closed(object sender, EventArgs e) Logging.CloseAll(); } + /// + /// Determines whether or not a device is an Xbox One receiver. + /// + private bool IsXboxOneReceiver(ILiveDevice device) + { + if (device.Description != null) + { + // TODO: Research if there are any other device names to check for, or other methods to detect receivers + // This won't work anymore if the receiver changes device name down the line + if (device.Description == "MT7612US_RL") + { + return true; + } + } + + return false; + } + /// /// Populates the Pcap device combo. /// @@ -223,8 +241,6 @@ private void PopulatePcapDropdown() // Populate combo and print the list StringBuilder sb = new StringBuilder(); - ILiveDevice xboxReceiver = null; - int xboxIndex = -1; for (int i = 0; i < pcapDeviceList.Count; i++) { ILiveDevice device = pcapDeviceList[i]; @@ -234,15 +250,6 @@ private void PopulatePcapDropdown() sb.Append($"{itemNumber}. "); if (device.Description != null) { - // Check if it's an Xbox receiver for later use - // TODO: Research if there are any other device names to check for, or other methods to detect receivers - // This won't work anymore if the receiver changes device name down the line - if (device.Description == "MT7612US_RL") - { - xboxReceiver = device; - xboxIndex = i; - } - sb.Append(device.Description); sb.Append($" ({device.Name})"); } @@ -255,7 +262,7 @@ private void PopulatePcapDropdown() string itemName = pcapComboBoxItemName + itemNumber; bool isSelected = device.Name.Equals(currentPcapSelection) || device.Name.Equals(pcapSelectedDevice?.Name); - if (isSelected) + if (isSelected || (string.IsNullOrEmpty(currentPcapSelection) && IsXboxOneReceiver(device))) { pcapSelectedDevice = device; } @@ -269,20 +276,11 @@ private void PopulatePcapDropdown() }); } - // Set selection to the detected receiver if saved device not detected if (pcapSelectedDevice == null) { - if (xboxReceiver != null) - { - pcapSelectedDevice = xboxReceiver; - pcapDeviceCombo.SelectedIndex = xboxIndex; - } - else - { - pcapDeviceCombo.SelectedIndex = -1; - Console.WriteLine("No Xbox controller receivers detected during refresh! Please ensure you have one connected, and that you are using WinPcap and not Npcap."); - Console.WriteLine("You may need to run through auto-detection or manually select the device from the dropdown as well."); - } + pcapDeviceCombo.SelectedIndex = -1; + Console.WriteLine("No Xbox controller receivers detected during refresh! Please ensure you have one connected, and that you are using WinPcap and not Npcap."); + Console.WriteLine("You may need to run through auto-detection or manually select the device from the dropdown as well."); } Console.WriteLine($"Discovered {pcapDeviceList.Count} Pcap devices."); From aaf7da5a6bed3f89466dfa2d1c2d293f500476e8 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 23:07:41 -0600 Subject: [PATCH 073/437] Add new auto-detect method to Auto-Detect button handling --- Program/MainWindow/MainWindow.xaml.cs | 45 ++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 3f4c4d8..124adfd 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -715,6 +715,49 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) { MessageBoxResult result; + // Get the list of devices for when receiver is unplugged + CaptureDeviceList deviceList = CaptureDeviceList.Instance; + foreach (var device in deviceList) + { + if (!IsXboxOneReceiver(device)) + { + continue; + } + + result = MessageBox.Show( + $"Found Xbox One receiver device: {device.Description}\nPress OK to set this device as your selected Pcap device, or press Cancel to continue with the auto-detection process.", + "Auto-Detect Receiver", + MessageBoxButton.OKCancel + ); + if (result == MessageBoxResult.OK) + { + // Assign the new device + pcapSelectedDevice = device; + + // Remember the new device + Properties.Settings.Default.pcapDevice = pcapSelectedDevice.Name; + Properties.Settings.Default.Save(); + + // Refresh the dropdown + PopulatePcapDropdown(); + return; + } + else + { + continue; + } + } + + result = MessageBox.Show( + "No Xbox One receivers could be found through checking device properties.\nYou will now be guided through a second auto-detection process. Press Cancel at any time to cancel the process.", + "Auto-Detect Receiver", + MessageBoxButton.OKCancel + ); + if (result == MessageBoxResult.Cancel) + { + return; + } + // Prompt user to unplug their receiver result = MessageBox.Show( "Unplug your receiver, then click OK.\n(A 1-second delay will be taken to ensure that it registers as disconnected.)", From 3215063e69e9834e8ef351669d2328916f1d8f1c Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 6 Aug 2022 23:13:44 -0600 Subject: [PATCH 074/437] Increase console font size to 12 --- Program/MainWindow/MainWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Program/MainWindow/MainWindow.xaml b/Program/MainWindow/MainWindow.xaml index 1526790..8ac3dc1 100644 --- a/Program/MainWindow/MainWindow.xaml +++ b/Program/MainWindow/MainWindow.xaml @@ -29,7 +29,7 @@ void ParseInput(ReadOnlySpan data, byte length, byte sequenceCount); - /// - /// Parses a virtual keycode packet. - /// - void ParseVirtualKey(ReadOnlySpan data, byte length, byte sequenceCount); - /// /// Performs cleanup for the mapper. /// diff --git a/Program/PacketParsing/PacketDefinitions.cs b/Program/PacketParsing/PacketDefinitions.cs index 0503376..ef6c8d8 100644 --- a/Program/PacketParsing/PacketDefinitions.cs +++ b/Program/PacketParsing/PacketDefinitions.cs @@ -8,7 +8,6 @@ static class Length public const int ReceiverHeader = 26, CommandHeader = 4, - VirtualKey = 2, Input_Gamepad = 0x0C, Input_Guitar = 0x0A, Input_Drums = 0x06; @@ -41,28 +40,9 @@ public const int static class CommandId { public const int - VirtualKey = 0x07, Input = 0x20; } - /// - /// Virtual keycodes that will be recognized. - /// - static class Keycodes - { - public const byte LeftWin = 0x5b; - } - - /// - /// Virtual keycode packet offsets relative to the end of the command header. - /// - static class KeycodeOffset - { - public const int - PressedState = 0, - Keycode = 1; - } - /// /// Flag definitions for the buttons bytes. /// diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index d013dcb..04d17bf 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -239,29 +239,6 @@ short ByteToVelocityNegative(byte value) } } - /// - /// Parses a virtual key report. - /// - public void ParseVirtualKey(ReadOnlySpan data, byte length, byte sequenceCount) - { - // Don't parse the same report twice - if (sequenceCount == prevVirtualKeySeqCount) - { - return; - } - else - { - prevVirtualKeySeqCount = sequenceCount; - } - - // Only respond to the Left Windows keycode, as this is what the guide button reports. - if (data[KeycodeOffset.Keycode] == Keycodes.LeftWin) - { - device.SetButtonState(Xbox360Button.Guide, data[KeycodeOffset.PressedState] != 0); - device.SubmitReport(); - } - } - /// /// Performs cleanup for the object. /// diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs index b5ce5c5..83b97fa 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/VjoyMapper.cs @@ -207,29 +207,6 @@ private void ParseDrums(ReadOnlySpan data) SetButton(Button.Nine, (data[DrumOffset.Buttons + 1] & DrumButton.KickTwo) != 0); } - /// - /// Parses a virtual key report. - /// - public void ParseVirtualKey(ReadOnlySpan data, byte length, byte sequenceCount) - { - // Don't parse the same report twice - if (sequenceCount == prevVirtualKeySeqCount) - { - return; - } - else - { - prevVirtualKeySeqCount = sequenceCount; - } - - // Only respond to the Left Windows keycode, as this is what the guide button reports. - if (data[KeycodeOffset.Keycode] == Keycodes.LeftWin) - { - SetButton(Button.Fourteen, data[KeycodeOffset.PressedState] != 0); - VjoyStatic.Client.UpdateVJD(deviceId, ref state); - } - } - /// /// Performs cleanup for the vJoy mapper. /// diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index a94ac61..dd8d67a 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -51,12 +51,6 @@ public void ParseCommand(ReadOnlySpan commandData) deviceMapper.ParseInput(commandData.Slice(Length.CommandHeader), commandData[CommandOffset.DataLength], commandData[CommandOffset.SequenceCount]); break; - // Probably don't actually want to parse the guide button and output it to the device, - // so as to not interfere with Windows processes that use it - // case CommandId.VirtualKey: - // deviceMapper.ParseVirtualKey(commandData.Slice(Length.CommandHeader), commandData[CommandOffset.DataLength], commandData[CommandOffset.SequenceCount]); - // break; - default: // Don't do anything with unrecognized command IDs break; From 2b51b700615f1528071f67ebc5911c04e94da619 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 31 Oct 2022 14:18:18 -0600 Subject: [PATCH 077/437] Missed some things for updating the packages --- Program/App.config | 2 +- Program/RB4InstrumentMapper.csproj | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Program/App.config b/Program/App.config index 21eff07..346a6f4 100644 --- a/Program/App.config +++ b/Program/App.config @@ -36,7 +36,7 @@ - + diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 4e51985..918968c 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -1,4 +1,4 @@ - + @@ -56,20 +56,20 @@ true - - packages\Nefarius.ViGEm.Client.1.17.178\lib\net452\Nefarius.ViGEm.Client.dll + + ..\packages\Nefarius.ViGEm.Client.1.19.199\lib\netstandard2.0\Nefarius.ViGEm.Client.dll - - packages\PacketDotNet.1.4.0\lib\net47\PacketDotNet.dll + + ..\packages\PacketDotNet.1.4.6\lib\net47\PacketDotNet.dll - - packages\SharpPcap.6.1.0\lib\netstandard2.0\SharpPcap.dll + + ..\packages\SharpPcap.6.2.2\lib\netstandard2.0\SharpPcap.dll packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + + ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll From 1f879861acc57cddf83157a987873d20ca846153 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 31 Oct 2022 14:18:36 -0600 Subject: [PATCH 078/437] Enable unafe code --- Program/RB4InstrumentMapper.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 918968c..e96473d 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -1,4 +1,4 @@ - + @@ -15,6 +15,7 @@ 4 true true + true AnyCPU From 09b3b1dbcf6dd16d981308a7f5d5b20a9f619fb9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 31 Oct 2022 15:13:04 -0600 Subject: [PATCH 079/437] Update packet docs to list everything in little-endian --- Docs/PacketFormats.md | 120 +++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/Docs/PacketFormats.md b/Docs/PacketFormats.md index 49d3f37..7557889 100644 --- a/Docs/PacketFormats.md +++ b/Docs/PacketFormats.md @@ -4,7 +4,7 @@ This document provides some details on how Xbox One device data packets are rece This documentation is far from fully comprehensive, as there are many parts of the Xbox One controller protocol that don't pertain to sniffing inputs. There are also some parts of the receiver header data that are not well understood. -Byte numbers in the lists are 0-indexed. +All values are listed as little-endian. Byte numbers are 0-indexed. ## Table of Contents @@ -94,23 +94,23 @@ The standard Xbox One controller layout is as follows: - 12 bytes long - ` ` - - `buttons`: 16-bit button bitmask. Note that while other values are little-endian, these are listed in big-endian format. - - Bit 0 (`0x0001`) - D-pad Up - - Bit 1 (`0x0002`) - D-pad Down - - Bit 2 (`0x0004`) - D-pad Left - - Bit 3 (`0x0008`) - D-pad Right - - Bit 4 (`0x0010`) - Left Bumper - - Bit 5 (`0x0020`) - Right Bumper - - Bit 6 (`0x0040`) - Left Stick Press - - Bit 7 (`0x0080`) - Right Stick Press - - Bit 8 (`0x0100`) - Sync button - - Bit 9 (`0x0200`) - Unused (undefined?) - - Bit 10 (`0x0400`) - Menu Button - - Bit 11 (`0x0800`) - Options Button - - Bit 12 (`0x1000`) - A Button - - Bit 13 (`0x2000`) - B Button - - Bit 14 (`0x4000`) - X Button - - Bit 15 (`0x8000`) - Y Button + - `buttons`: 16-bit button bitmask + - Bit 0 (`0x0001`) - Sync button + - Bit 1 (`0x0002`) - Unused + - Bit 2 (`0x0004`) - Menu Button + - Bit 3 (`0x0008`) - Options Button + - Bit 4 (`0x0010`) - A Button + - Bit 5 (`0x0020`) - B Button + - Bit 6 (`0x0040`) - X Button + - Bit 7 (`0x0080`) - Y Button + - Bit 8 (`0x0100`) - D-pad Up + - Bit 9 (`0x0200`) - D-pad Down + - Bit 10 (`0x0400`) - D-pad Left + - Bit 11 (`0x0800`) - D-pad Right + - Bit 12 (`0x1000`) - Left Bumper + - Bit 13 (`0x2000`) - Right Bumper + - Bit 14 (`0x4000`) - Left Stick Press + - Bit 15 (`0x8000`) - Right Stick Press - `left trigger` and `right trigger`: 16-bit little-endian unsigned axis - `left stick X`/`Y`, `right stick X`/`Y`: 16-bit little-endian signed axis @@ -121,22 +121,22 @@ The standard Xbox One controller layout is as follows: ` ` - `buttons`: 16-bit button bitmask - - Bit 0 (`0x0001`) - D-pad Up/Strum Up - - Bit 1 (`0x0002`) - D-pad Down/Strum Down - - Bit 2 (`0x0004`) - D-pad Left - - Bit 3 (`0x0008`) - D-pad Right - - Bit 4 (`0x0010`) - Orange Fret Flag (equivalent to Left Bumper) - - Bit 5 (`0x0020`) - Unused (equivalent to Right Bumper) - - Bit 6 (`0x0040`) - Lower Fret Flag (equivalent to Left Stick Press) - - Bit 7 (`0x0080`) - Unused (equivalent to Right Stick Press) - - Bit 8 (`0x0100`) - Sync button? - - Bit 9 (`0x0200`) - Unused (undefined?) - - Bit 10 (`0x0400`) - Menu Button - - Bit 11 (`0x0800`) - Options Button - - Bit 12 (`0x1000`) - Green Fret Flag (equivalent to A Button) - - Bit 13 (`0x2000`) - Red Fret Flag (equivalent to B Button) - - Bit 14 (`0x4000`) - Blue Fret Flag (equivalent to X Button) - - Bit 15 (`0x8000`) - Yellow Fret Flag (equivalent to Y Button) + - Bit 0 (`0x0001`) - Sync button + - Bit 1 (`0x0002`) - Unused + - Bit 2 (`0x0004`) - Menu Button + - Bit 3 (`0x0008`) - Options Button + - Bit 4 (`0x0010`) - Green Fret Flag (equivalent to A Button) + - Bit 5 (`0x0020`) - Red Fret Flag (equivalent to B Button) + - Bit 6 (`0x0040`) - Blue Fret Flag (equivalent to X Button) + - Bit 7 (`0x0080`) - Yellow Fret Flag (equivalent to Y Button) + - Bit 8 (`0x0100`) - D-pad Up/Strum Up + - Bit 9 (`0x0200`) - D-pad Down/Strum Down + - Bit 10 (`0x0400`) - D-pad Left + - Bit 11 (`0x0800`) - D-pad Right + - Bit 12 (`0x1000`) - Orange Fret Flag (equivalent to Left Bumper) + - Bit 13 (`0x2000`) - Unused (equivalent to Right Bumper) + - Bit 14 (`0x4000`) - Lower Fret Flag (equivalent to Left Stick Press) + - Bit 15 (`0x8000`) - Unused (equivalent to Right Stick Press) - `tilt`: 8-bit tilt axis - Has a threshold of `0x70`? (values below get cut off to `0x00`) - `whammy`: 8-bit whammy bar axis @@ -185,33 +185,33 @@ Some of the data here is speculatory. It needs to be verified using packet captu Bytes: - `buttons`: 16-bit button bitmask - - Bit 0 (`0x0001`) - D-pad Up - - Bit 1 (`0x0002`) - D-pad Down - - Bit 2 (`0x0004`) - D-pad Left - - Bit 3 (`0x0008`) - D-pad Right - - Bit 4 (`0x0010`) - 1st Kick Pedal (equivalent to Left Bumper) - - Bit 5 (`0x0020`) - 2nd Kick Pedal (equivalent to Right Bumper) - - Bit 6 (`0x0040`) - Unused (equivalent to Left Stick Press) - - Bit 7 (`0x0080`) - Unused (equivalent to Right Stick Press) - - Bit 8 (`0x0100`) - Sync button? - - Bit 9 (`0x0200`) - Unused (undefined?) - - Bit 10 (`0x0400`) - Menu Button - - Bit 11 (`0x0800`) - Options Button - - Bit 12 (`0x1000`) - Green Pad (equivalent to A Button) - - Bit 13 (`0x2000`) - Red Pad (equivalent to B Button) - - Bit 14 (`0x4000`) - Unused (equivalent to X Button) - - Bit 15 (`0x8000`) - Unused (equivalent to Y Button) -- Bytes 32-33 - Pad velocities - - Bits 0-3 (`0x000F`) - Green Pad - - Bits 4-7 (`0x00F0`) - Blue Pad - - Bits 8-11 (`0x0F00`) - Yellow Pad - - Bits 12-15 (`0xF000`) - Red Pad + - Bit 0 (`0x0001`) - Sync button + - Bit 1 (`0x0002`) - Unused + - Bit 2 (`0x0004`) - Menu Button + - Bit 3 (`0x0008`) - Options Button + - Bit 4 (`0x0010`) - Green Pad (equivalent to A Button) + - Bit 5 (`0x0020`) - Red Pad (equivalent to B Button) + - Bit 6 (`0x0040`) - Unused (equivalent to X Button) + - Bit 7 (`0x0080`) - Unused (equivalent to Y Button) + - Bit 8 (`0x0100`) - D-pad Up + - Bit 9 (`0x0200`) - D-pad Down + - Bit 10 (`0x0400`) - D-pad Left + - Bit 11 (`0x0800`) - D-pad Right + - Bit 12 (`0x1000`) - 1st Kick Pedal (equivalent to Left Bumper) + - Bit 13 (`0x2000`) - 2nd Kick Pedal (equivalent to Right Bumper) + - Bit 14 (`0x4000`) - Unused (equivalent to Left Stick Press) + - Bit 15 (`0x8000`) - Unused (equivalent to Right Stick Press) +- `pad velocities` - 16 bits for the pad velocities (remember that this is little-endian) + - Bits 0-3 (`0x000F`) - Yellow Pad + - Bits 4-7 (`0x00F0`) - Red Pad + - Bits 8-11 (`0x0F00`) - Green Pad + - Bits 12-15 (`0xF000`) - Blue Pad - Seem to range from 0-7 -- Bytes 34-35 - Cymbal velocities - - Bits 0-3 (`0x000F`) - Unused - - Bits 4-7 (`0x00F0`) - Green Cymbal - - Bits 8-11 (`0x0F00`) - Blue Cymbal - - Bits 12-15 (`0xF000`) - Yellow Cymbal +- `cymbal velocities` - 16 bits for the cymbal velocities (remember that this is little-endian) + - Bits 0-3 (`0x000F`) - Blue Cymbal + - Bits 4-7 (`0x00F0`) - Yellow Cymbal + - Bits 8-11 (`0x0F00`) - Unused + - Bits 12-15 (`0xF000`) - Green Cymbal - Seem to range from 0-7 ### Drum Packet Samples From 71728c1cc63d0d89ba7839cae8d080f63a055d24 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 1 Nov 2022 22:23:31 -0600 Subject: [PATCH 080/437] Add note about more proper method for getting Xbox One controller data --- Docs/PacketFormats.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Docs/PacketFormats.md b/Docs/PacketFormats.md index 7557889..645b56d 100644 --- a/Docs/PacketFormats.md +++ b/Docs/PacketFormats.md @@ -4,6 +4,8 @@ This document provides some details on how Xbox One device data packets are rece This documentation is far from fully comprehensive, as there are many parts of the Xbox One controller protocol that don't pertain to sniffing inputs. There are also some parts of the receiver header data that are not well understood. +A note: There is a more direct/built-in way to get Xbox One controller data, [detailed here](https://gist.github.com/TheNathannator/bcebc77e653f71e77634144940871596), however this method only works while the application has focus, so packet sniffing is still required for remapper programs. + All values are listed as little-endian. Byte numbers are 0-indexed. ## Table of Contents From 162a0fa2323619749910863b1c3fd2477cf2e5ce Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 2 Nov 2022 00:10:40 -0600 Subject: [PATCH 081/437] Missed the sequence count for virtual key reports --- Program/PacketParsing/VigemMapper.cs | 1 - Program/PacketParsing/VjoyMapper.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 04d17bf..83e6f41 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -18,7 +18,6 @@ class VigemMapper : IDeviceMapper private bool deviceConnected = false; private int prevInputSeqCount = -1; - private int prevVirtualKeySeqCount = -1; /// /// Creates a new VigemMapper. diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs index 83b97fa..64d40e8 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/VjoyMapper.cs @@ -12,7 +12,6 @@ class VjoyMapper : IDeviceMapper private uint deviceId = 0; private int prevInputSeqCount = -1; - private int prevVirtualKeySeqCount = -1; /// /// Creates a new VjoyMapper. From d843575ac62c4554dd030ad38dcc824990667fbb Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 2 Nov 2022 00:12:32 -0600 Subject: [PATCH 082/437] Change previous sequence counts to bytes --- Program/PacketParsing/VigemMapper.cs | 2 +- Program/PacketParsing/VjoyMapper.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 83e6f41..f85449d 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -17,7 +17,7 @@ class VigemMapper : IDeviceMapper /// private bool deviceConnected = false; - private int prevInputSeqCount = -1; + private byte prevInputSeqCount = 0xFF; /// /// Creates a new VigemMapper. diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs index 64d40e8..b317351 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/VjoyMapper.cs @@ -11,7 +11,7 @@ class VjoyMapper : IDeviceMapper private vJoy.JoystickState state = new vJoy.JoystickState(); private uint deviceId = 0; - private int prevInputSeqCount = -1; + private byte prevInputSeqCount = 0xFF; /// /// Creates a new VjoyMapper. From 1c53ff92e691788d4b0d00b0d2f6f90efd16e4b6 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 2 Nov 2022 02:21:18 -0600 Subject: [PATCH 083/437] Rewrite packet parsing to use structs instead of raw byte buffers --- Program/PacketParsing/IDeviceMapper.cs | 2 +- Program/PacketParsing/PacketDefinitions.cs | 305 +++++++++++---------- Program/PacketParsing/PacketParser.cs | 31 +-- Program/PacketParsing/ParsingUtils.cs | 32 --- Program/PacketParsing/VigemMapper.cs | 94 ++++--- Program/PacketParsing/VjoyMapper.cs | 92 ++++--- Program/PacketParsing/XboxDevice.cs | 14 +- 7 files changed, 285 insertions(+), 285 deletions(-) diff --git a/Program/PacketParsing/IDeviceMapper.cs b/Program/PacketParsing/IDeviceMapper.cs index 803bd70..13ad670 100644 --- a/Program/PacketParsing/IDeviceMapper.cs +++ b/Program/PacketParsing/IDeviceMapper.cs @@ -10,7 +10,7 @@ interface IDeviceMapper /// /// Parses an input packet. /// - void ParseInput(ReadOnlySpan data, byte length, byte sequenceCount); + void ParseInput(CommandHeader header, ReadOnlySpan data); /// /// Performs cleanup for the mapper. diff --git a/Program/PacketParsing/PacketDefinitions.cs b/Program/PacketParsing/PacketDefinitions.cs index ef6c8d8..24dfedf 100644 --- a/Program/PacketParsing/PacketDefinitions.cs +++ b/Program/PacketParsing/PacketDefinitions.cs @@ -1,173 +1,202 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + namespace RB4InstrumentMapper.Parsing { /// - /// Definitions for the receiver header. + /// Header data from the receiver. /// - static class Length + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct ReceiverHeader { - public const int - ReceiverHeader = 26, - CommandHeader = 4, - Input_Gamepad = 0x0C, - Input_Guitar = 0x0A, - Input_Drums = 0x06; - } + byte unk0; + byte unk1; + ushort unk2; + uint receiverId1_1; + ushort receiverId1_2; + uint deviceId_1; + ushort deviceId_2; + uint receiverId2_1; + ushort receiverId2_2; + ushort sequence; + byte unk3; + byte unk4; - /// - /// Definitions for the receiver header. - /// - static class HeaderOffset - { - public const int - DeviceId = 10; + public unsafe ulong DeviceId + { + get + { + fixed (uint* ptr = &deviceId_1) + { + // Read a ulong starting from deviceId_1 + ulong deviceId = *(ulong*)ptr; + // Last 2 bytes aren't part of the device ID + deviceId &= 0x0000FFFF_FFFFFFFF; + return deviceId; + } + } + } } /// - /// Command header offsets relative to the end of the receiver header. + /// Header data for a message. /// - static class CommandOffset + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct CommandHeader { - public const int - CommandId = 0, - Flags = 1, - SequenceCount = 2, - DataLength = 3; - } + /// + /// Command ID definitions. + /// + public enum Command : byte + { + Input = 0x20 + } - /// - /// Command IDs to be parsed. - /// - static class CommandId - { - public const int - Input = 0x20; + public byte CommandId; + public byte Flags; + public byte SequenceCount; + public byte DataLength; } /// /// Flag definitions for the buttons bytes. /// - static class GamepadButton + [Flags] + enum GamepadButton : ushort { - public const ushort - DpadUp = 0x01, - DpadDown = 0x02, - DpadLeft = 0x04, - DpadRight = 0x08, - LeftBumper = 0x10, - RightBumper = 0x20, - LeftStickPress = 0x40, - RightStickPress = 0x80, - - // Nothing useful can be done with this - // Sync = 0x0100, - // No known use for this bit - // Unused = 0x0200, - - Menu = 0x0400, - Options = 0x0800, - A = 0x1000, - B = 0x2000, - X = 0x4000, - Y = 0x8000; - } - - static class GamepadOffset - { - public const int - Buttons = 0, - LeftTrigger = 2, - RightTrigger = 4, - LeftStickX = 6, - LeftStickY = 8, - RightStickX = 10, - RightStickY = 12; + Sync = 0x0001, + Unused = 0x0002, + Menu = 0x0004, + Options = 0x0008, + A = 0x0010, + B = 0x0020, + X = 0x0040, + Y = 0x0080, + DpadUp = 0x0100, + DpadDown = 0x0200, + DpadLeft = 0x0400, + DpadRight = 0x0800, + LeftBumper = 0x1000, + RightBumper = 0x2000, + LeftStickPress = 0x4000, + RightStickPress = 0x8000 } /// - /// Guitar input data offsets relative to the end of the command header. + /// An input report from a guitar. /// - static class GuitarOffset + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct GuitarInput { - public const int - Buttons = 0, - Tilt = 2, - WhammyBar = 3, - PickupSwitch = 4, - UpperFrets = 5, - LowerFrets = 6; - - // Final 3 bytes are uknown - } + /// + /// Re-definitions for button flags that have specific meanings. + /// + [Flags] + public enum Button : ushort + { + StrumUp = GamepadButton.DpadUp, + StrumDown = GamepadButton.DpadDown, + GreenFret = GamepadButton.A, + RedFret = GamepadButton.B, + YellowFret = GamepadButton.Y, + BlueFret = GamepadButton.X, + OrangeFret = GamepadButton.LeftBumper, + LowerFretFlag = GamepadButton.LeftStickPress + } - static class GuitarButton - { - public const ushort - StrumUp = GamepadButton.DpadUp, - StrumDown = GamepadButton.DpadDown, - GreenFret = GamepadButton.A, - RedFret = GamepadButton.B, - YellowFret = GamepadButton.Y, - BlueFret = GamepadButton.X, - OrangeFret = GamepadButton.LeftBumper, - LowerFretFlag = GamepadButton.LeftStickPress; - } + /// + /// Flags used in and + /// + [Flags] + public enum Fret : byte + { + Green = 0x01, + Red = 0x02, + Yellow = 0x04, + Blue = 0x08, + Orange = 0x10 + } - /// - /// Flag definitions for the guitar fret bytes. - /// - static class GuitarFret - { - public const byte - Green = 0x01, - Red = 0x02, - Yellow = 0x04, - Blue = 0x08, - Orange = 0x10, - All = 0x1F; - } + public ushort Buttons; + public byte Tilt; + public byte WhammyBar; + public byte PickupSwitch; + public byte UpperFrets; + public byte LowerFrets; + byte unk1; + byte unk2; + byte unk3; - /// - /// Drums input data offsets relative to the end of the command header. - /// - static class DrumOffset - { - public const int - Buttons = 0, - PadVels = 2, - CymbalVels = 4; - } + public bool Green => ((UpperFrets | LowerFrets) & (byte)Fret.Green) != 0; + public bool Red => ((UpperFrets | LowerFrets) & (byte)Fret.Red) != 0; + public bool Yellow => ((UpperFrets | LowerFrets) & (byte)Fret.Yellow) != 0; + public bool Blue => ((UpperFrets | LowerFrets) & (byte)Fret.Blue) != 0; + public bool Orange => ((UpperFrets | LowerFrets) & (byte)Fret.Orange) != 0; - static class DrumButton - { - public const ushort - RedPad = GamepadButton.B, - GreenPad = GamepadButton.A, - KickOne = GamepadButton.LeftBumper, - KickTwo = GamepadButton.RightBumper; + public bool LowerFretFlag => (Buttons & (ushort)Button.LowerFretFlag) != 0; } /// - /// Definitions for drumkit pad velocity data. + /// An input report from a drumkit. /// - static class DrumPadVel + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct DrumInput { - public const byte - Red = 0xF0, - Yellow = 0x0F, - Blue = 0xF0, - Green = 0x0F; - } + /// + /// Re-definitions for button flags that have specific meanings. + /// + [Flags] + public enum Button : ushort + { + // Not used as these are for menu navigation purposes + // RedPad = GamepadButton.B, + // GreenPad = GamepadButton.A, + KickOne = GamepadButton.LeftBumper, + KickTwo = GamepadButton.RightBumper + } - /// - /// Definitions for drumkit pad velocity data. - /// - static class DrumCymVel - { - public const byte - // No red cymbal - // Red = 0, - Yellow = 0xF0, - Blue = 0x0F, - Green = 0xF0; + /// + /// Masks for each pad's value. + /// + enum Pad : ushort + { + Red = 0x00F0, + Yellow = 0x000F, + Blue = 0xF000, + Green = 0x0F00 + } + + /// + /// Masks for each cymbal's value. + /// + enum Cymbal : ushort + { + Yellow = 0x00F0, + Blue = 0x000F, + Green = 0xF000 + } + + public ushort Buttons; + ushort pads; + ushort cymbals; + + public byte RedPad => getByteValue(pads, (ushort)Pad.Red, 4); + public byte YellowPad => getByteValue(pads, (ushort)Pad.Yellow, 0); + public byte BluePad => getByteValue(pads, (ushort)Pad.Blue, 12); + public byte GreenPad => getByteValue(pads, (ushort)Pad.Green, 8); + + public byte YellowCymbal => getByteValue(cymbals, (ushort)Cymbal.Yellow, 4); + public byte BlueCymbal => getByteValue(cymbals, (ushort)Cymbal.Blue, 0); + public byte GreenCymbal => getByteValue(cymbals, (ushort)Cymbal.Green, 12); + + /// + /// Gets a byte value from a ushort field of multiple values. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + byte getByteValue(ushort field, ushort mask, ushort offset) + { + return (byte)((field & mask) >> offset); + } } } \ No newline at end of file diff --git a/Program/PacketParsing/PacketParser.cs b/Program/PacketParsing/PacketParser.cs index 18b9c79..def4859 100644 --- a/Program/PacketParsing/PacketParser.cs +++ b/Program/PacketParsing/PacketParser.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using RB4InstrumentMapper.Parsing; // This is in the regular namespace to keep the other packet parsing stuff from bogging up @@ -38,33 +39,17 @@ public static class PacketParser /// /// Handles a received Pcap packet. /// - public static void HandlePcapPacket(ReadOnlySpan data, ref ulong processedCount) + public static unsafe void HandlePcapPacket(ReadOnlySpan data, ref ulong processedCount) { - // Packet must be at least 30 bytes long - if (data.Length < (Length.ReceiverHeader + Length.CommandHeader)) + // Packet must be at least 30 bytes long, to contain both the receiver and command headers + if (data.Length < (sizeof(ReceiverHeader) + sizeof(CommandHeader)) + || !MemoryMarshal.TryRead(data, out ReceiverHeader header)) { return; } - // Get device ID - // Have to do it in chunks to avoid bit shift wraparounds and sign extension weirdness from casting - ulong deviceIdLow = (ulong)( - data[HeaderOffset.DeviceId + 5] | - (data[HeaderOffset.DeviceId + 4] << 8) | - (data[HeaderOffset.DeviceId + 3] << 16) | - (data[HeaderOffset.DeviceId + 2] << 24) - ); - ulong deviceIdHigh = (ulong)( - data[HeaderOffset.DeviceId + 1] | - (data[HeaderOffset.DeviceId] << 8) - ); - - ulong deviceId = ( - deviceIdLow | - (deviceIdHigh << 32) - ); - - // Check if ID has been encountered yet + // Check if device ID has been encountered yet + ulong deviceId = header.DeviceId; if (!pcapIds.ContainsKey(deviceId)) { if (!canHandleNewDevices) @@ -90,7 +75,7 @@ public static void HandlePcapPacket(ReadOnlySpan data, ref ulong processed } // Strip off receiver header and send the data to be parsed - pcapIds[deviceId].ParseCommand(data.Slice(Length.ReceiverHeader)); + pcapIds[deviceId].ParseCommand(data.Slice(sizeof(ReceiverHeader))); processedCount++; } diff --git a/Program/PacketParsing/ParsingUtils.cs b/Program/PacketParsing/ParsingUtils.cs index 5d097ff..d1665ec 100644 --- a/Program/PacketParsing/ParsingUtils.cs +++ b/Program/PacketParsing/ParsingUtils.cs @@ -46,37 +46,5 @@ public static ushort ScaleToUInt16(this byte input) // first bit of each region set to 1 return (ushort)(input * 0x0101); } - - /// - /// Gets an unsigned short value from a specified index, parsed as a little-endian value. - /// - public static ushort GetUInt16LE(this ReadOnlySpan span, int index) - { - return (ushort)(span[index + 1] << 8 | span[index]); - } - - /// - /// Gets an unsigned short value from a specified index, parsed as a big-endian value. - /// - public static ushort GetUInt16BE(this ReadOnlySpan span, int index) - { - return (ushort)(span[index] << 8 | span[index + 1]); - } - - /// - /// Gets a short value from a specified index, parsed as a little-endian value. - /// - public static short GetInt16LE(this ReadOnlySpan span, int index) - { - return (short)(span[index + 1] << 8 | span[index]); - } - - /// - /// Gets a short value from a specified index, parsed as a big-endian value. - /// - public static short GetInt16BE(this ReadOnlySpan span, int index) - { - return (short)(span[index] << 8 | span[index + 1]); - } } } diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index f85449d..cf9125d 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics; +using System.Runtime.InteropServices; using Nefarius.ViGEm.Client.Exceptions; using Nefarius.ViGEm.Client.Targets; using Nefarius.ViGEm.Client.Targets.Xbox360; @@ -66,37 +68,43 @@ void ReceiveUserIndex(object sender, Xbox360FeedbackReceivedEventArgs args) /// /// Parses an input report. /// - public void ParseInput(ReadOnlySpan data, byte length, byte sequenceCount) + public unsafe void ParseInput(CommandHeader header, ReadOnlySpan data) { - // Don't parse the same report twice - if (sequenceCount == prevInputSeqCount) + // Don't process if not connected + if (!deviceConnected) { return; } - else + + // Ensure lengths match + if (header.DataLength != data.Length) { - prevInputSeqCount = sequenceCount; + // This is probably a bug, emit a debug message + Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {data.Length}"); + return; } - if (!deviceConnected) + // Don't parse the same report twice + if (header.SequenceCount == prevInputSeqCount) { - // Device has not connected yet return; } - switch (length) + header.SequenceCount = prevInputSeqCount; + + int length = header.DataLength; + if (length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) + { + ParseGuitar(guitarReport); + } + else if (length == sizeof(DrumInput) && MemoryMarshal.TryRead(data, out DrumInput drumReport)) { - case Length.Input_Guitar: - ParseGuitar(data); - break; - - case Length.Input_Drums: - ParseDrums(data); - break; - - default: - // Don't parse unknown button data - return; + ParseDrums(drumReport); + } + else + { + // Report is not valid + return; } // Send data @@ -106,7 +114,7 @@ public void ParseInput(ReadOnlySpan data, byte length, byte sequenceCount) /// /// Parses common button data from an input report. /// - private void ParseCoreButtons(ushort buttons) + private void ParseCoreButtons(GamepadButton buttons) { // Menu device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); @@ -125,36 +133,36 @@ private void ParseCoreButtons(ushort buttons) /// /// Parses guitar input data from an input report. /// - private void ParseGuitar(ReadOnlySpan data) + private void ParseGuitar(GuitarInput report) { // Buttons - ParseCoreButtons(data.GetUInt16BE(GuitarOffset.Buttons)); + ParseCoreButtons((GamepadButton)report.Buttons); // Frets - byte frets = data[GuitarOffset.UpperFrets]; - frets |= data[GuitarOffset.LowerFrets]; + device.SetButtonState(Xbox360Button.A, report.Green); + device.SetButtonState(Xbox360Button.B, report.Red); + device.SetButtonState(Xbox360Button.Y, report.Yellow); + device.SetButtonState(Xbox360Button.X, report.Blue); + device.SetButtonState(Xbox360Button.LeftShoulder, report.Orange); - device.SetButtonState(Xbox360Button.A, (frets & GuitarFret.Green) != 0); - device.SetButtonState(Xbox360Button.B, (frets & GuitarFret.Red) != 0); - device.SetButtonState(Xbox360Button.Y, (frets & GuitarFret.Yellow) != 0); - device.SetButtonState(Xbox360Button.X, (frets & GuitarFret.Blue) != 0); - device.SetButtonState(Xbox360Button.LeftShoulder, (frets & GuitarFret.Orange) != 0); + // Lower fret flag + device.SetButtonState(Xbox360Button.LeftThumb, report.LowerFretFlag); // Whammy - device.SetAxisValue(Xbox360Axis.RightThumbX, data[GuitarOffset.WhammyBar].ScaleToInt16()); + device.SetAxisValue(Xbox360Axis.RightThumbX, report.WhammyBar.ScaleToInt16()); // Tilt - device.SetAxisValue(Xbox360Axis.RightThumbY, data[GuitarOffset.Tilt].ScaleToInt16()); + device.SetAxisValue(Xbox360Axis.RightThumbY, report.Tilt.ScaleToInt16()); // Pickup Switch - device.SetSliderValue(Xbox360Slider.LeftTrigger, data[GuitarOffset.PickupSwitch]); + device.SetSliderValue(Xbox360Slider.LeftTrigger, report.PickupSwitch); } /// /// Parses drums input data from an input report. /// - private void ParseDrums(ReadOnlySpan data) + private void ParseDrums(DrumInput report) { // Buttons - ushort buttons = data.GetUInt16BE(DrumOffset.Buttons); + var buttons = (GamepadButton)report.Buttons; ParseCoreButtons(buttons); device.SetButtonState(Xbox360Button.A, (buttons & GamepadButton.A) != 0); @@ -163,14 +171,14 @@ private void ParseDrums(ReadOnlySpan data) device.SetButtonState(Xbox360Button.Y, (buttons & GamepadButton.Y) != 0); // Pads and cymbals - byte redPad = (byte)(data[DrumOffset.PadVels] >> 4); - byte yellowPad = (byte)(data[DrumOffset.PadVels] & DrumPadVel.Yellow); - byte bluePad = (byte)(data[DrumOffset.PadVels + 1] >> 4); - byte greenPad = (byte)(data[DrumOffset.PadVels + 1] & DrumPadVel.Green); + byte redPad = report.RedPad; + byte yellowPad = report.YellowPad; + byte bluePad = report.BluePad; + byte greenPad = report.GreenPad; - byte yellowCym = (byte)(data[DrumOffset.CymbalVels] >> 4); - byte blueCym = (byte)(data[DrumOffset.CymbalVels] & DrumCymVel.Blue); - byte greenCym = (byte)(data[DrumOffset.CymbalVels + 1] >> 4); + byte yellowCym = report.YellowCymbal; + byte blueCym = report.BlueCymbal; + byte greenCym = report.GreenCymbal; // Color flags device.SetButtonState(Xbox360Button.B, (redPad) != 0); @@ -187,9 +195,9 @@ private void ParseDrums(ReadOnlySpan data) // Pedals device.SetButtonState(Xbox360Button.LeftShoulder, - (data[DrumOffset.Buttons + 1] & DrumButton.KickOne) != 0); + (report.Buttons & (ushort)DrumInput.Button.KickOne) != 0); device.SetButtonState(Xbox360Button.LeftThumb, - (data[DrumOffset.Buttons + 1] & DrumButton.KickTwo) != 0); + (report.Buttons & (ushort)DrumInput.Button.KickTwo) != 0); // Velocities device.SetAxisValue( diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs index b317351..e0928b5 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/VjoyMapper.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using vJoyInterfaceWrap; using Button = RB4InstrumentMapper.Parsing.VjoyStatic.Button; @@ -34,32 +36,37 @@ public VjoyMapper() /// /// Parses an input report. /// - public void ParseInput(ReadOnlySpan data, byte length, byte sequenceCount) + public unsafe void ParseInput(CommandHeader header, ReadOnlySpan data) { - // Don't parse the same report twice - if (sequenceCount == prevInputSeqCount) + // Ensure lengths match + if (header.DataLength != data.Length) { + // This is probably a bug, emit a debug message + Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {data.Length}"); return; } - else - { - prevInputSeqCount = sequenceCount; - } - // Parse the respective device - switch (length) + // Don't parse the same report twice + if (header.SequenceCount == prevInputSeqCount) { - case Length.Input_Guitar: - ParseGuitar(data); - break; + return; + } - case Length.Input_Drums: - ParseDrums(data); - break; + header.SequenceCount = prevInputSeqCount; - default: - // Don't parse unknown input reports - return; + int length = header.DataLength; + if (length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) + { + ParseGuitar(guitarReport); + } + else if (length == sizeof(DrumInput) && MemoryMarshal.TryRead(data, out DrumInput drumReport)) + { + ParseDrums(drumReport); + } + else + { + // Report is not valid + return; } // Send data @@ -85,7 +92,7 @@ private void SetButton(uint button, bool condition) /// /// Parses common button data from an input report. /// - private void ParseCoreButtons(ushort buttons) + private void ParseCoreButtons(GamepadButton buttons) { // Menu SetButton(Button.Fifteen, (buttons & GamepadButton.Menu) != 0); @@ -146,64 +153,61 @@ private void ParseCoreButtons(ushort buttons) /// /// Parses guitar input data from an input report. /// - private void ParseGuitar(ReadOnlySpan data) + private void ParseGuitar(GuitarInput report) { // Buttons - ParseCoreButtons(data.GetUInt16BE(GuitarOffset.Buttons)); - - // Frets - byte frets = data[GuitarOffset.UpperFrets]; - frets |= data[GuitarOffset.LowerFrets]; + ParseCoreButtons((GamepadButton)report.Buttons); - SetButton(Button.One, (frets & GuitarFret.Green) != 0); - SetButton(Button.Two, (frets & GuitarFret.Red) != 0); - SetButton(Button.Three, (frets & GuitarFret.Yellow) != 0); - SetButton(Button.Four, (frets & GuitarFret.Blue) != 0); - SetButton(Button.Five, (frets & GuitarFret.Orange) != 0); + SetButton(Button.One, report.Green); + SetButton(Button.Two, report.Red); + SetButton(Button.Three, report.Yellow); + SetButton(Button.Four, report.Blue); + SetButton(Button.Five, report.Orange); // Whammy // Value ranges from 0 (not pressed) to 255 (fully pressed) - state.AxisY = data[GuitarOffset.WhammyBar].ScaleToInt32(); + state.AxisY = report.WhammyBar.ScaleToInt32(); // Tilt // Value ranges from 0 to 255 // It seems to have a threshold of around 0x70 though, // after a certain point values will get floored to 0 - state.AxisZ = data[GuitarOffset.Tilt].ScaleToInt32(); + state.AxisZ = report.Tilt.ScaleToInt32(); // Pickup switch // Reported values are 0x00, 0x10, 0x20, 0x30, and 0x40 (ranges from 0 to 64) - state.AxisX = data[GuitarOffset.PickupSwitch].ScaleToInt32(); + state.AxisX = report.PickupSwitch.ScaleToInt32(); } /// /// Parses drums input data from an input report. /// - private void ParseDrums(ReadOnlySpan data) + private void ParseDrums(DrumInput report) { // Buttons - ushort buttons = data.GetUInt16BE(DrumOffset.Buttons); + var buttons = (GamepadButton)report.Buttons; ParseCoreButtons(buttons); + // Face buttons SetButton(Button.Four, (buttons & GamepadButton.A) != 0); SetButton(Button.One, (buttons & GamepadButton.B) != 0); SetButton(Button.Three, (buttons & GamepadButton.X) != 0); SetButton(Button.Two, (buttons & GamepadButton.Y) != 0); // Pads - SetButton(Button.One, (data[DrumOffset.PadVels] & DrumPadVel.Red) != 0); - SetButton(Button.Two, (data[DrumOffset.PadVels] & DrumPadVel.Yellow) != 0); - SetButton(Button.Three, (data[DrumOffset.PadVels + 1] & DrumPadVel.Blue) != 0); - SetButton(Button.Four, (data[DrumOffset.PadVels + 1] & DrumPadVel.Green) != 0); + SetButton(Button.One, report.RedPad != 0); + SetButton(Button.Two, report.YellowPad != 0); + SetButton(Button.Three, report.BluePad != 0); + SetButton(Button.Four, report.GreenPad != 0); // Cymbals - SetButton(Button.Six, (data[DrumOffset.CymbalVels] & DrumCymVel.Yellow) != 0); - SetButton(Button.Seven, (data[DrumOffset.CymbalVels] & DrumCymVel.Blue) != 0); - SetButton(Button.Eight, (data[DrumOffset.CymbalVels + 1] & DrumCymVel.Green) != 0); + SetButton(Button.Six, report.YellowCymbal != 0); + SetButton(Button.Seven, report.BlueCymbal != 0); + SetButton(Button.Eight, report.GreenCymbal != 0); // Kick pedals - SetButton(Button.Five, (data[DrumOffset.Buttons + 1] & DrumButton.KickOne) != 0); - SetButton(Button.Nine, (data[DrumOffset.Buttons + 1] & DrumButton.KickTwo) != 0); + SetButton(Button.Five, (report.Buttons & (ushort)DrumInput.Button.KickOne) != 0); + SetButton(Button.Nine, (report.Buttons & (ushort)DrumInput.Button.KickTwo) != 0); } /// diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index dd8d67a..84461c7 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; namespace RB4InstrumentMapper.Parsing { @@ -43,12 +44,17 @@ public XboxDevice(ParsingMode parseMode) /// /// Parses command data from a packet. /// - public void ParseCommand(ReadOnlySpan commandData) + public unsafe void ParseCommand(ReadOnlySpan commandData) { - switch (commandData[CommandOffset.CommandId]) + if (!MemoryMarshal.TryRead(commandData, out CommandHeader header)) { - case CommandId.Input: - deviceMapper.ParseInput(commandData.Slice(Length.CommandHeader), commandData[CommandOffset.DataLength], commandData[CommandOffset.SequenceCount]); + return; + } + + switch (header.CommandId) + { + case (byte)CommandHeader.Command.Input: + deviceMapper.ParseInput(header, commandData.Slice(sizeof(CommandHeader))); break; default: From a41a705077a19b90109dcaab665abe7f0f7ca298 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 5 Nov 2022 11:08:51 -0600 Subject: [PATCH 084/437] Bump version to v2.1 --- Installer/Product.wxs | 2 +- Program/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 4a95c6f..89f0df4 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -4,7 +4,7 @@ Id="*" Name="RB4InstrumentMapper" Language="1033" - Version="2.0.2.0" + Version="2.1.0.0" Manufacturer="Andreas Schiffler" UpgradeCode="94bef546-701f-4571-9828-d4fa39b2ea84"> diff --git a/Program/Properties/AssemblyInfo.cs b/Program/Properties/AssemblyInfo.cs index e986c76..0d2d445 100644 --- a/Program/Properties/AssemblyInfo.cs +++ b/Program/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.0.2.0")] -[assembly: AssemblyFileVersion("2.0.2.0")] +[assembly: AssemblyVersion("2.1.0.0")] +[assembly: AssemblyFileVersion("2.1.0.0")] From 1def0f6fca000e2d4884cd9048053c375c98bc45 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 5 Nov 2022 11:15:11 -0600 Subject: [PATCH 085/437] Add drums power-on log to docs folder Thanks yoshiegg52! --- Docs/drums_power_on.txt | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 Docs/drums_power_on.txt diff --git a/Docs/drums_power_on.txt b/Docs/drums_power_on.txt new file mode 100644 index 0000000..780b3b1 --- /dev/null +++ b/Docs/drums_power_on.txt @@ -0,0 +1,102 @@ +2022-11-02 11:52:31.923 [58] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46000001000220011C62BCCE85ED7E00006F0E71010100000007001E000002010001000100 +2022-11-02 11:52:31.923 [58] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46000001000220011C62BCCE85ED7E00006F0E71010100000007001E000002010001000100 +2022-11-02 11:52:31.980 [90] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B461000010004F0033AC3011000010000000000000000000000C3009B0016001B001C00230029006A0000000000000000000101000000000601020304060705010405060A02 +2022-11-02 11:52:31.980 [90] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B461000010004F0033AC3011000010000000000000000000000C3009B0016001B001C00230029006A0000000000000000000101000000000601020304060705010405060A02 +2022-11-02 11:52:31.996 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B462000010004A063BA003A15005044502E58626F782E4472756D732E5461626C6168270057696E646F77732E58626F782E496E7075742E4E617669676174696F6E436F6E74 +2022-11-02 11:52:31.996 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B462000010004A063BA003A15005044502E58626F782E4472756D732E5461626C6168270057696E646F77732E58626F782E496E7075742E4E617669676174696F6E436F6E74 +2022-11-02 11:52:31.996 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B460000040004A063BA0074726F6C6C657203B0F903A55E95C447A2EDB1336FA7703EE71FF3B88673E940A9F82F21263ACFB756FF7697FD9B8145AD45B645BBA526D6011700 +2022-11-02 11:52:31.996 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B460000040004A063BA0074726F6C6C657203B0F903A55E95C447A2EDB1336FA7703EE71FF3B88673E940A9F82F21263ACFB756FF7697FD9B8145AD45B645BBA526D6011700 +2022-11-02 11:52:32.004 [53] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B463000010004B06315AE01200A00010014000000000000000000000000000000 +2022-11-02 11:52:32.004 [53] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B463000010004B06315AE01200A00010014000000000000000000000000000000 +2022-11-02 11:52:32.028 [32] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B464000010004A06400C301 +2022-11-02 11:52:32.028 [32] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B464000010004A06400C301 +2022-11-02 11:52:32.080 [36] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B460000000020000806000000000000 +2022-11-02 11:52:32.080 [36] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B460000000020000806000000000000 +2022-11-02 11:52:32.088 [34] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46500001000320090480010000 +2022-11-02 11:52:32.088 [34] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46500001000320090480010000 +2022-11-02 11:52:32.160 [39] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B466000010001200A090006303A0000000000 +2022-11-02 11:52:32.160 [39] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B466000010001200A090006303A0000000000 +2022-11-02 11:52:32.160 [36] 88113C006245BD0F8B467EED85CEBC626045BD0F8B461000040006300B0600C100010000 +2022-11-02 11:52:32.160 [36] 88113C006245BD0F8B467EED85CEBC626045BD0F8B461000040006300B0600C100010000 +2022-11-02 11:52:32.282 [39] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B467000010001200D090006300E0000000000 +2022-11-02 11:52:32.282 [39] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B467000010001200D090006300E0000000000 +2022-11-02 11:52:32.282 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B462000040006F00EBA005A00C200020054020100500ECADE7845F4FFECFB163BC6232A2F206C7FC454822F68EA0A6FB29FA1C51CB43126E151000003E900410205B80D000D +2022-11-02 11:52:32.282 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B462000040006F00EBA005A00C200020054020100500ECADE7845F4FFECFB163BC6232A2F206C7FC454822F68EA0A6FB29FA1C51CB43126E151000003E900410205B80D000D +2022-11-02 11:52:32.306 [64] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B468000010006B06B20BA00006F0013DADECE4B030002800000000000000000000000000000000000000000 +2022-11-02 11:52:32.306 [64] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B468000010006B06B20BA00006F0013DADECE4B030002800000000000000000000000000000000000000000 +2022-11-02 11:52:32.330 [32] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B469000010006A06C00DA00 +2022-11-02 11:52:32.330 [32] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B469000010006A06C00DA00 +2022-11-02 11:52:32.539 [39] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46A0000100012011090006300E0000000000 +2022-11-02 11:52:32.539 [39] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46A0000100012011090006300E0000000000 +2022-11-02 11:52:32.548 [90] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46B000010006F0123AB90600C2000303330301032F3082032B30820213A00302010202047A5D35D5300D06092A864886F70D01010B05003076310B30090603550406130244 +2022-11-02 11:52:32.548 [90] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46B000010006F0123AB90600C2000303330301032F3082032B30820213A00302010202047A5D35D5300D06092A864886F70D01010B05003076310B30090603550406130244 +2022-11-02 11:52:32.564 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B46C000010006A06FBA003A45310F300D060355040813065361786F6E7931163014060355040A130D537562636C61737320303030323111300F060355040B1308436C617373 +2022-11-02 11:52:32.564 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B46C000010006A06FBA003A45310F300D060355040813065361786F6E7931163014060355040A130D537562636C61737320303030323111300F060355040B1308436C617373 +2022-11-02 11:52:32.564 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B463000040006A06FBA0074203033312B30290603550403132258626F78204163636573736F7269657320436C6173732050726F6420434120303035301E170D313630333234 +2022-11-02 11:52:32.564 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B463000040006A06FBA0074203033312B30290603550403132258626F78204163636573736F7269657320436C6173732050726F6420434120303035301E170D313630333234 +2022-11-02 11:52:32.564 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B464000040006A06F3AAE013132323034345A170D3434313031353233353935395A300030820122300D06092A864886F70D01010105000382010F003082010A028201010098 +2022-11-02 11:52:32.564 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B464000040006A06F3AAE013132323034345A170D3434313031353233353935395A300030820122300D06092A864886F70D01010105000382010F003082010A028201010098 +2022-11-02 11:52:32.572 [90] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46D000010006B06F3AE8011443DDA4F529BFE07F08DDCF2672F0F2DC972CA7199547F775F8FD9F6717FD58825D0C323BB36DEFD1CB860478387DC62E6EBDB9BF5317488298 +2022-11-02 11:52:32.572 [90] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46D000010006B06F3AE8011443DDA4F529BFE07F08DDCF2672F0F2DC972CA7199547F775F8FD9F6717FD58825D0C323BB36DEFD1CB860478387DC62E6EBDB9BF5317488298 +2022-11-02 11:52:32.588 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B46E000010006A0703AA202521E8206A7AA38AE3684BB838DD9368FA70111D204A7183E1FDDD339DDE49D00AA1A7964162B2EECF0F7D70944D247C72A4259B771FB9B26A54E +2022-11-02 11:52:32.588 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B46E000010006A0703AA202521E8206A7AA38AE3684BB838DD9368FA70111D204A7183E1FDDD339DDE49D00AA1A7964162B2EECF0F7D70944D247C72A4259B771FB9B26A54E +2022-11-02 11:52:32.588 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B465000040006A0703ADC023943D1EA8A1688C523D219EA12EAAB4F03021EFF7DCD3AEE0B81F181150F83E5BCF183A24643F504AEA45D3A4071040D2F4A4F52E50A7727B029 +2022-11-02 11:52:32.588 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B465000040006A0703ADC023943D1EA8A1688C523D219EA12EAAB4F03021EFF7DCD3AEE0B81F181150F83E5BCF183A24643F504AEA45D3A4071040D2F4A4F52E50A7727B029 +2022-11-02 11:52:32.588 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B466000040006A0703A96036A67DBC2179E696D55DD81864EDC2FB769BAFAA3B04960096246B7A249A4531E25081FE3463F4B097E4C099EA2F738D8C88B0417743152CDFBCD +2022-11-02 11:52:32.588 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B466000040006A0703A96036A67DBC2179E696D55DD81864EDC2FB769BAFAA3B04960096246B7A249A4531E25081FE3463F4B097E4C099EA2F738D8C88B0417743152CDFBCD +2022-11-02 11:52:32.588 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B467000040006A0703AD0035C2B80D5CC87945571DF3823813A41A9E56C28024D027F0203010001A3373035300E0603551D0F0101FF0404030200B0300C0603551D130101FF +2022-11-02 11:52:32.588 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B467000040006A0703AD0035C2B80D5CC87945571DF3823813A41A9E56C28024D027F0203010001A3373035300E0603551D0F0101FF0404030200B0300C0603551D130101FF +2022-11-02 11:52:32.597 [90] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46F000010006B0703A8A040402300030150603551D25040E300C060A2B060104018237780301300D06092A864886F70D01010B05000382010100019F0C948A2886FB0B5FD5 +2022-11-02 11:52:32.597 [90] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46F000010006B0703A8A040402300030150603551D25040E300C060A2B060104018237780301300D06092A864886F70D01010B05000382010100019F0C948A2886FB0B5FD5 +2022-11-02 11:52:32.605 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B460001010006A0013AFE04EA76FD2B12466AB53C3C8C16A6254E596DC423A68A1D5BFB898490C399EB9A4AF11D81D3A1FA9C51E059A5E88E4B6CED12E91161726D9F700C37 +2022-11-02 11:52:32.605 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B460001010006A0013AFE04EA76FD2B12466AB53C3C8C16A6254E596DC423A68A1D5BFB898490C399EB9A4AF11D81D3A1FA9C51E059A5E88E4B6CED12E91161726D9F700C37 +2022-11-02 11:52:32.606 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B468000040006A0013AB805BE5A3B116C676AB9387320DDA8BC19F6BEAE6BFD28971F902D80FF7E62CF18A556A9BE87DCCB3C115E60DCEC2C877A8158FF4BDE9D6505932380 +2022-11-02 11:52:32.606 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B468000040006A0013AB805BE5A3B116C676AB9387320DDA8BC19F6BEAE6BFD28971F902D80FF7E62CF18A556A9BE87DCCB3C115E60DCEC2C877A8158FF4BDE9D6505932380 +2022-11-02 11:52:32.606 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B469000040006A0013AF2054B00AE38019E9382B59F99BE77102BEFB581BAFEE97F97DF799720D5FB63CFCEA28586369C29D1F4F0A397907AB8B4C2581740431CBFE4B60BAE +2022-11-02 11:52:32.606 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B469000040006A0013AF2054B00AE38019E9382B59F99BE77102BEFB581BAFEE97F97DF799720D5FB63CFCEA28586369C29D1F4F0A397907AB8B4C2581740431CBFE4B60BAE +2022-11-02 11:52:32.606 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B46A000040006B0703A8A040402300030150603551D25040E300C060A2B060104018237780301300D06092A864886F70D01010B05000382010100019F0C948A2886FB0B5FD5 +2022-11-02 11:52:32.606 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B46A000040006B0703A8A040402300030150603551D25040E300C060A2B060104018237780301300D06092A864886F70D01010B05000382010100019F0C948A2886FB0B5FD5 +2022-11-02 11:52:32.614 [45] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B461001010006B0010DAC06A99DC6B1E490B2A95244F51F8D +2022-11-02 11:52:32.614 [45] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B461001010006B0010DAC06A99DC6B1E490B2A95244F51F8D +2022-11-02 11:52:32.622 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B462001010006A0033AC4049FFCC5F7353F71BFE9D056E9D84E99F1F9C26B3BFA8DED10355FA6BB675180E68581C695DE35E26DBA062F6224CE5E6656164044EB24A031EA6C +2022-11-02 11:52:32.622 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B462001010006A0033AC4049FFCC5F7353F71BFE9D056E9D84E99F1F9C26B3BFA8DED10355FA6BB675180E68581C695DE35E26DBA062F6224CE5E6656164044EB24A031EA6C +2022-11-02 11:52:32.622 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B46B000040006A0033AFE04EA76FD2B12466AB53C3C8C16A6254E596DC423A68A1D5BFB898490C399EB9A4AF11D81D3A1FA9C51E059A5E88E4B6CED12E91161726D9F700C37 +2022-11-02 11:52:32.622 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B46B000040006A0033AFE04EA76FD2B12466AB53C3C8C16A6254E596DC423A68A1D5BFB898490C399EB9A4AF11D81D3A1FA9C51E059A5E88E4B6CED12E91161726D9F700C37 +2022-11-02 11:52:32.622 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B46C000040006A0033AB805BE5A3B116C676AB9387320DDA8BC19F6BEAE6BFD28971F902D80FF7E62CF18A556A9BE87DCCB3C115E60DCEC2C877A8158FF4BDE9D6505932380 +2022-11-02 11:52:32.622 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B46C000040006A0033AB805BE5A3B116C676AB9387320DDA8BC19F6BEAE6BFD28971F902D80FF7E62CF18A556A9BE87DCCB3C115E60DCEC2C877A8158FF4BDE9D6505932380 +2022-11-02 11:52:32.622 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B46D000040006A0033AF2054B00AE38019E9382B59F99BE77102BEFB581BAFEE97F97DF799720D5FB63CFCEA28586369C29D1F4F0A397907AB8B4C2581740431CBFE4B60BAE +2022-11-02 11:52:32.622 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B46D000040006A0033AF2054B00AE38019E9382B59F99BE77102BEFB581BAFEE97F97DF799720D5FB63CFCEA28586369C29D1F4F0A397907AB8B4C2581740431CBFE4B60BAE +2022-11-02 11:52:32.630 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B463001010006A0633AC4049FFCC5F7353F71BFE9D056E9D84E99F1F9C26B3BFA8DED10355FA6BB675180E68581C695DE35E26DBA062F6224CE5E6656164044EB24A031EA6C +2022-11-02 11:52:32.630 [90] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B463001010006A0633AC4049FFCC5F7353F71BFE9D056E9D84E99F1F9C26B3BFA8DED10355FA6BB675180E68581C695DE35E26DBA062F6224CE5E6656164044EB24A031EA6C +2022-11-02 11:52:32.630 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B46E000040006A0633AFE04EA76FD2B12466AB53C3C8C16A6254E596DC423A68A1D5BFB898490C399EB9A4AF11D81D3A1FA9C51E059A5E88E4B6CED12E91161726D9F700C37 +2022-11-02 11:52:32.630 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B46E000040006A0633AFE04EA76FD2B12466AB53C3C8C16A6254E596DC423A68A1D5BFB898490C399EB9A4AF11D81D3A1FA9C51E059A5E88E4B6CED12E91161726D9F700C37 +2022-11-02 11:52:32.630 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B46F000040006A0633AB805BE5A3B116C676AB9387320DDA8BC19F6BEAE6BFD28971F902D80FF7E62CF18A556A9BE87DCCB3C115E60DCEC2C877A8158FF4BDE9D6505932380 +2022-11-02 11:52:32.630 [90] 88313C006245BD0F8B467EED85CEBC626045BD0F8B46F000040006A0633AB805BE5A3B116C676AB9387320DDA8BC19F6BEAE6BFD28971F902D80FF7E62CF18A556A9BE87DCCB3C115E60DCEC2C877A8158FF4BDE9D6505932380 +2022-11-02 11:52:32.631 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B460001040006A0633AF2054B00AE38019E9382B59F99BE77102BEFB581BAFEE97F97DF799720D5FB63CFCEA28586369C29D1F4F0A397907AB8B4C2581740431CBFE4B60BAE +2022-11-02 11:52:32.631 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B460001040006A0633AF2054B00AE38019E9382B59F99BE77102BEFB581BAFEE97F97DF799720D5FB63CFCEA28586369C29D1F4F0A397907AB8B4C2581740431CBFE4B60BAE +2022-11-02 11:52:32.648 [45] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B464001010006B0630DAC06A99DC6B1E490B2A95244F51F8D +2022-11-02 11:52:32.648 [45] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B464001010006B0630DAC06A99DC6B1E490B2A95244F51F8D +2022-11-02 11:52:32.671 [32] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B465001010006A06400B906 +2022-11-02 11:52:32.671 [32] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B465001010006A06400B906 +2022-11-02 11:52:32.771 [39] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B466001010001202C090006F03A000000D800 +2022-11-02 11:52:32.771 [39] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B466001010001202C090006F03A000000D800 +2022-11-02 11:52:32.809 [39] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B467001010001202D090006B0120100000000 +2022-11-02 11:52:32.809 [39] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B467001010001202D090006B0120100000000 +2022-11-02 11:52:33.502 [36] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B468001010006302E0600C100010000 +2022-11-02 11:52:33.502 [36] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B468001010006302E0600C100010000 +2022-11-02 11:52:33.712 [39] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B469001010001203009000630320000000000 +2022-11-02 11:52:33.712 [39] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B469001010001203009000630320000000000 +2022-11-02 11:52:33.712 [36] 88113C006245BD0F8B467EED85CEBC626045BD0F8B46100104000630310600C100010000 +2022-11-02 11:52:33.712 [36] 88113C006245BD0F8B467EED85CEBC626045BD0F8B46100104000630310600C100010000 +2022-11-02 11:52:33.937 [39] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B46A0010100012033090006300E0000000000 +2022-11-02 11:52:33.937 [39] 8831A0006245BD0F8B467EED85CEBC626245BD0F8B46A0010100012033090006300E0000000000 +2022-11-02 11:52:33.937 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B462001040006F034BA004A00C20008004408010040847773551DBE20255136A41C8E5C413F64C2B20245CF174D23D3EB1584C6D5A0369369B223E47D570564200087CC304E +2022-11-02 11:52:33.937 [90] 88113C006245BD0F8B467EED85CEBC626045BD0F8B462001040006F034BA004A00C20008004408010040847773551DBE20255136A41C8E5C413F64C2B20245CF174D23D3EB1584C6D5A0369369B223E47D570564200087CC304E +2022-11-02 11:52:33.961 [48] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46B001010006B06C10BA00D8C0736719C53FD2A9352FDE76D44E37 +2022-11-02 11:52:33.961 [48] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46B001010006B06C10BA00D8C0736719C53FD2A9352FDE76D44E37 +2022-11-02 11:52:33.985 [32] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46C001010006A01100CA00 +2022-11-02 11:52:33.985 [32] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B46C001010006A01100CA00 +2022-11-02 11:52:39.008 [36] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B461000000020003706100000010000 +2022-11-02 11:52:39.008 [36] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B461000000020003706100000010000 +2022-11-02 11:52:39.040 [36] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B462000000020003806000000000000 +2022-11-02 11:52:39.040 [36] 8811A0006245BD0F8B467EED85CEBC626245BD0F8B462000000020003806000000000000 From aa43e2eb486cae26349bf58ae58d0105e81182d2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 5 Nov 2022 13:36:34 -0600 Subject: [PATCH 086/437] Some additional docs updates - Change "Options" to "View" - Remove note about drums data being speculatory - There are actual ABXY face buttons on the drumkit, their inputs aren't unused --- Docs/PacketFormats.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Docs/PacketFormats.md b/Docs/PacketFormats.md index 645b56d..dfc292c 100644 --- a/Docs/PacketFormats.md +++ b/Docs/PacketFormats.md @@ -100,7 +100,7 @@ The standard Xbox One controller layout is as follows: - Bit 0 (`0x0001`) - Sync button - Bit 1 (`0x0002`) - Unused - Bit 2 (`0x0004`) - Menu Button - - Bit 3 (`0x0008`) - Options Button + - Bit 3 (`0x0008`) - View Button - Bit 4 (`0x0010`) - A Button - Bit 5 (`0x0020`) - B Button - Bit 6 (`0x0040`) - X Button @@ -126,7 +126,7 @@ The standard Xbox One controller layout is as follows: - Bit 0 (`0x0001`) - Sync button - Bit 1 (`0x0002`) - Unused - Bit 2 (`0x0004`) - Menu Button - - Bit 3 (`0x0008`) - Options Button + - Bit 3 (`0x0008`) - View Button - Bit 4 (`0x0010`) - Green Fret Flag (equivalent to A Button) - Bit 5 (`0x0020`) - Red Fret Flag (equivalent to B Button) - Bit 6 (`0x0040`) - Blue Fret Flag (equivalent to X Button) @@ -178,8 +178,6 @@ The standard Xbox One controller layout is as follows: ## Drums Input Data -Some of the data here is speculatory. It needs to be verified using packet captures. - 6 bytes long ` ` @@ -190,11 +188,11 @@ Bytes: - Bit 0 (`0x0001`) - Sync button - Bit 1 (`0x0002`) - Unused - Bit 2 (`0x0004`) - Menu Button - - Bit 3 (`0x0008`) - Options Button - - Bit 4 (`0x0010`) - Green Pad (equivalent to A Button) - - Bit 5 (`0x0020`) - Red Pad (equivalent to B Button) - - Bit 6 (`0x0040`) - Unused (equivalent to X Button) - - Bit 7 (`0x0080`) - Unused (equivalent to Y Button) + - Bit 3 (`0x0008`) - View Button + - Bit 4 (`0x0010`) - A Button/Green Pad + - Bit 5 (`0x0020`) - B Button/Red Pad + - Bit 6 (`0x0040`) - X Button + - Bit 7 (`0x0080`) - Y Button - Bit 8 (`0x0100`) - D-pad Up - Bit 9 (`0x0200`) - D-pad Down - Bit 10 (`0x0400`) - D-pad Left From 97afd68d91443e1c7af86294b8179c2830e54a3a Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 25 Nov 2022 17:40:35 -0700 Subject: [PATCH 087/437] Emulate the d-pad inputs that RB2/3 kits' yellow/blue cymbals trigger in the ViGEm mapper --- Program/PacketParsing/VigemMapper.cs | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index cf9125d..459b60d 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -156,6 +156,15 @@ private void ParseGuitar(GuitarInput report) device.SetSliderValue(Xbox360Slider.LeftTrigger, report.PickupSwitch); } + // Constants for masks below + const int yellowBit = 0x01; + const int blueBit = 0x02; + + // The previous state of the yellow/blue cymbals + int previousDpadCymbals; + // The current state of the d-pad mask from the hit yellow/blue cymbals + int dpadMask; + /// /// Parses drums input data from an input report. /// @@ -180,6 +189,40 @@ private void ParseDrums(DrumInput report) byte blueCym = report.BlueCymbal; byte greenCym = report.GreenCymbal; + // Yellow and blue cymbal trigger d-pad up and down respectively on the RB2/3 kit we're emulating + // However, they only trigger one or the other, not both at the same time, so we need to mimic that + int cymbalMask = (yellowCym != 0 ? yellowBit : 0) | (blueCym != 0 ? blueBit : 0); + if (cymbalMask != previousDpadCymbals) + { + if (cymbalMask == 0) + dpadMask = 0; + + // This could probably be done more simply, but this works + if (dpadMask != 0) + { + // D-pad is already set + // Only remove the set value + if ((cymbalMask & yellowBit) == 0) + dpadMask &= ~yellowBit; + else if ((cymbalMask & blueBit) == 0) + dpadMask &= ~blueBit; + } + else + { + // D-pad is not set + // If both cymbals are hit at the same time, yellow takes priority + if ((cymbalMask & yellowBit) != 0) + dpadMask |= yellowBit; + else if ((cymbalMask & blueBit) != 0) + dpadMask |= blueBit; + } + + previousDpadCymbals = cymbalMask; + } + + device.SetButtonState(Xbox360Button.Up, ((dpadMask & yellowBit) != 0) || ((buttons & GamepadButton.DpadUp) != 0)); + device.SetButtonState(Xbox360Button.Down, ((dpadMask & blueBit) != 0) || ((buttons & GamepadButton.DpadDown) != 0)); + // Color flags device.SetButtonState(Xbox360Button.B, (redPad) != 0); device.SetButtonState(Xbox360Button.Y, (yellowPad | yellowCym) != 0); @@ -193,6 +236,9 @@ private void ParseDrums(DrumInput report) device.SetButtonState(Xbox360Button.RightShoulder, (yellowCym | blueCym | greenCym) != 0); + device.SetButtonState(Xbox360Button.Up, (buttons & GamepadButton.DpadUp) != 0); + device.SetButtonState(Xbox360Button.Down, (buttons & GamepadButton.DpadDown) != 0); + // Pedals device.SetButtonState(Xbox360Button.LeftShoulder, (report.Buttons & (ushort)DrumInput.Button.KickOne) != 0); From 2ad86d4fd2948b1d69ec7e288acc9a1b5906c87f Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 25 Nov 2022 17:45:52 -0700 Subject: [PATCH 088/437] Allow the d-pad direction to change if hitting both Y+B cymbal and Y cymbal releases first --- Program/PacketParsing/VigemMapper.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 459b60d..d2ef85d 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -207,7 +207,10 @@ private void ParseDrums(DrumInput report) else if ((cymbalMask & blueBit) == 0) dpadMask &= ~blueBit; } - else + + // Explicitly check this so that if the d-pad is cleared but the other cymbal is still active, + // it will get set to that cymbal's d-pad + if (dpadMask == 0) { // D-pad is not set // If both cymbals are hit at the same time, yellow takes priority From e263ddcbc804b0aa1bfbd1904d7c6b5f051eb757 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 26 Nov 2022 15:15:09 -0700 Subject: [PATCH 089/437] Don't set ViGEm d-pad state twice in drums path --- Program/PacketParsing/VigemMapper.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index d2ef85d..6edb121 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -239,9 +239,6 @@ private void ParseDrums(DrumInput report) device.SetButtonState(Xbox360Button.RightShoulder, (yellowCym | blueCym | greenCym) != 0); - device.SetButtonState(Xbox360Button.Up, (buttons & GamepadButton.DpadUp) != 0); - device.SetButtonState(Xbox360Button.Down, (buttons & GamepadButton.DpadDown) != 0); - // Pedals device.SetButtonState(Xbox360Button.LeftShoulder, (report.Buttons & (ushort)DrumInput.Button.KickOne) != 0); From e943d48a48fee07ccee17a7b89ab40227fd6281b Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 26 Nov 2022 15:19:11 -0700 Subject: [PATCH 090/437] Don't set ABXY buttons twice either --- Program/PacketParsing/VigemMapper.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 6edb121..942fbe7 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -227,10 +227,10 @@ private void ParseDrums(DrumInput report) device.SetButtonState(Xbox360Button.Down, ((dpadMask & blueBit) != 0) || ((buttons & GamepadButton.DpadDown) != 0)); // Color flags - device.SetButtonState(Xbox360Button.B, (redPad) != 0); - device.SetButtonState(Xbox360Button.Y, (yellowPad | yellowCym) != 0); - device.SetButtonState(Xbox360Button.X, (bluePad | blueCym) != 0); - device.SetButtonState(Xbox360Button.A, (greenPad | greenCym) != 0); + device.SetButtonState(Xbox360Button.B, (redPad != 0) || ((buttons & GamepadButton.A) != 0)); + device.SetButtonState(Xbox360Button.Y, ((yellowPad | yellowCym) != 0) || ((buttons & GamepadButton.B) != 0)); + device.SetButtonState(Xbox360Button.X, ((bluePad | blueCym) != 0) || ((buttons & GamepadButton.X) != 0)); + device.SetButtonState(Xbox360Button.A, ((greenPad | greenCym) != 0) || ((buttons & GamepadButton.Y) != 0)); // Pad flag device.SetButtonState(Xbox360Button.RightThumb, From d2be025900e813bd9a6b045c20427e7e4a8d5013 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 26 Nov 2022 15:20:40 -0700 Subject: [PATCH 091/437] let's actually get the full change in here --- Program/PacketParsing/VigemMapper.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 942fbe7..19c153b 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -174,11 +174,6 @@ private void ParseDrums(DrumInput report) var buttons = (GamepadButton)report.Buttons; ParseCoreButtons(buttons); - device.SetButtonState(Xbox360Button.A, (buttons & GamepadButton.A) != 0); - device.SetButtonState(Xbox360Button.B, (buttons & GamepadButton.B) != 0); - device.SetButtonState(Xbox360Button.X, (buttons & GamepadButton.X) != 0); - device.SetButtonState(Xbox360Button.Y, (buttons & GamepadButton.Y) != 0); - // Pads and cymbals byte redPad = report.RedPad; byte yellowPad = report.YellowPad; From ddbbab1f4741468e05b04661971e32e9916fe066 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 26 Nov 2022 15:54:53 -0700 Subject: [PATCH 092/437] Fix wrong face buttons being tested for when setting ABXY --- Program/PacketParsing/VigemMapper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 19c153b..8f485da 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -222,10 +222,10 @@ private void ParseDrums(DrumInput report) device.SetButtonState(Xbox360Button.Down, ((dpadMask & blueBit) != 0) || ((buttons & GamepadButton.DpadDown) != 0)); // Color flags - device.SetButtonState(Xbox360Button.B, (redPad != 0) || ((buttons & GamepadButton.A) != 0)); - device.SetButtonState(Xbox360Button.Y, ((yellowPad | yellowCym) != 0) || ((buttons & GamepadButton.B) != 0)); + device.SetButtonState(Xbox360Button.B, (redPad != 0) || ((buttons & GamepadButton.B) != 0)); + device.SetButtonState(Xbox360Button.Y, ((yellowPad | yellowCym) != 0) || ((buttons & GamepadButton.Y) != 0)); device.SetButtonState(Xbox360Button.X, ((bluePad | blueCym) != 0) || ((buttons & GamepadButton.X) != 0)); - device.SetButtonState(Xbox360Button.A, ((greenPad | greenCym) != 0) || ((buttons & GamepadButton.Y) != 0)); + device.SetButtonState(Xbox360Button.A, ((greenPad | greenCym) != 0) || ((buttons & GamepadButton.A) != 0)); // Pad flag device.SetButtonState(Xbox360Button.RightThumb, From 24d14f9d3bc5e86e461cf9f9520b313aa693e159 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 26 Nov 2022 15:55:25 -0700 Subject: [PATCH 093/437] Bump version to v2.1.1 --- Installer/Product.wxs | 2 +- Program/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 89f0df4..a0bda12 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -4,7 +4,7 @@ Id="*" Name="RB4InstrumentMapper" Language="1033" - Version="2.1.0.0" + Version="2.1.1.0" Manufacturer="Andreas Schiffler" UpgradeCode="94bef546-701f-4571-9828-d4fa39b2ea84"> diff --git a/Program/Properties/AssemblyInfo.cs b/Program/Properties/AssemblyInfo.cs index 0d2d445..8f82295 100644 --- a/Program/Properties/AssemblyInfo.cs +++ b/Program/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.1.0.0")] -[assembly: AssemblyFileVersion("2.1.0.0")] +[assembly: AssemblyVersion("2.1.1.0")] +[assembly: AssemblyFileVersion("2.1.1.0")] From b585239637e2acde6e3c791c29b460d547fdf95a Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 13 Dec 2022 22:33:43 -0700 Subject: [PATCH 094/437] Update readme - Update PDP drumkit connection steps with more info and to imply usability for all devices - Update note about Npcap not working - Don't specify device numbers for vJoy devices - Move vJoy config screenshot - Add suggestion to make sure to use a USB 2.0 port instead of a 3.0 port - Other minor tweaks --- README.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1bf3ce1..543c4f0 100644 --- a/README.md +++ b/README.md @@ -20,35 +20,42 @@ Jaguar guitars require a firmware update in order to connect to the receiver. - [Instructions](https://bit.ly/2UHzonU) - [Firmware installer backup](https://drive.google.com/file/d/1DQxkkbBfi-UOqdX6vp5TaX6F2N2OBDra/view?usp=drivesdk) -RB4 drums made by PDP don't seem to work, they turn off after syncing to the receiver. You may be able to get them to connect with these steps: +Some guitars/drumkits might not sync properly when using just the sync button. This includes the PDP drumkit. Follow these steps to sync your device correctly: -- Go to Windows settings > `Devices` > `Bluetooth & other devices` -- Click `Add Bluetooth or other device` and pick the `Everything else` option. +1. Go to Windows settings > Devices > Bluetooth & other devices +2. Click `Add Bluetooth or other device` and pick the `Everything else` option. +3. Press and hold the sync button until the Xbox button light flashes quickly. +4. Select `Xbox compatible game controller` from the list once it appears. +5. If that doesn't work, restart your PC and try again. ## Installation 1. Install [WinPCap](https://www.winpcap.org/install/default.htm). - - Do not install Npcap, as it doesn't seem to work with Xbox One receivers. + - While Npcap is newer, it does not seem to work with Xbox One receivers currently, and trying it isn't recommended unless WinPcap doesn't work and none of the other troubleshooting helps. 2. Install [USBPCap](https://desowin.org/usbpcap/). 3. Install [ViGEmBus](https://github.com/ViGEm/ViGEmBus/releases/latest) (recommended) or [vJoy](https://github.com/jshafer817/vJoy/releases/latest). - If you installed vJoy, configure it: 1. Open your Start menu, find the `vJoy` folder, and open the `Configure vJoy` program inside it. - 2. Configure devices 1, 2, and 3 with these settings: + 2. Configure one device for each one of your guitars/drumkits, using these settings: - Number of Buttons: 16 - POV Hat Switch: Continuous, POVs: 1 - Axes: `X`, `Y`, `Z` - 3. Click Apply.\ - ![vJoy Configuration Screenshot](/Docs/Images/vJoyConfiguration.png "vJoy Configuration Screenshot") + + ![vJoy Configuration Screenshot](/Docs/Images/vJoyConfiguration.png "vJoy Configuration Screenshot") + + 3. Click Apply. - If you installed ViGEmBus, there's no configuration required. Outputs for guitars and drums will match that of their Xbox 360 counterparts. 4. Restart your PC. ## Usage -1. Configure the selected Pcap device: +1. Select your Xbox One receiver from the dropdown menu. - Xbox receivers should be detected automatically by device name. - If they are not, click the `Auto-Detect Pcap` button and follow its instructions. - You can also check the device list yourself for a device called `MT7612US_RL`. - - If that doesn't work, then if you installed WinPcap, try installing Npcap instead, or vice versa. + - If that doesn't work: + - Make sure you are not using a USB 3.0 port, as those may cause the receiver to not show up for some reason. + - If you installed WinPcap, try installing Npcap instead, or vice versa. 2. Select either vJoy or ViGEmBus in the Controller Type dropdown. 3. Connect your instruments if you haven't yet. 4. Click the Start button. Devices will be detected automatically. From aad6fb81efef606aebc0983daabe0c856583826b Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 1 Mar 2023 05:15:00 -0700 Subject: [PATCH 095/437] Remove processed packets count It's a bit more confusing than it is helpful for some --- Program/MainWindow/MainWindow.xaml | 2 -- Program/MainWindow/MainWindow.xaml.cs | 32 +-------------------------- Program/PacketParsing/PacketParser.cs | 3 +-- 3 files changed, 2 insertions(+), 35 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml b/Program/MainWindow/MainWindow.xaml index 8ac3dc1..f42b1af 100644 --- a/Program/MainWindow/MainWindow.xaml +++ b/Program/MainWindow/MainWindow.xaml @@ -24,8 +24,6 @@ - private static readonly vJoy vjoy = new vJoy(); - /// - /// Counter for processed packets. - /// - private ulong processedPacketCount = 0; - private enum ControllerType { None = -1, @@ -403,12 +398,6 @@ private void StartCapture() startButton.Content = "Stop"; - // Enable packet count display - packetsProcessedLabel.Visibility = Visibility.Visible; - packetsProcessedCountLabel.Visibility = Visibility.Visible; - packetsProcessedCountLabel.Content = "0"; - processedPacketCount = 0; - // Initialize packet log if (packetDebugLog) { @@ -472,12 +461,6 @@ private void StopCapture() startButton.Content = "Start"; - // Disable packet count display - packetsProcessedLabel.Visibility = Visibility.Hidden; - packetsProcessedCountLabel.Visibility = Visibility.Hidden; - packetsProcessedCountLabel.Content = string.Empty; - processedPacketCount = 0; - // Force a refresh of the controller textbox controllerDeviceTypeCombo_SelectionChanged(null, null); @@ -496,7 +479,7 @@ private void OnPacketArrival(object sender, PacketCapture packet) { try { - PacketParser.HandlePcapPacket(packet.Data, ref processedPacketCount); + PacketParser.HandlePcapPacket(packet.Data); } catch (ThreadAbortException) { @@ -521,19 +504,6 @@ private void OnPacketArrival(object sender, PacketCapture packet) Console.WriteLine(packetLogString); Logging.Packet_WriteLine(packetLogString); } - - // Status reporting (slow) - if ((processedPacketCount < 10) || - ((processedPacketCount < 100) && (processedPacketCount % 10 == 0)) || - (processedPacketCount % 100 == 0)) - { - // Update UI - uiDispatcher.Invoke(() => - { - string ulongString = processedPacketCount.ToString("N0"); - packetsProcessedCountLabel.Content = ulongString; - }); - } } /// diff --git a/Program/PacketParsing/PacketParser.cs b/Program/PacketParsing/PacketParser.cs index def4859..c87f7f9 100644 --- a/Program/PacketParsing/PacketParser.cs +++ b/Program/PacketParsing/PacketParser.cs @@ -39,7 +39,7 @@ public static class PacketParser /// /// Handles a received Pcap packet. /// - public static unsafe void HandlePcapPacket(ReadOnlySpan data, ref ulong processedCount) + public static unsafe void HandlePcapPacket(ReadOnlySpan data) { // Packet must be at least 30 bytes long, to contain both the receiver and command headers if (data.Length < (sizeof(ReceiverHeader) + sizeof(CommandHeader)) @@ -76,7 +76,6 @@ public static unsafe void HandlePcapPacket(ReadOnlySpan data, ref ulong pr // Strip off receiver header and send the data to be parsed pcapIds[deviceId].ParseCommand(data.Slice(sizeof(ReceiverHeader))); - processedCount++; } // TODO: Add libusb support From c8d7e8b63ae70f3eb987ae2a4d7784a5a9722d86 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 1 Mar 2023 05:42:34 -0700 Subject: [PATCH 096/437] Whoops, these need to be swapped around --- Program/PacketParsing/VigemMapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 8f485da..85e0846 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -90,7 +90,7 @@ public unsafe void ParseInput(CommandHeader header, ReadOnlySpan data) return; } - header.SequenceCount = prevInputSeqCount; + prevInputSeqCount = header.SequenceCount; int length = header.DataLength; if (length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) From 7373147ddf40b5c53a62c8825704bcb4e50287c2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 1 Mar 2023 05:19:55 -0700 Subject: [PATCH 097/437] Move PacketParser to the .Parsing namespace --- Program/MainWindow/MainWindow.xaml.cs | 3 ++- Program/PacketParsing/PacketParser.cs | 5 +---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 8d87420..5160499 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -22,6 +22,7 @@ using Nefarius.ViGEm.Client.Exceptions; using SharpPcap; using SharpPcap.LibPcap; +using RB4InstrumentMapper.Parsing; namespace RB4InstrumentMapper { diff --git a/Program/PacketParsing/PacketParser.cs b/Program/PacketParsing/PacketParser.cs index c87f7f9..6c6e8b6 100644 --- a/Program/PacketParsing/PacketParser.cs +++ b/Program/PacketParsing/PacketParser.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; -using RB4InstrumentMapper.Parsing; -// This is in the regular namespace to keep the other packet parsing stuff from bogging up -// auto-completions when the code in this file is the only code that needs to be referenced elsewhere -namespace RB4InstrumentMapper +namespace RB4InstrumentMapper.Parsing { /// /// Emulated devices that can be parsed to. From 2b65c8786380a2959306a10507b43f4b45312e8a Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 1 Mar 2023 05:31:11 -0700 Subject: [PATCH 098/437] Separate out Pcap handling code --- Program/MainWindow/MainWindow.xaml.cs | 61 +----------- Program/PacketParsing/Backends/PcapBackend.cs | 93 +++++++++++++++++++ Program/PacketParsing/PacketParser.cs | 14 +-- Program/RB4InstrumentMapper.csproj | 1 + 4 files changed, 100 insertions(+), 69 deletions(-) create mode 100644 Program/PacketParsing/Backends/PcapBackend.cs diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 5160499..27553c6 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -409,22 +409,9 @@ private void StartCapture() } } - // Register handler for packet debugging/logging and status reporting - pcapSelectedDevice.OnPacketArrival += OnPacketArrival; - - // Open the device - pcapSelectedDevice.Open(new DeviceConfiguration() - { - Snaplen = 45, // Capture small packets - Mode = DeviceModes.Promiscuous | DeviceModes.MaxResponsiveness, // Promiscuous mode with maximum speed - ReadTimeout = 50 // Read timeout - }); - - // Configure packet receive event handler - pcapSelectedDevice.OnPacketArrival += OnPacketArrival; - // Start capture - pcapSelectedDevice.StartCapture(); + PcapBackend.LogPackets = packetDebug; + PcapBackend.StartCapture(pcapSelectedDevice); Console.WriteLine($"Listening on {pcapSelectedDevice.Description}..."); } @@ -433,14 +420,7 @@ private void StartCapture() /// private void StopCapture() { - // Stop packet capture - if (pcapSelectedDevice != null) - { - // Close will automatically remove assigned event handlers and call StopCapture(), - // so we don't need to do those ourselves - pcapSelectedDevice.Close(); - } - + PcapBackend.StopCapture(); PacketParser.Close(); // Store whether or not the packet log was created @@ -472,41 +452,6 @@ private void StopCapture() } } - /// - /// Handles captured packets. - /// - /// The received packet - private void OnPacketArrival(object sender, PacketCapture packet) - { - try - { - PacketParser.HandlePcapPacket(packet.Data); - } - catch (ThreadAbortException) - { - // Don't log ThreadAbortExceptions, just return - return; - } - catch (Exception ex) - { - Console.WriteLine($"Error while handling packet: {ex.GetFirstLine()}"); - Logging.Main_WriteException(ex, "Context: Unhandled error during packet handling"); - - // Stop capture - uiDispatcher.Invoke(StopCapture); - return; - } - - // Debugging (if enabled) - if (packetDebug) - { - RawCapture raw = packet.GetPacket(); - string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; - Console.WriteLine(packetLogString); - Logging.Packet_WriteLine(packetLogString); - } - } - /// /// Handles Pcap device selection changes. /// diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs new file mode 100644 index 0000000..ab85239 --- /dev/null +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -0,0 +1,93 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using SharpPcap; + +namespace RB4InstrumentMapper.Parsing +{ + public delegate void PacketReceivedHandler(DateTime timestamp, ReadOnlySpan data); + + public static class PcapBackend + { + public static bool LogPackets = false; + private static ILiveDevice captureDevice = null; + + public static event Action OnCaptureStop; + + /// + /// Starts capturing packets from the given device. + /// + public static void StartCapture(ILiveDevice device) + { + // Open the device + device.Open(new DeviceConfiguration() + { + Snaplen = 45, // Capture small packets + Mode = DeviceModes.Promiscuous | DeviceModes.MaxResponsiveness, // Promiscuous mode with maximum speed + ReadTimeout = 50 // Read timeout + }); + + // Configure packet receive event handler + device.OnPacketArrival += OnPacketArrival; + + // Start capture + device.StartCapture(); + captureDevice = device; + } + + /// + /// Stops capturing packets. + /// + public static void StopCapture() + { + if (captureDevice != null) + { + captureDevice.OnPacketArrival -= OnPacketArrival; + captureDevice.Close(); + captureDevice = null; + } + } + + /// + /// Handles captured packets. + /// + private static unsafe void OnPacketArrival(object sender, PacketCapture packet) + { + // Read out receiver header + var data = packet.Data; + if (data.Length < sizeof(ReceiverHeader) || !MemoryMarshal.TryRead(data, out ReceiverHeader header)) + { + return; + } + data = data.Slice(sizeof(ReceiverHeader)); + + try + { + PacketParser.HandlePacket(header.DeviceId, data); + } + catch (ThreadAbortException) + { + // Don't log thread aborts, just return + return; + } + catch (Exception ex) + { + Console.WriteLine($"Error while handling packet: {ex.GetFirstLine()}"); + Logging.Main_WriteException(ex, "Context: Unhandled error during packet handling"); + + // Stop capture + OnCaptureStop.Invoke(); + return; + } + + // Debugging (if enabled) + if (LogPackets) + { + RawCapture raw = packet.GetPacket(); + string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; + Console.WriteLine(packetLogString); + Logging.Packet_WriteLine(packetLogString); + } + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/PacketParser.cs b/Program/PacketParsing/PacketParser.cs index 6c6e8b6..17d4bd9 100644 --- a/Program/PacketParsing/PacketParser.cs +++ b/Program/PacketParsing/PacketParser.cs @@ -36,17 +36,9 @@ public static class PacketParser /// /// Handles a received Pcap packet. /// - public static unsafe void HandlePcapPacket(ReadOnlySpan data) + public static unsafe void HandlePacket(ulong deviceId, ReadOnlySpan data) { - // Packet must be at least 30 bytes long, to contain both the receiver and command headers - if (data.Length < (sizeof(ReceiverHeader) + sizeof(CommandHeader)) - || !MemoryMarshal.TryRead(data, out ReceiverHeader header)) - { - return; - } - // Check if device ID has been encountered yet - ulong deviceId = header.DeviceId; if (!pcapIds.ContainsKey(deviceId)) { if (!canHandleNewDevices) @@ -71,8 +63,8 @@ public static unsafe void HandlePcapPacket(ReadOnlySpan data) Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); } - // Strip off receiver header and send the data to be parsed - pcapIds[deviceId].ParseCommand(data.Slice(sizeof(ReceiverHeader))); + // Parse data + pcapIds[deviceId].ParseCommand(data); } // TODO: Add libusb support diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index e96473d..8291cfa 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -107,6 +107,7 @@ Designer + From b431e7bd917307d05b32c85106cc1e3fbfc23043 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 1 Mar 2023 11:02:54 -0700 Subject: [PATCH 099/437] Upgrade VigemStatic and VjoyStatic into proper singleton client wrappers --- Program/MainWindow/MainWindow.xaml.cs | 49 +++----- Program/PacketParsing/VigemMapper.cs | 3 +- Program/PacketParsing/VigemStatic.cs | 38 ------- Program/PacketParsing/VjoyMapper.cs | 93 +++++++++------- Program/PacketParsing/VjoyStatic.cs | 154 -------------------------- Program/RB4InstrumentMapper.csproj | 8 +- Program/Vigem/VigemClient.cs | 60 ++++++++++ Program/Vjoy/VjoyClient.cs | 98 ++++++++++++++++ Program/Vjoy/VjoyDefinitions.cs | 44 ++++++++ Program/Vjoy/VjoyExtensions.cs | 20 ++++ 10 files changed, 295 insertions(+), 272 deletions(-) delete mode 100644 Program/PacketParsing/VigemStatic.cs delete mode 100644 Program/PacketParsing/VjoyStatic.cs create mode 100644 Program/Vigem/VigemClient.cs create mode 100644 Program/Vjoy/VjoyClient.cs create mode 100644 Program/Vjoy/VjoyDefinitions.cs create mode 100644 Program/Vjoy/VjoyExtensions.cs diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 27553c6..576ff75 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -15,14 +15,13 @@ using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; -using vJoyInterfaceWrap; using System.Runtime.InteropServices; using System.Windows.Threading; -using Nefarius.ViGEm.Client; -using Nefarius.ViGEm.Client.Exceptions; using SharpPcap; using SharpPcap.LibPcap; using RB4InstrumentMapper.Parsing; +using RB4InstrumentMapper.Vjoy; +using RB4InstrumentMapper.Vigem; namespace RB4InstrumentMapper { @@ -66,11 +65,6 @@ public partial class MainWindow : Window /// private const string pcapComboBoxItemName = "pcapDeviceComboBoxItem"; - /// - /// vJoy client. - /// - private static readonly vJoy vjoy = new vJoy(); - private enum ControllerType { None = -1, @@ -114,11 +108,11 @@ private void Window_Loaded(object sender, RoutedEventArgs e) int deviceType = controllerDeviceTypeCombo.SelectedIndex = Properties.Settings.Default.controllerDeviceType; // Check for vJoy - bool vjoyFound = vjoy.vJoyEnabled(); + bool vjoyFound = VjoyClient.Enabled; if (vjoyFound) { // Log vJoy driver attributes (Vendor Name, Product Name, Version Number) - Console.WriteLine("vJoy found! - Vendor: " + vjoy.GetvJoyManufacturerString() + ", Product: " + vjoy.GetvJoyProductString() + ", Version Number: " + vjoy.GetvJoySerialNumberString()); + Console.WriteLine($"vJoy found! - Vendor: {VjoyClient.Manufacturer}, Product: {VjoyClient.Product}, Version Number: {VjoyClient.SerialNumber}"); if (CountAvailableVjoyDevices() > 0) { (controllerDeviceTypeCombo.Items[0] as ComboBoxItem).IsEnabled = true; @@ -144,16 +138,13 @@ private void Window_Loaded(object sender, RoutedEventArgs e) } // Check for ViGEmBus - bool vigemFound; - try + bool vigemFound = VigemClient.TryInitialize(); + if (vigemFound) { - var vigem = new ViGEmClient(); - vigemFound = true; Console.WriteLine("ViGEmBus found!"); - vigem.Dispose(); (controllerDeviceTypeCombo.Items[1] as ComboBoxItem).IsEnabled = true; } - catch (VigemBusNotFoundException) + else { vigemFound = false; Console.WriteLine("ViGEmBus not found. ViGEmBus selection will be unavailable."); @@ -192,6 +183,9 @@ private void Window_Closed(object sender, EventArgs e) // Close the log files Logging.CloseAll(); + + // Dispose ViGEmBus + VigemClient.Dispose(); } /// @@ -287,7 +281,7 @@ private void PopulatePcapDropdown() /// private int CountAvailableVjoyDevices() { - if (!vjoy.vJoyEnabled()) + if (!VjoyClient.Enabled) { return 0; } @@ -296,24 +290,9 @@ private int CountAvailableVjoyDevices() int freeDeviceCount = 0; for (uint id = 1; id <= 16; id++) { - if (vjoy.GetVJDStatus(id) == VjdStat.VJD_STAT_FREE) + if (VjoyClient.IsDeviceAvailable(id)) { - // Check that the vJoy device is configured correctly - int numButtons = vjoy.GetVJDButtonNumber(id); - int numContPov = vjoy.GetVJDContPovNumber(id); - bool xExists = vjoy.GetVJDAxisExist(id, HID_USAGES.HID_USAGE_X); // X axis - bool yExists = vjoy.GetVJDAxisExist(id, HID_USAGES.HID_USAGE_Y); // Y axis - bool zExists = vjoy.GetVJDAxisExist(id, HID_USAGES.HID_USAGE_Z); // Z axis - - if (numButtons >= 16 && - numContPov >= 1 && - xExists && - yExists && - zExists - ) - { - freeDeviceCount++; - } + freeDeviceCount++; } } diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 85e0846..d57e528 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -4,6 +4,7 @@ using Nefarius.ViGEm.Client.Exceptions; using Nefarius.ViGEm.Client.Targets; using Nefarius.ViGEm.Client.Targets.Xbox360; +using RB4InstrumentMapper.Vigem; namespace RB4InstrumentMapper.Parsing { @@ -26,7 +27,7 @@ class VigemMapper : IDeviceMapper /// public VigemMapper() { - device = VigemStatic.CreateDevice(); + device = VigemClient.CreateDevice(); device.FeedbackReceived += ReceiveUserIndex; try diff --git a/Program/PacketParsing/VigemStatic.cs b/Program/PacketParsing/VigemStatic.cs deleted file mode 100644 index 4278335..0000000 --- a/Program/PacketParsing/VigemStatic.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Nefarius.ViGEm.Client; -using Nefarius.ViGEm.Client.Targets; - -namespace RB4InstrumentMapper.Parsing -{ - /// - /// Static vJoy client. - /// - static class VigemStatic - { - /// - /// Static ViGEmBus client. - /// - private static ViGEmClient client = null; - - /// - /// Whether or not the ViGEmBus client has been initialized. - /// - public static bool Initialized - { - get => client != null; - } - - static VigemStatic() - { - client = new ViGEmClient(); - } - - /// - /// Creates a new Xbox 360 device with the Xbox 360 Rock Band wireless instrument vendor/product IDs. - /// - public static IXbox360Controller CreateDevice() => client.CreateXbox360Controller(0x1BAD, 0x0719); - // Rock Band Guitar: USB\VID_1BAD&PID_0719&IG_00 XUSB\TYPE_00\SUB_86\VEN_1BAD\REV_0002 - // Rock Band Drums: USB\VID_1BAD&PID_0719&IG_02 XUSB\TYPE_00\SUB_88\VEN_1BAD\REV_0002 - // If subtype ID specification through ViGEmBus becomes possible at some point, - // the guitar should be subtype 6, and the drums should be subtype 8 - } -} \ No newline at end of file diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs index e0928b5..4aaac59 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/VjoyMapper.cs @@ -2,10 +2,9 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using RB4InstrumentMapper.Vjoy; using vJoyInterfaceWrap; -using Button = RB4InstrumentMapper.Parsing.VjoyStatic.Button; - namespace RB4InstrumentMapper.Parsing { class VjoyMapper : IDeviceMapper @@ -20,7 +19,17 @@ class VjoyMapper : IDeviceMapper /// public VjoyMapper() { - deviceId = VjoyStatic.ClaimNextAvailableDevice(); + deviceId = VjoyClient.GetNextAvailableID(); + if (deviceId == 0) + { + throw new ParseException("No new vJoy devices are available."); + } + + if (!VjoyClient.AcquireDevice(deviceId)) + { + throw new ParseException($"Could not claim vJoy device {deviceId}."); + } + state.bDevice = (byte)deviceId; Console.WriteLine($"Acquired vJoy device with ID of {deviceId}"); } @@ -70,22 +79,22 @@ public unsafe void ParseInput(CommandHeader header, ReadOnlySpan data) } // Send data - VjoyStatic.Client.UpdateVJD(deviceId, ref state); + VjoyClient.UpdateDevice(deviceId, ref state); } /// /// Sets the state of the specified button. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SetButton(uint button, bool condition) + protected void SetButton(VjoyButton button, bool set) { - if (condition) + if (set) { - state.Buttons |= button; + state.Buttons |= (uint)button; } else { - state.Buttons &= ~button; + state.Buttons &= (uint)~button; } } @@ -95,58 +104,60 @@ private void SetButton(uint button, bool condition) private void ParseCoreButtons(GamepadButton buttons) { // Menu - SetButton(Button.Fifteen, (buttons & GamepadButton.Menu) != 0); + SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); // Options - SetButton(Button.Sixteen, (buttons & GamepadButton.Options) != 0); + SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); - // D-pad to POV + VjoyPoV direction; if ((buttons & GamepadButton.DpadUp) != 0) { if ((buttons & GamepadButton.DpadLeft) != 0) { - state.bHats = VjoyStatic.PoV.UpLeft; + direction = VjoyPoV.UpLeft; } else if ((buttons & GamepadButton.DpadRight) != 0) { - state.bHats = VjoyStatic.PoV.UpRight; + direction = VjoyPoV.UpRight; } else { - state.bHats = VjoyStatic.PoV.Up; + direction = VjoyPoV.Up; } } else if ((buttons & GamepadButton.DpadDown) != 0) { if ((buttons & GamepadButton.DpadLeft) != 0) { - state.bHats = VjoyStatic.PoV.DownLeft; + direction = VjoyPoV.DownLeft; } else if ((buttons & GamepadButton.DpadRight) != 0) { - state.bHats = VjoyStatic.PoV.DownRight; + direction = VjoyPoV.DownRight; } else { - state.bHats = VjoyStatic.PoV.Down; + direction = VjoyPoV.Down; } } else { if ((buttons & GamepadButton.DpadLeft) != 0) { - state.bHats = VjoyStatic.PoV.Left; + direction = VjoyPoV.Left; } else if ((buttons & GamepadButton.DpadRight) != 0) { - state.bHats = VjoyStatic.PoV.Right; + direction = VjoyPoV.Right; } else { - state.bHats = VjoyStatic.PoV.Neutral; + direction = VjoyPoV.Neutral; } } + state.bHats = (uint)direction; + // Other buttons are not mapped here since they may have specific uses } @@ -158,11 +169,11 @@ private void ParseGuitar(GuitarInput report) // Buttons ParseCoreButtons((GamepadButton)report.Buttons); - SetButton(Button.One, report.Green); - SetButton(Button.Two, report.Red); - SetButton(Button.Three, report.Yellow); - SetButton(Button.Four, report.Blue); - SetButton(Button.Five, report.Orange); + SetButton(VjoyButton.One, report.Green); + SetButton(VjoyButton.Two, report.Red); + SetButton(VjoyButton.Three, report.Yellow); + SetButton(VjoyButton.Four, report.Blue); + SetButton(VjoyButton.Five, report.Orange); // Whammy // Value ranges from 0 (not pressed) to 255 (fully pressed) @@ -189,25 +200,25 @@ private void ParseDrums(DrumInput report) ParseCoreButtons(buttons); // Face buttons - SetButton(Button.Four, (buttons & GamepadButton.A) != 0); - SetButton(Button.One, (buttons & GamepadButton.B) != 0); - SetButton(Button.Three, (buttons & GamepadButton.X) != 0); - SetButton(Button.Two, (buttons & GamepadButton.Y) != 0); + SetButton(VjoyButton.Four, (buttons & GamepadButton.A) != 0); + SetButton(VjoyButton.One, (buttons & GamepadButton.B) != 0); + SetButton(VjoyButton.Three, (buttons & GamepadButton.X) != 0); + SetButton(VjoyButton.Two, (buttons & GamepadButton.Y) != 0); // Pads - SetButton(Button.One, report.RedPad != 0); - SetButton(Button.Two, report.YellowPad != 0); - SetButton(Button.Three, report.BluePad != 0); - SetButton(Button.Four, report.GreenPad != 0); + SetButton(VjoyButton.One, report.RedPad != 0); + SetButton(VjoyButton.Two, report.YellowPad != 0); + SetButton(VjoyButton.Three, report.BluePad != 0); + SetButton(VjoyButton.Four, report.GreenPad != 0); // Cymbals - SetButton(Button.Six, report.YellowCymbal != 0); - SetButton(Button.Seven, report.BlueCymbal != 0); - SetButton(Button.Eight, report.GreenCymbal != 0); + SetButton(VjoyButton.Six, report.YellowCymbal != 0); + SetButton(VjoyButton.Seven, report.BlueCymbal != 0); + SetButton(VjoyButton.Eight, report.GreenCymbal != 0); // Kick pedals - SetButton(Button.Five, (report.Buttons & (ushort)DrumInput.Button.KickOne) != 0); - SetButton(Button.Nine, (report.Buttons & (ushort)DrumInput.Button.KickTwo) != 0); + SetButton(VjoyButton.Five, (report.Buttons & (ushort)DrumInput.Button.KickOne) != 0); + SetButton(VjoyButton.Nine, (report.Buttons & (ushort)DrumInput.Button.KickTwo) != 0); } /// @@ -216,11 +227,11 @@ private void ParseDrums(DrumInput report) public void Close() { // Reset report - state.ResetState(); - VjoyStatic.Client.UpdateVJD(deviceId, ref state); + state.Reset(); + VjoyClient.UpdateDevice(deviceId, ref state); // Free device - VjoyStatic.ReleaseDevice(deviceId); + VjoyClient.ReleaseDevice(deviceId); } } } diff --git a/Program/PacketParsing/VjoyStatic.cs b/Program/PacketParsing/VjoyStatic.cs deleted file mode 100644 index ee4fa89..0000000 --- a/Program/PacketParsing/VjoyStatic.cs +++ /dev/null @@ -1,154 +0,0 @@ -using vJoyInterfaceWrap; - -namespace RB4InstrumentMapper.Parsing -{ - /// - /// Static vJoy client. - /// - static class VjoyStatic - { - /// - /// Static vJoy client. - /// - private static vJoy client = new vJoy(); - - /// - /// Gets the vJoy client. - /// - public static vJoy Client - { - get => client; - } - - /// - /// Gets the next available device ID. - /// - public static uint GetNextAvailableID() - { - // Get available devices - for (uint deviceId = 1; deviceId <= 16; deviceId++) - { - // Ensure device is available - if (client.GetVJDStatus(deviceId) == VjdStat.VJD_STAT_FREE) - { - // Check that the vJoy device is configured correctly - int numButtons = client.GetVJDButtonNumber(deviceId); - int numContPov = client.GetVJDContPovNumber(deviceId); - bool xExists = client.GetVJDAxisExist(deviceId, HID_USAGES.HID_USAGE_X); // X axis - bool yExists = client.GetVJDAxisExist(deviceId, HID_USAGES.HID_USAGE_Y); // Y axis - bool zExists = client.GetVJDAxisExist(deviceId, HID_USAGES.HID_USAGE_Z); // Z axis - - if (numButtons >= 16 && - numContPov >= 1 && - xExists && - yExists && - zExists - ) - { - return deviceId; - } - } - } - - // No devices available - return 0; - } - - /// - /// Claims a vJoy device. - /// - public static uint ClaimNextAvailableDevice() - { - uint deviceId = GetNextAvailableID(); - - if (deviceId == 0) - { - throw new ParseException("No new vJoy devices are available."); - } - - if (!client.AcquireVJD(deviceId)) - { - throw new ParseException($"Could not claim vJoy device {deviceId}."); - } - - return deviceId; - } - - /// - /// Releases a vJoy device. - /// - public static void ReleaseDevice(uint deviceId) - { - // Ensure device is owned - if (client.GetVJDStatus(deviceId) == VjdStat.VJD_STAT_OWN) - { - client.RelinquishVJD(deviceId); - } - } - - public static void FreeAllDevices() - { - for (uint i = 1; i <= 16; i++) - { - ReleaseDevice(i); - } - } - - /// - /// Resets the values of this state. - /// - public static void ResetState(this vJoy.JoystickState state) - { - // Only these values are used, don't reset anything else to save on performance - state.Buttons = Button.None; - state.bHats = PoV.Neutral; - state.AxisX = 0; - state.AxisY = 0; - state.AxisZ = 0; - } - - /// - /// vJoy button flag constants. - /// - public class Button - { - public const uint - None = 0, - One = 1 << 0, - Two = 1 << 1, - Three = 1 << 2, - Four = 1 << 3, - Five = 1 << 4, - Six = 1 << 5, - Seven = 1 << 6, - Eight = 1 << 7, - Nine = 1 << 8, - Ten = 1 << 9, - Eleven = 1 << 10, - Twelve = 1 << 11, - Thirteen = 1 << 12, - Fourteen = 1 << 13, - Fifteen = 1 << 14, - Sixteen = 1 << 15; - } - - /// - /// vJoy PoV hat constants. - /// - public class PoV - { - // vJoy continuous PoV hat values range from 0 to 35999 (measured in 1/100 of a degree). - // The value is measured clockwise, with up being 0. - public const uint - Neutral = 0xFFFFFFFF, - Up = 0, - UpRight = 4500, - Right = 9000, - DownRight = 13500, - Down = 18000, - DownLeft = 22500, - Left = 27000, - UpLeft = 31500; - } - } -} diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 8291cfa..b5906fe 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -1,4 +1,4 @@ - + @@ -114,12 +114,14 @@ - - + + + + MSBuild:Compile Designer diff --git a/Program/Vigem/VigemClient.cs b/Program/Vigem/VigemClient.cs new file mode 100644 index 0000000..e61f7ce --- /dev/null +++ b/Program/Vigem/VigemClient.cs @@ -0,0 +1,60 @@ +using System; +using Nefarius.ViGEm.Client; +using Nefarius.ViGEm.Client.Targets; + +namespace RB4InstrumentMapper.Vigem +{ + /// + /// Static vJoy client. + /// + public static class VigemClient + { + /// + /// Static ViGEmBus client. + /// + private static ViGEmClient client; + + /// + /// Whether or not the ViGEmBus client has been initialized. + /// + public static bool Initialized => client != null; + + public static bool TryInitialize() + { + if (client != null) + return true; + + try + { + client = new ViGEmClient(); + return true; + } + catch + { + client = null; + return false; + } + } + + /// + /// Creates a new Xbox 360 device with the Xbox 360 Rock Band wireless instrument vendor/product IDs. + /// + public static IXbox360Controller CreateDevice() + { + if (!Initialized) + throw new ObjectDisposedException(nameof(client), "ViGEmBus client is disposed or not initialized yet!"); + + return client.CreateXbox360Controller(0x1BAD, 0x0719); + } + // Rock Band Guitar: USB\VID_1BAD&PID_0719&IG_00 XUSB\TYPE_00\SUB_86\VEN_1BAD\REV_0002 + // Rock Band Drums: USB\VID_1BAD&PID_0719&IG_02 XUSB\TYPE_00\SUB_88\VEN_1BAD\REV_0002 + // If subtype ID specification through ViGEmBus becomes possible at some point, + // the guitar should be subtype 6, and the drums should be subtype 8 + + public static void Dispose() + { + client?.Dispose(); + client = null; + } + } +} \ No newline at end of file diff --git a/Program/Vjoy/VjoyClient.cs b/Program/Vjoy/VjoyClient.cs new file mode 100644 index 0000000..e95a15f --- /dev/null +++ b/Program/Vjoy/VjoyClient.cs @@ -0,0 +1,98 @@ +using vJoyInterfaceWrap; + +namespace RB4InstrumentMapper.Vjoy +{ + /// + /// Provides functionality for logging. + /// + public static class VjoyClient + { + private static readonly vJoy client = new vJoy(); + + public static bool Enabled => client.vJoyEnabled(); + + public static string Manufacturer => client.GetvJoyManufacturerString(); + public static string Product => client.GetvJoyProductString(); + public static string SerialNumber => client.GetvJoySerialNumberString(); + + public static VjdStat GetDeviceStatus(uint deviceId) => client.GetVJDStatus(deviceId); + + public static bool IsDeviceAvailable(uint deviceId) + { + return (GetDeviceStatus(deviceId) == VjdStat.VJD_STAT_FREE) && IsDeviceCompatible(deviceId); + } + + public static bool IsDeviceCompatible(uint deviceId) + { + // Check that the vJoy device is configured correctly + int numButtons = client.GetVJDButtonNumber(deviceId); + int numContPov = client.GetVJDContPovNumber(deviceId); + bool xExists = client.GetVJDAxisExist(deviceId, HID_USAGES.HID_USAGE_X); + bool yExists = client.GetVJDAxisExist(deviceId, HID_USAGES.HID_USAGE_Y); + bool zExists = client.GetVJDAxisExist(deviceId, HID_USAGES.HID_USAGE_Z); + + if (numButtons >= 16 && numContPov >= 1 && + xExists && yExists && zExists + ) + { + return true; + } + + return false; + } + + /// + /// Gets the next available device ID. + /// + public static uint GetNextAvailableID() + { + // Get available devices + for (uint deviceId = 1; deviceId <= 16; deviceId++) + { + // Ensure device is available + if (client.GetVJDStatus(deviceId) == VjdStat.VJD_STAT_FREE && IsDeviceCompatible(deviceId)) + { + return deviceId; + } + } + + // No devices available + return 0; + } + + /// + /// Acquires a vJoy device. + /// + public static bool AcquireDevice(uint deviceId) + { + return client.AcquireVJD(deviceId); + } + + /// + /// Releases a vJoy device. + /// + public static void ReleaseDevice(uint deviceId) + { + if (client.GetVJDStatus(deviceId) == VjdStat.VJD_STAT_OWN) + { + client.RelinquishVJD(deviceId); + } + } + + public static void FreeAllDevices() + { + for (uint i = 1; i <= 16; i++) + { + ReleaseDevice(i); + } + } + + /// + /// Acquires a vJoy device. + /// + public static bool UpdateDevice(uint deviceId, ref vJoy.JoystickState state) + { + return client.UpdateVJD(deviceId, ref state); + } + } +} \ No newline at end of file diff --git a/Program/Vjoy/VjoyDefinitions.cs b/Program/Vjoy/VjoyDefinitions.cs new file mode 100644 index 0000000..8321082 --- /dev/null +++ b/Program/Vjoy/VjoyDefinitions.cs @@ -0,0 +1,44 @@ +namespace RB4InstrumentMapper.Vjoy +{ + /// + /// vJoy button flag constants. + /// + public enum VjoyButton : uint + { + None = 0, + One = 1 << 0, + Two = 1 << 1, + Three = 1 << 2, + Four = 1 << 3, + Five = 1 << 4, + Six = 1 << 5, + Seven = 1 << 6, + Eight = 1 << 7, + Nine = 1 << 8, + Ten = 1 << 9, + Eleven = 1 << 10, + Twelve = 1 << 11, + Thirteen = 1 << 12, + Fourteen = 1 << 13, + Fifteen = 1 << 14, + Sixteen = 1 << 15 + } + + /// + /// vJoy PoV hat constants. + /// + public enum VjoyPoV : uint + { + // vJoy continuous PoV hat values range from 0 to 35999 (measured in 1/100 of a degree). + // The value is measured clockwise, with up being 0. + Neutral = 0xFFFFFFFF, + Up = 0, + UpRight = 4500, + Right = 9000, + DownRight = 13500, + Down = 18000, + DownLeft = 22500, + Left = 27000, + UpLeft = 31500 + } +} \ No newline at end of file diff --git a/Program/Vjoy/VjoyExtensions.cs b/Program/Vjoy/VjoyExtensions.cs new file mode 100644 index 0000000..976c44d --- /dev/null +++ b/Program/Vjoy/VjoyExtensions.cs @@ -0,0 +1,20 @@ +using vJoyInterfaceWrap; + +namespace RB4InstrumentMapper.Vjoy +{ + public static class VjoyExtensions + { + /// + /// Resets the values of this state. + /// + public static void Reset(this vJoy.JoystickState state) + { + // Only reset the values we use + state.Buttons = (uint)VjoyButton.None; + state.bHats = (uint)VjoyPoV.Neutral; + state.AxisX = 0; + state.AxisY = 0; + state.AxisZ = 0; + } + } +} \ No newline at end of file From 3e86c7b93fe10167be0648e6839fc7b32afa27b3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 1 Mar 2023 11:34:52 -0700 Subject: [PATCH 100/437] Add warning if vJoy library version doesn't match driver version --- Program/MainWindow/MainWindow.xaml.cs | 7 +++++++ Program/Vjoy/VjoyClient.cs | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 576ff75..e82fb4b 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -113,6 +113,13 @@ private void Window_Loaded(object sender, RoutedEventArgs e) { // Log vJoy driver attributes (Vendor Name, Product Name, Version Number) Console.WriteLine($"vJoy found! - Vendor: {VjoyClient.Manufacturer}, Product: {VjoyClient.Product}, Version Number: {VjoyClient.SerialNumber}"); + + // Check if versions match + if (!VjoyClient.DriverMatch(out uint libraryVersion, out uint driverVersion)) + { + Console.WriteLine($"WARNING: vJoy library version (0x{libraryVersion:X8}) does not match driver version (0x{driverVersion:X8})! vJoy mode may cause errors!"); + } + if (CountAvailableVjoyDevices() > 0) { (controllerDeviceTypeCombo.Items[0] as ComboBoxItem).IsEnabled = true; diff --git a/Program/Vjoy/VjoyClient.cs b/Program/Vjoy/VjoyClient.cs index e95a15f..443a435 100644 --- a/Program/Vjoy/VjoyClient.cs +++ b/Program/Vjoy/VjoyClient.cs @@ -15,6 +15,13 @@ public static class VjoyClient public static string Product => client.GetvJoyProductString(); public static string SerialNumber => client.GetvJoySerialNumberString(); + public static bool DriverMatch(out uint libraryVersion, out uint driverVersion) + { + libraryVersion = 0; + driverVersion = 0; + return client.DriverMatch(ref libraryVersion, ref driverVersion); + } + public static VjdStat GetDeviceStatus(uint deviceId) => client.GetVJDStatus(deviceId); public static bool IsDeviceAvailable(uint deviceId) From 03b6074b6a19d630aebce9782477413e685b1541 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 1 Mar 2023 13:53:37 -0700 Subject: [PATCH 101/437] Remove PacketParser and merge its code into more relevant places --- Program/MainWindow/MainWindow.xaml.cs | 7 +- Program/PacketParsing/Backends/PcapBackend.cs | 38 +++++++- Program/PacketParsing/PacketParser.cs | 90 ------------------- Program/PacketParsing/XboxDevice.cs | 19 ++-- 4 files changed, 52 insertions(+), 102 deletions(-) delete mode 100644 Program/PacketParsing/PacketParser.cs diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index e82fb4b..69c732a 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -407,7 +407,6 @@ private void StartCapture() private void StopCapture() { PcapBackend.StopCapture(); - PacketParser.Close(); // Store whether or not the packet log was created bool doPacketLogMessage = Logging.PacketLogExists; @@ -576,7 +575,7 @@ private void controllerDeviceTypeCombo_SelectionChanged(object sender, Selection case 0: if (CountAvailableVjoyDevices() > 0) { - PacketParser.ParseMode = ParsingMode.vJoy; + XboxDevice.MapperMode = MappingMode.vJoy; Properties.Settings.Default.controllerDeviceType = (int)ControllerType.vJoy; } else @@ -590,12 +589,12 @@ private void controllerDeviceTypeCombo_SelectionChanged(object sender, Selection // ViGEmBus case 1: - PacketParser.ParseMode = ParsingMode.ViGEmBus; + XboxDevice.MapperMode = MappingMode.ViGEmBus; Properties.Settings.Default.controllerDeviceType = (int)ControllerType.VigemBus; break; default: - PacketParser.ParseMode = (ParsingMode)0; + XboxDevice.MapperMode = 0; Properties.Settings.Default.controllerDeviceType = (int)ControllerType.None; break; } diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index ab85239..31fa6e2 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Runtime.InteropServices; using System.Threading; using SharpPcap; @@ -10,7 +11,10 @@ namespace RB4InstrumentMapper.Parsing public static class PcapBackend { public static bool LogPackets = false; + private static ILiveDevice captureDevice = null; + private static readonly Dictionary devices = new Dictionary(); + private static bool canHandleNewDevices = true; public static event Action OnCaptureStop; @@ -46,6 +50,13 @@ public static void StopCapture() captureDevice.Close(); captureDevice = null; } + + // Clean up devices + foreach (XboxDevice device in devices.Values) + { + device.Close(); + } + devices.Clear(); } /// @@ -61,9 +72,34 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) } data = data.Slice(sizeof(ReceiverHeader)); + // Check if device ID has been encountered yet + ulong deviceId = header.DeviceId; + if (!devices.TryGetValue(deviceId, out var device)) + { + if (!canHandleNewDevices) + { + return; + } + + try + { + device = new XboxDevice(ParseMode); + } + catch (ParseException ex) + { + canHandleNewDevices = false; + Console.WriteLine("Device limit reached, or an error occured when creating virtual device. No more devices will be registered."); + Console.WriteLine($"Exception: {ex.GetFirstLine()}"); + return; + } + + devices.Add(deviceId, device); + Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); + } + try { - PacketParser.HandlePacket(header.DeviceId, data); + device.ParseCommand(data); } catch (ThreadAbortException) { diff --git a/Program/PacketParsing/PacketParser.cs b/Program/PacketParsing/PacketParser.cs deleted file mode 100644 index 17d4bd9..0000000 --- a/Program/PacketParsing/PacketParser.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace RB4InstrumentMapper.Parsing -{ - /// - /// Emulated devices that can be parsed to. - /// - public enum ParsingMode - { - ViGEmBus = 1, - vJoy = 2 - } - - /// - /// Handles packets from a capture device. - /// - public static class PacketParser - { - /// - /// Device IDs detected during Pcap. - /// - private static Dictionary pcapIds = new Dictionary(); - - /// - /// Gets or sets the current parsing mode. - /// - public static ParsingMode ParseMode { get; set; } = (ParsingMode)0; - - /// - /// Whether or not new devices can be added. - /// - private static bool canHandleNewDevices = true; - - /// - /// Handles a received Pcap packet. - /// - public static unsafe void HandlePacket(ulong deviceId, ReadOnlySpan data) - { - // Check if device ID has been encountered yet - if (!pcapIds.ContainsKey(deviceId)) - { - if (!canHandleNewDevices) - { - return; - } - - XboxDevice device; - try - { - device = new XboxDevice(ParseMode); - } - catch (ParseException ex) - { - canHandleNewDevices = false; - Console.WriteLine("Device limit reached, or an error occured when creating virtual device. No more devices will be registered."); - Console.WriteLine($"Exception: {ex.GetFirstLine()}"); - return; - } - - pcapIds.Add(deviceId, device); - Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); - } - - // Parse data - pcapIds[deviceId].ParseCommand(data); - } - - // TODO: Add libusb support - - /// - /// Performs cleanup for the parser. - /// - public static void Close() - { - // Clean up devices - foreach (XboxDevice device in pcapIds.Values) - { - device.Close(); - } - - // Clear IDs list - pcapIds.Clear(); - - // Reset flags - canHandleNewDevices = true; - } - } -} diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 84461c7..eb0819d 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -3,31 +3,36 @@ namespace RB4InstrumentMapper.Parsing { + public enum MappingMode + { + ViGEmBus = 1, + vJoy = 2 + } + /// /// Interface for Xbox devices. /// class XboxDevice { + public static MappingMode MapperMode; + /// /// Mapper interface to use. /// private IDeviceMapper deviceMapper; - // Lock off parameterless constructor - private XboxDevice() {} - /// /// Creates a new XboxDevice with the given device ID and parsing mode. /// - public XboxDevice(ParsingMode parseMode) + public XboxDevice() { - switch (parseMode) + switch (MapperMode) { - case ParsingMode.ViGEmBus: + case MappingMode.ViGEmBus: deviceMapper = new VigemMapper(); break; - case ParsingMode.vJoy: + case MappingMode.vJoy: deviceMapper = new VjoyMapper(); break; } From 6ff61273b0e52ad73b80f4dfb3437ee8730bc9e4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 4 Mar 2023 19:56:41 -0700 Subject: [PATCH 102/437] Remove PacketFormats.md and link to PlasticBand instead --- Docs/PacketFormats.md | 235 ----------------------------------------- Docs/drums_inputs.txt | 8 ++ Docs/guitar_inputs.txt | 16 +++ README.md | 3 +- 4 files changed, 26 insertions(+), 236 deletions(-) delete mode 100644 Docs/PacketFormats.md create mode 100644 Docs/drums_inputs.txt create mode 100644 Docs/guitar_inputs.txt diff --git a/Docs/PacketFormats.md b/Docs/PacketFormats.md deleted file mode 100644 index dfc292c..0000000 --- a/Docs/PacketFormats.md +++ /dev/null @@ -1,235 +0,0 @@ -# Xbox One RB4 Instrument Data Packets - -This document provides some details on how Xbox One device data packets are received through Pcap packet captures. - -This documentation is far from fully comprehensive, as there are many parts of the Xbox One controller protocol that don't pertain to sniffing inputs. There are also some parts of the receiver header data that are not well understood. - -A note: There is a more direct/built-in way to get Xbox One controller data, [detailed here](https://gist.github.com/TheNathannator/bcebc77e653f71e77634144940871596), however this method only works while the application has focus, so packet sniffing is still required for remapper programs. - -All values are listed as little-endian. Byte numbers are 0-indexed. - -## Table of Contents - -- [Packet Sections](#packet-sections) - - [Receiver Header](#receiver-header) - - [Command Header](#command-header) - - [Important Command IDs](#important-command-ids) - - [`0x07`: Virtual Keycode](#0x07-virtual-keycode) - - [`0x20`: Input Data](#0x20-input-data) -- [Guitar Input Data](#guitar-input-data) - - [Guitar Packet Samples](#guitar-packet-samples) -- [Drums Input Data](#drums-input-data) - - [Drum Packet Samples](#drum-packet-samples) -- [References](#references) - -## Packet Sections - -- Bytes 0-25: Receiver header -- Bytes 26-29: Command header -- Bytes 30 onward: Message data - -### Receiver Header - -`0x88 0xA0-00 0x00` - -- 26 bytes long -- Not well understood, more research needed - -Bytes: - -- Byte 0: Always `0x88`? -- Byte 1: Flags value? - - Typically `0x11` but is `0x19` every so often -- Bytes 2-3: Constant `0xA0-00`? -- Bytes 4-9: Receiver ID? -- Bytes 10-15: Device ID - - This is the value to keep track of for identifying which device is which. -- Bytes 16-21: Redundant receiver ID? (mirrors 4-9) -- Bytes 22-23: Little-endian packet count - - This one is encoded weirdly, it seems to be an 8-bit value that's been bit-shifted left by 4 bits such that the value is split between two different bytes. - - In more explicit terms, the high 4 bits of 22 seem to increment by 1 with every packet, and 23 seems to increment every time 22's high 4 bits roll over from F to 0. -- Byte 24: Another flags value? - - Seems to be `0x01` during power-on or Xbox button packets, and `0x00` everywhere else -- Byte 25: Constant `0x00`? - -### Command Header - -` ` - -- 4 bytes long - -Bytes: - -- Byte 0: Command ID - - This indicates what the data following this header represents. - - Important IDs: - - `0x07`: Virtual keycode (used for the guide button) - - `0x20`: Input report -- Byte 1: Flags/client ID - - This is a combination flags and client ID value for messages. - - Understanding this value isn't important for just sniffing input data. -- Byte 2: Sequence count - - This keeps track of the order/sequence of packets. - - Understanding this value isn't important for just sniffing input data. -- Byte 3: Data length - - Number of bytes in the rest of the packet (some exceptions to this, but none that pertain to input data). - -The bytes following the header are defined per command ID. - -#### Important Command IDs - -##### `0x07`: Virtual Keycode - -This command is used to report virtual keycode presses. It is also used to report the guide button press. - -` ` - -- `pressed` is a boolean value (`0x00` or `0x01`) indicating whether or not the key is pressed. -- `keycode` is the virtual keycode for the key. - - This is `0x5B` when the guide button is pressed, which equates to the Left Windows key. - -##### `0x20`: Input Data - -This command is used to report input data. The actual input data varies per device type. - -The standard Xbox One controller layout is as follows: - -- 12 bytes long -- ` ` - - `buttons`: 16-bit button bitmask - - Bit 0 (`0x0001`) - Sync button - - Bit 1 (`0x0002`) - Unused - - Bit 2 (`0x0004`) - Menu Button - - Bit 3 (`0x0008`) - View Button - - Bit 4 (`0x0010`) - A Button - - Bit 5 (`0x0020`) - B Button - - Bit 6 (`0x0040`) - X Button - - Bit 7 (`0x0080`) - Y Button - - Bit 8 (`0x0100`) - D-pad Up - - Bit 9 (`0x0200`) - D-pad Down - - Bit 10 (`0x0400`) - D-pad Left - - Bit 11 (`0x0800`) - D-pad Right - - Bit 12 (`0x1000`) - Left Bumper - - Bit 13 (`0x2000`) - Right Bumper - - Bit 14 (`0x4000`) - Left Stick Press - - Bit 15 (`0x8000`) - Right Stick Press - - `left trigger` and `right trigger`: 16-bit little-endian unsigned axis - - `left stick X`/`Y`, `right stick X`/`Y`: 16-bit little-endian signed axis - -## Guitar Input Data - -10 bytes long - -` ` - -- `buttons`: 16-bit button bitmask - - Bit 0 (`0x0001`) - Sync button - - Bit 1 (`0x0002`) - Unused - - Bit 2 (`0x0004`) - Menu Button - - Bit 3 (`0x0008`) - View Button - - Bit 4 (`0x0010`) - Green Fret Flag (equivalent to A Button) - - Bit 5 (`0x0020`) - Red Fret Flag (equivalent to B Button) - - Bit 6 (`0x0040`) - Blue Fret Flag (equivalent to X Button) - - Bit 7 (`0x0080`) - Yellow Fret Flag (equivalent to Y Button) - - Bit 8 (`0x0100`) - D-pad Up/Strum Up - - Bit 9 (`0x0200`) - D-pad Down/Strum Down - - Bit 10 (`0x0400`) - D-pad Left - - Bit 11 (`0x0800`) - D-pad Right - - Bit 12 (`0x1000`) - Orange Fret Flag (equivalent to Left Bumper) - - Bit 13 (`0x2000`) - Unused (equivalent to Right Bumper) - - Bit 14 (`0x4000`) - Lower Fret Flag (equivalent to Left Stick Press) - - Bit 15 (`0x8000`) - Unused (equivalent to Right Stick Press) -- `tilt`: 8-bit tilt axis - - Has a threshold of `0x70`? (values below get cut off to `0x00`) -- `whammy`: 8-bit whammy bar axis -- `pickup/slider`: 8-bit pickup switch/slider axis - - Seems to use top 4 bytes, values from the Guitar Sniffer logs are `0x00`, `0x10`, `0x20`, `0x30`, and `0x40` -- `upper frets`, `lower frets`: 8-bit fret bitmask - - Bit 0 (`0x01`) - Green - - Bit 1 (`0x02`) - Red - - Bit 2 (`0x04`) - Yellow - - Bit 3 (`0x08`) - Blue - - Bit 4 (`0x10`) - Orange - - Bits 5-7 - Unused -- `unused[3]`: unknown data values - -### Guitar Packet Samples - -``` -2021-10-31 01:35:10.730 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A20550000 2000530A 00003C00000000000000 -2021-10-31 01:35:10.742 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A30550000 2000540A 00003E00000000000000 -2021-10-31 01:35:10.750 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A40550000 2000550A 00003D00000000000000 -2021-10-31 01:35:10.758 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A50550000 2000560A 00003C00000000000000 -2021-10-31 01:35:10.766 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A60550000 2000570A 00003B00000000000000 -2021-10-31 01:35:10.783 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A70550000 2000580A 00003C00000000000000 -2021-10-31 01:35:10.799 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A80550000 2000590A 00003B00000000000000 -2021-10-31 01:35:10.807 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A90550000 20005A0A 00003C00000000000000 -``` - -``` -2021-10-31 01:58:23.354 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A209B0000 2000C60A 10008A00400100000000 -2021-10-31 01:58:23.363 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A309B0000 2000C70A 10008900400100000000 -2021-10-31 01:58:23.371 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A409B0000 2000C80A 10008A00400100000000 -2021-10-31 01:58:23.403 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A509B0000 2000C90A 10008B00400100000000 -2021-10-31 01:58:23.411 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A609B0000 2000CA0A 10008D00400100000000 -2021-10-31 01:58:23.443 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A709B0000 2000CB0A 10008F00400100000000 -2021-10-31 01:58:23.459 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A809B0000 2000CC0A 10009000400100000000 -``` - -## Drums Input Data - -6 bytes long - -` ` - -Bytes: - -- `buttons`: 16-bit button bitmask - - Bit 0 (`0x0001`) - Sync button - - Bit 1 (`0x0002`) - Unused - - Bit 2 (`0x0004`) - Menu Button - - Bit 3 (`0x0008`) - View Button - - Bit 4 (`0x0010`) - A Button/Green Pad - - Bit 5 (`0x0020`) - B Button/Red Pad - - Bit 6 (`0x0040`) - X Button - - Bit 7 (`0x0080`) - Y Button - - Bit 8 (`0x0100`) - D-pad Up - - Bit 9 (`0x0200`) - D-pad Down - - Bit 10 (`0x0400`) - D-pad Left - - Bit 11 (`0x0800`) - D-pad Right - - Bit 12 (`0x1000`) - 1st Kick Pedal (equivalent to Left Bumper) - - Bit 13 (`0x2000`) - 2nd Kick Pedal (equivalent to Right Bumper) - - Bit 14 (`0x4000`) - Unused (equivalent to Left Stick Press) - - Bit 15 (`0x8000`) - Unused (equivalent to Right Stick Press) -- `pad velocities` - 16 bits for the pad velocities (remember that this is little-endian) - - Bits 0-3 (`0x000F`) - Yellow Pad - - Bits 4-7 (`0x00F0`) - Red Pad - - Bits 8-11 (`0x0F00`) - Green Pad - - Bits 12-15 (`0xF000`) - Blue Pad - - Seem to range from 0-7 -- `cymbal velocities` - 16 bits for the cymbal velocities (remember that this is little-endian) - - Bits 0-3 (`0x000F`) - Blue Cymbal - - Bits 4-7 (`0x00F0`) - Yellow Cymbal - - Bits 8-11 (`0x0F00`) - Unused - - Bits 12-15 (`0xF000`) - Green Cymbal - - Seem to range from 0-7 - -### Drum Packet Samples - -``` -2021-10-31 02:25:31.725 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AA0020000 20002B06 0000 0400 0000 -2021-10-31 02:25:31.773 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AB0020000 20002C06 0000 0000 0000 -2021-10-31 02:25:32.038 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AC0020000 20002D06 2000 4000 0000 -2021-10-31 02:25:32.086 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AD0020000 20002E06 0000 0000 0000 -2021-10-31 02:25:32.327 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AE0020000 20002F06 0000 0400 0000 -2021-10-31 02:25:32.367 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AF0020000 20003006 0000 0000 0000 -2021-10-31 02:25:32.608 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18A00030000 20003106 0000 0040 0000 -2021-10-31 02:25:32.656 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18A10030000 20003206 0000 0000 0000 -``` - -## References - -- [GuitarSniffer guitar packet logs](https://1drv.ms/f/s!AgQGk0OeTMLwhA-uDO9IQHEHqGhv) -- GuitarSniffer guitar packet spreadsheets: [New](https://docs.google.com/spreadsheets/d/1ITZUvRniGpfS_HV_rBpSwlDdGukc3GC1CeOe7SavQBo/edit?usp=sharing), [Old](https://1drv.ms/x/s!AgQGk0OeTMLwg3GBDXFUC3Erj4Wb) -- https://github.com/quantus/xbox-one-controller-protocol -- https://github.com/medusalix/xone diff --git a/Docs/drums_inputs.txt b/Docs/drums_inputs.txt new file mode 100644 index 0000000..450fc50 --- /dev/null +++ b/Docs/drums_inputs.txt @@ -0,0 +1,8 @@ +2021-10-31 02:25:31.725 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AA0020000 20002B06 0000 0400 0000 +2021-10-31 02:25:31.773 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AB0020000 20002C06 0000 0000 0000 +2021-10-31 02:25:32.038 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AC0020000 20002D06 2000 4000 0000 +2021-10-31 02:25:32.086 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AD0020000 20002E06 0000 0000 0000 +2021-10-31 02:25:32.327 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AE0020000 20002F06 0000 0400 0000 +2021-10-31 02:25:32.367 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18AF0020000 20003006 0000 0000 0000 +2021-10-31 02:25:32.608 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18A00030000 20003106 0000 0040 0000 +2021-10-31 02:25:32.656 [36] 8811A0006245B4E9D18A 7EED8FFFCF6B 6245B4E9D18A10030000 20003206 0000 0000 0000 \ No newline at end of file diff --git a/Docs/guitar_inputs.txt b/Docs/guitar_inputs.txt new file mode 100644 index 0000000..53cccf6 --- /dev/null +++ b/Docs/guitar_inputs.txt @@ -0,0 +1,16 @@ +2021-10-31 01:35:10.730 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A20550000 2000530A 00003C00000000000000 +2021-10-31 01:35:10.742 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A30550000 2000540A 00003E00000000000000 +2021-10-31 01:35:10.750 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A40550000 2000550A 00003D00000000000000 +2021-10-31 01:35:10.758 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A50550000 2000560A 00003C00000000000000 +2021-10-31 01:35:10.766 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A60550000 2000570A 00003B00000000000000 +2021-10-31 01:35:10.783 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A70550000 2000580A 00003C00000000000000 +2021-10-31 01:35:10.799 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A80550000 2000590A 00003B00000000000000 +2021-10-31 01:35:10.807 [40] 8811A0006245B4E9D18A 7EED8FFE198A 6245B4E9D18A90550000 20005A0A 00003C00000000000000 + +2021-10-31 01:58:23.354 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A209B0000 2000C60A 10008A00400100000000 +2021-10-31 01:58:23.363 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A309B0000 2000C70A 10008900400100000000 +2021-10-31 01:58:23.371 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A409B0000 2000C80A 10008A00400100000000 +2021-10-31 01:58:23.403 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A509B0000 2000C90A 10008B00400100000000 +2021-10-31 01:58:23.411 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A609B0000 2000CA0A 10008D00400100000000 +2021-10-31 01:58:23.443 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A709B0000 2000CB0A 10008F00400100000000 +2021-10-31 01:58:23.459 [40] 8811A0006245B4E9D18A 7EED8FFB14BF 6245B4E9D18A809B0000 2000CC0A 10009000400100000000 \ No newline at end of file diff --git a/README.md b/README.md index 543c4f0..91be906 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,8 @@ Packet Data: - [GuitarSniffer guitar packet logs](https://1drv.ms/f/s!AgQGk0OeTMLwhA-uDO9IQHEHqGhv) - GuitarSniffer guitar packet spreadsheets: [New](https://docs.google.com/spreadsheets/d/1ITZUvRniGpfS_HV_rBpSwlDdGukc3GC1CeOe7SavQBo/edit?usp=sharing), [Old](https://1drv.ms/x/s!AgQGk0OeTMLwg3GBDXFUC3Erj4Wb) -- See [PacketFormats.md](/Docs/PacketFormats.md) for a breakdown of the known packet data. + +Additional documentation is available in the [PlasticBand documentation repository](https://github.com/TheNathannator/PlasticBand). ## Building From f928fb3ada0ce3cfe646a6f45fac4efe175b4d8c Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 4 Mar 2023 21:18:35 -0700 Subject: [PATCH 103/437] Ah whoops, missed this --- Program/PacketParsing/Backends/PcapBackend.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 31fa6e2..56284ff 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -83,7 +83,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) try { - device = new XboxDevice(ParseMode); + device = new XboxDevice(); } catch (ParseException ex) { From a0da5b26d4080b4d61845c9de22bbfead2a9fa49 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 4 Mar 2023 21:23:32 -0700 Subject: [PATCH 104/437] Properly make use of receiver header in Pcap backend --- Program/PacketParsing/Backends/PcapBackend.cs | 44 ++++++++++++++++++- Program/PacketParsing/PacketDefinitions.cs | 35 --------------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 56284ff..3e262f0 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -8,6 +8,39 @@ namespace RB4InstrumentMapper.Parsing { public delegate void PacketReceivedHandler(DateTime timestamp, ReadOnlySpan data); + /// + /// A standard IEEE 802.11 QoS header. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + unsafe struct QoSHeader + { + ushort frameControl; + ushort durationId; + fixed byte receiverAddress[6]; + fixed byte transmitterAddress[6]; + fixed byte destinationAddress[6]; + ushort sequenceControl; + ushort qosControl; + + public byte FrameType => (byte)((frameControl & 0xC) >> 2); + public byte FrameSubtype => (byte)((frameControl & 0xF0) >> 4); + + public ulong DeviceId + { + get + { + fixed (byte* ptr = transmitterAddress) + { + // Read a ulong starting from deviceId_1 + ulong deviceId = *(ulong*)ptr; + // Last 2 bytes aren't part of the device ID + deviceId &= 0x0000FFFF_FFFFFFFF; + return deviceId; + } + } + } + } + public static class PcapBackend { public static bool LogPackets = false; @@ -66,11 +99,18 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) { // Read out receiver header var data = packet.Data; - if (data.Length < sizeof(ReceiverHeader) || !MemoryMarshal.TryRead(data, out ReceiverHeader header)) + if (data.Length < sizeof(QoSHeader) || !MemoryMarshal.TryRead(data, out QoSHeader header)) + { + return; + } + data = data.Slice(sizeof(QoSHeader)); + + // Ensure type and subtype are Data, QoS Data respectively + // Other frame types are irrelevant for our purposes + if (header.FrameType != 2 || header.FrameSubtype != 8) { return; } - data = data.Slice(sizeof(ReceiverHeader)); // Check if device ID has been encountered yet ulong deviceId = header.DeviceId; diff --git a/Program/PacketParsing/PacketDefinitions.cs b/Program/PacketParsing/PacketDefinitions.cs index 24dfedf..b74b8b7 100644 --- a/Program/PacketParsing/PacketDefinitions.cs +++ b/Program/PacketParsing/PacketDefinitions.cs @@ -4,41 +4,6 @@ namespace RB4InstrumentMapper.Parsing { - /// - /// Header data from the receiver. - /// - [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct ReceiverHeader - { - byte unk0; - byte unk1; - ushort unk2; - uint receiverId1_1; - ushort receiverId1_2; - uint deviceId_1; - ushort deviceId_2; - uint receiverId2_1; - ushort receiverId2_2; - ushort sequence; - byte unk3; - byte unk4; - - public unsafe ulong DeviceId - { - get - { - fixed (uint* ptr = &deviceId_1) - { - // Read a ulong starting from deviceId_1 - ulong deviceId = *(ulong*)ptr; - // Last 2 bytes aren't part of the device ID - deviceId &= 0x0000FFFF_FFFFFFFF; - return deviceId; - } - } - } - } - /// /// Header data for a message. /// From f73db6d30335cf4b0789980fbd56318d948fbc57 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 5 Mar 2023 01:49:23 -0700 Subject: [PATCH 105/437] Implement proper parsing of command headers --- Program/PacketParsing/PacketDefinitions.cs | 45 ++++++++++++++++++++-- Program/PacketParsing/ParsingUtils.cs | 36 +++++++++++++++++ Program/PacketParsing/XboxDevice.cs | 5 ++- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/Program/PacketParsing/PacketDefinitions.cs b/Program/PacketParsing/PacketDefinitions.cs index b74b8b7..d0a1312 100644 --- a/Program/PacketParsing/PacketDefinitions.cs +++ b/Program/PacketParsing/PacketDefinitions.cs @@ -18,10 +18,49 @@ public enum Command : byte Input = 0x20 } - public byte CommandId; - public byte Flags; + /// + /// Flag definitions. + /// + [Flags] + public enum Flags : byte + { + None = 0, + NeedsAcknowledgement = 0x10, + SystemCommand = 0x20, + ChunkStart = 0x40, + ChunkPacket = 0x80 + } + + public Command CommandId; + public Flags CommandFlags; public byte SequenceCount; - public byte DataLength; + public int DataLength; + + public static bool TryParse(ReadOnlySpan data, out CommandHeader header, out int bytesRead) + { + header = default; + bytesRead = 0; + if (data == null || data.Length < 4) + { + return false; + } + + if (!ParsingUtils.DecodeLEB128(data.Slice(3), out int dataLength, out int byteLength)) + { + return false; + } + + header = new CommandHeader() + { + CommandId = (Command)data[0], + CommandFlags = (Flags)data[1], + SequenceCount = data[2], + DataLength = dataLength + }; + bytesRead = 3 + byteLength; + + return true; + } } /// diff --git a/Program/PacketParsing/ParsingUtils.cs b/Program/PacketParsing/ParsingUtils.cs index d1665ec..b53bae6 100644 --- a/Program/PacketParsing/ParsingUtils.cs +++ b/Program/PacketParsing/ParsingUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; namespace RB4InstrumentMapper.Parsing { @@ -7,6 +8,41 @@ namespace RB4InstrumentMapper.Parsing /// static class ParsingUtils { + // https://en.wikipedia.org/wiki/LEB128 + public static bool DecodeLEB128(ReadOnlySpan data, out int result, out int byteLength) + { + byteLength = 0; + result = 0; + + if (data == null || data.Length < 1) + { + return false; + } + + // Decode variable-length length value + // Sequence length is limited to 4 bytes + byte value = data[0]; + for (int index = 0; + (index < data.Length) && (index < sizeof(int)) && ((value & 0x80) != 0); + index++) + { + value = data[index]; + result |= (value & 0x7F) << (index * 7); + byteLength++; + } + + // Detect length sequences longer than 4 bytes + if ((value & 0x80) != 0) + { + Debug.WriteLine($"Variable-length value is greater than 4 bytes! Buffer: {BitConverter.ToString(data.ToArray())}"); + byteLength = 0; + result = 0; + return false; + } + + return true; + } + /// /// Scales this byte to an int, starting from the negative end. /// diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index eb0819d..eba0eaa 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -51,14 +51,15 @@ public XboxDevice() /// public unsafe void ParseCommand(ReadOnlySpan commandData) { - if (!MemoryMarshal.TryRead(commandData, out CommandHeader header)) + if (!CommandHeader.TryParse(commandData, out var header, out int bytesRead)) { return; } + commandData = commandData.Slice(bytesRead); switch (header.CommandId) { - case (byte)CommandHeader.Command.Input: + case CommandHeader.Command.Input: deviceMapper.ParseInput(header, commandData.Slice(sizeof(CommandHeader))); break; From 25a2b58c02dfe8c49d443cba63c02043c80d4af4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 5 Mar 2023 01:51:17 -0700 Subject: [PATCH 106/437] Un-nest command ID and flag enums --- Program/PacketParsing/PacketDefinitions.cs | 50 +++++++++++----------- Program/PacketParsing/XboxDevice.cs | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Program/PacketParsing/PacketDefinitions.cs b/Program/PacketParsing/PacketDefinitions.cs index d0a1312..f36012e 100644 --- a/Program/PacketParsing/PacketDefinitions.cs +++ b/Program/PacketParsing/PacketDefinitions.cs @@ -4,35 +4,35 @@ namespace RB4InstrumentMapper.Parsing { + /// + /// Command ID definitions. + /// + public enum CommandId : byte + { + Input = 0x20 + } + + /// + /// Command flag definitions. + /// + [Flags] + public enum CommandFlags : byte + { + None = 0, + NeedsAcknowledgement = 0x10, + SystemCommand = 0x20, + ChunkStart = 0x40, + ChunkPacket = 0x80 + } + /// /// Header data for a message. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] struct CommandHeader { - /// - /// Command ID definitions. - /// - public enum Command : byte - { - Input = 0x20 - } - - /// - /// Flag definitions. - /// - [Flags] - public enum Flags : byte - { - None = 0, - NeedsAcknowledgement = 0x10, - SystemCommand = 0x20, - ChunkStart = 0x40, - ChunkPacket = 0x80 - } - - public Command CommandId; - public Flags CommandFlags; + public CommandId CommandId; + public CommandFlags Flags; public byte SequenceCount; public int DataLength; @@ -52,8 +52,8 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o header = new CommandHeader() { - CommandId = (Command)data[0], - CommandFlags = (Flags)data[1], + CommandId = (CommandId)data[0], + Flags = (CommandFlags)data[1], SequenceCount = data[2], DataLength = dataLength }; diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index eba0eaa..752abc2 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -59,7 +59,7 @@ public unsafe void ParseCommand(ReadOnlySpan commandData) switch (header.CommandId) { - case CommandHeader.Command.Input: + case CommandId.Input: deviceMapper.ParseInput(header, commandData.Slice(sizeof(CommandHeader))); break; From 643ddc818687521fab26f35dd6cdada55155110c Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 5 Mar 2023 22:53:30 -0700 Subject: [PATCH 107/437] Implement handling of chunked packets --- Program/PacketParsing/XboxDevice.cs | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 752abc2..761a343 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Runtime.InteropServices; namespace RB4InstrumentMapper.Parsing @@ -20,6 +21,7 @@ class XboxDevice /// Mapper interface to use. /// private IDeviceMapper deviceMapper; + private byte[] chunkBuffer; /// /// Creates a new XboxDevice with the given device ID and parsing mode. @@ -57,6 +59,46 @@ public unsafe void ParseCommand(ReadOnlySpan commandData) } commandData = commandData.Slice(bytesRead); + // Chunked packets + if ((header.Flags & CommandFlags.ChunkPacket) != 0) + { + // Get sequence length/index + if (!ParsingUtils.DecodeLEB128(commandData, out int bufferIndex, out bytesRead)) + { + return; + } + commandData = commandData.Slice(bytesRead); + + // Do nothing with chunks of length 0 + if (bufferIndex > 0) + { + // Buffer index equalling buffer length signals the end of the sequence + if (chunkBuffer != null && bufferIndex >= chunkBuffer.Length) + { + Debug.Assert(commandData.Length == 0); + commandData = chunkBuffer; + } + else + { + if ((header.Flags & CommandFlags.ChunkStart) != 0) + { + Debug.Assert(chunkBuffer == null); + // Buffer index is the total size of the buffer on the starting packet + chunkBuffer = new byte[bufferIndex]; + } + + Debug.Assert(chunkBuffer != null); + Debug.Assert((bufferIndex + commandData.Length) >= chunkBuffer.Length); + if (chunkBuffer == null || ((bufferIndex + commandData.Length) >= chunkBuffer.Length)) + { + return; + } + + commandData.CopyTo(chunkBuffer.AsSpan(bufferIndex, commandData.Length)); + } + } + } + switch (header.CommandId) { case CommandId.Input: From 318cf3e7ebe3bf200bbbd20dd48efaaf28d08250 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 00:39:16 -0700 Subject: [PATCH 108/437] Re-organize packet definitions --- Program/PacketParsing/PacketDefinitions.cs | 206 ------------------ .../PacketParsing/Packets/CommandHeader.cs | 64 ++++++ Program/PacketParsing/Packets/DrumInput.cs | 69 ++++++ .../PacketParsing/Packets/GamepadButton.cs | 28 +++ Program/PacketParsing/Packets/GuitarInput.cs | 59 +++++ Program/PacketParsing/VigemMapper.cs | 4 +- Program/PacketParsing/VjoyMapper.cs | 4 +- Program/RB4InstrumentMapper.csproj | 5 +- 8 files changed, 228 insertions(+), 211 deletions(-) delete mode 100644 Program/PacketParsing/PacketDefinitions.cs create mode 100644 Program/PacketParsing/Packets/CommandHeader.cs create mode 100644 Program/PacketParsing/Packets/DrumInput.cs create mode 100644 Program/PacketParsing/Packets/GamepadButton.cs create mode 100644 Program/PacketParsing/Packets/GuitarInput.cs diff --git a/Program/PacketParsing/PacketDefinitions.cs b/Program/PacketParsing/PacketDefinitions.cs deleted file mode 100644 index f36012e..0000000 --- a/Program/PacketParsing/PacketDefinitions.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace RB4InstrumentMapper.Parsing -{ - /// - /// Command ID definitions. - /// - public enum CommandId : byte - { - Input = 0x20 - } - - /// - /// Command flag definitions. - /// - [Flags] - public enum CommandFlags : byte - { - None = 0, - NeedsAcknowledgement = 0x10, - SystemCommand = 0x20, - ChunkStart = 0x40, - ChunkPacket = 0x80 - } - - /// - /// Header data for a message. - /// - [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct CommandHeader - { - public CommandId CommandId; - public CommandFlags Flags; - public byte SequenceCount; - public int DataLength; - - public static bool TryParse(ReadOnlySpan data, out CommandHeader header, out int bytesRead) - { - header = default; - bytesRead = 0; - if (data == null || data.Length < 4) - { - return false; - } - - if (!ParsingUtils.DecodeLEB128(data.Slice(3), out int dataLength, out int byteLength)) - { - return false; - } - - header = new CommandHeader() - { - CommandId = (CommandId)data[0], - Flags = (CommandFlags)data[1], - SequenceCount = data[2], - DataLength = dataLength - }; - bytesRead = 3 + byteLength; - - return true; - } - } - - /// - /// Flag definitions for the buttons bytes. - /// - [Flags] - enum GamepadButton : ushort - { - Sync = 0x0001, - Unused = 0x0002, - Menu = 0x0004, - Options = 0x0008, - A = 0x0010, - B = 0x0020, - X = 0x0040, - Y = 0x0080, - DpadUp = 0x0100, - DpadDown = 0x0200, - DpadLeft = 0x0400, - DpadRight = 0x0800, - LeftBumper = 0x1000, - RightBumper = 0x2000, - LeftStickPress = 0x4000, - RightStickPress = 0x8000 - } - - /// - /// An input report from a guitar. - /// - [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct GuitarInput - { - /// - /// Re-definitions for button flags that have specific meanings. - /// - [Flags] - public enum Button : ushort - { - StrumUp = GamepadButton.DpadUp, - StrumDown = GamepadButton.DpadDown, - GreenFret = GamepadButton.A, - RedFret = GamepadButton.B, - YellowFret = GamepadButton.Y, - BlueFret = GamepadButton.X, - OrangeFret = GamepadButton.LeftBumper, - LowerFretFlag = GamepadButton.LeftStickPress - } - - /// - /// Flags used in and - /// - [Flags] - public enum Fret : byte - { - Green = 0x01, - Red = 0x02, - Yellow = 0x04, - Blue = 0x08, - Orange = 0x10 - } - - public ushort Buttons; - public byte Tilt; - public byte WhammyBar; - public byte PickupSwitch; - public byte UpperFrets; - public byte LowerFrets; - byte unk1; - byte unk2; - byte unk3; - - public bool Green => ((UpperFrets | LowerFrets) & (byte)Fret.Green) != 0; - public bool Red => ((UpperFrets | LowerFrets) & (byte)Fret.Red) != 0; - public bool Yellow => ((UpperFrets | LowerFrets) & (byte)Fret.Yellow) != 0; - public bool Blue => ((UpperFrets | LowerFrets) & (byte)Fret.Blue) != 0; - public bool Orange => ((UpperFrets | LowerFrets) & (byte)Fret.Orange) != 0; - - public bool LowerFretFlag => (Buttons & (ushort)Button.LowerFretFlag) != 0; - } - - /// - /// An input report from a drumkit. - /// - [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct DrumInput - { - /// - /// Re-definitions for button flags that have specific meanings. - /// - [Flags] - public enum Button : ushort - { - // Not used as these are for menu navigation purposes - // RedPad = GamepadButton.B, - // GreenPad = GamepadButton.A, - KickOne = GamepadButton.LeftBumper, - KickTwo = GamepadButton.RightBumper - } - - /// - /// Masks for each pad's value. - /// - enum Pad : ushort - { - Red = 0x00F0, - Yellow = 0x000F, - Blue = 0xF000, - Green = 0x0F00 - } - - /// - /// Masks for each cymbal's value. - /// - enum Cymbal : ushort - { - Yellow = 0x00F0, - Blue = 0x000F, - Green = 0xF000 - } - - public ushort Buttons; - ushort pads; - ushort cymbals; - - public byte RedPad => getByteValue(pads, (ushort)Pad.Red, 4); - public byte YellowPad => getByteValue(pads, (ushort)Pad.Yellow, 0); - public byte BluePad => getByteValue(pads, (ushort)Pad.Blue, 12); - public byte GreenPad => getByteValue(pads, (ushort)Pad.Green, 8); - - public byte YellowCymbal => getByteValue(cymbals, (ushort)Cymbal.Yellow, 4); - public byte BlueCymbal => getByteValue(cymbals, (ushort)Cymbal.Blue, 0); - public byte GreenCymbal => getByteValue(cymbals, (ushort)Cymbal.Green, 12); - - /// - /// Gets a byte value from a ushort field of multiple values. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - byte getByteValue(ushort field, ushort mask, ushort offset) - { - return (byte)((field & mask) >> offset); - } - } -} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs new file mode 100644 index 0000000..b7972bd --- /dev/null +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Command ID definitions. + /// + public enum CommandId : byte + { + Input = 0x20 + } + + /// + /// Command flag definitions. + /// + [Flags] + public enum CommandFlags : byte + { + None = 0, + NeedsAcknowledgement = 0x10, + SystemCommand = 0x20, + ChunkStart = 0x40, + ChunkPacket = 0x80 + } + + /// + /// Header data for a message. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct CommandHeader + { + public CommandId CommandId; + public CommandFlags Flags; + public byte SequenceCount; + public int DataLength; + + public static bool TryParse(ReadOnlySpan data, out CommandHeader header, out int bytesRead) + { + header = default; + bytesRead = 0; + if (data == null || data.Length < 4) + { + return false; + } + + if (!ParsingUtils.DecodeLEB128(data.Slice(3), out int dataLength, out int byteLength)) + { + return false; + } + + header = new CommandHeader() + { + CommandId = (CommandId)data[0], + Flags = (CommandFlags)data[1], + SequenceCount = data[2], + DataLength = dataLength + }; + bytesRead = 3 + byteLength; + + return true; + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/DrumInput.cs b/Program/PacketParsing/Packets/DrumInput.cs new file mode 100644 index 0000000..b0934a8 --- /dev/null +++ b/Program/PacketParsing/Packets/DrumInput.cs @@ -0,0 +1,69 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Re-definitions for button flags that have specific meanings. + /// + [Flags] + public enum DrumButton : ushort + { + // Not used as these are for menu navigation purposes + // RedPad = GamepadButton.B, + // GreenPad = GamepadButton.A, + KickOne = GamepadButton.LeftBumper, + KickTwo = GamepadButton.RightBumper + } + + /// + /// An input report from a drumkit. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct DrumInput + { + /// + /// Masks for each pad's value. + /// + enum DrumPad : ushort + { + Red = 0x00F0, + Yellow = 0x000F, + Blue = 0xF000, + Green = 0x0F00 + } + + /// + /// Masks for each cymbal's value. + /// + enum DrumCymbal : ushort + { + Yellow = 0x00F0, + Blue = 0x000F, + Green = 0xF000 + } + + public ushort Buttons; + ushort pads; + ushort cymbals; + + public byte RedPad => (byte)((pads & (ushort)DrumPad.Red) >> 4); + public byte YellowPad => (byte)(pads & (ushort)DrumPad.Yellow); + public byte BluePad => (byte)((pads & (ushort)DrumPad.Blue) >> 12); + public byte GreenPad => (byte)((pads & (ushort)DrumPad.Green) >> 8); + + public byte YellowCymbal => (byte)((cymbals & (ushort)DrumCymbal.Yellow) >> 4); + public byte BlueCymbal => (byte)(cymbals & (ushort)DrumCymbal.Blue); + public byte GreenCymbal => (byte)((cymbals & (ushort)DrumCymbal.Green) >> 12); + + /// + /// Gets a byte value from a ushort field of multiple values. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + byte getByteValue(ushort field, ushort mask, ushort offset) + { + return (byte)((field & mask) >> offset); + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/GamepadButton.cs b/Program/PacketParsing/Packets/GamepadButton.cs new file mode 100644 index 0000000..7554240 --- /dev/null +++ b/Program/PacketParsing/Packets/GamepadButton.cs @@ -0,0 +1,28 @@ +using System; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Flag definitions for the buttons bytes. + /// + [Flags] + public enum GamepadButton : ushort + { + Sync = 0x0001, + Unused = 0x0002, + Menu = 0x0004, + Options = 0x0008, + A = 0x0010, + B = 0x0020, + X = 0x0040, + Y = 0x0080, + DpadUp = 0x0100, + DpadDown = 0x0200, + DpadLeft = 0x0400, + DpadRight = 0x0800, + LeftBumper = 0x1000, + RightBumper = 0x2000, + LeftStickPress = 0x4000, + RightStickPress = 0x8000 + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/GuitarInput.cs b/Program/PacketParsing/Packets/GuitarInput.cs new file mode 100644 index 0000000..2bf6b58 --- /dev/null +++ b/Program/PacketParsing/Packets/GuitarInput.cs @@ -0,0 +1,59 @@ +using System; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Re-definitions for button flags that have specific meanings. + /// + [Flags] + public enum GuitarButton : ushort + { + StrumUp = GamepadButton.DpadUp, + StrumDown = GamepadButton.DpadDown, + GreenFret = GamepadButton.A, + RedFret = GamepadButton.B, + YellowFret = GamepadButton.Y, + BlueFret = GamepadButton.X, + OrangeFret = GamepadButton.LeftBumper, + LowerFretFlag = GamepadButton.LeftStickPress + } + + /// + /// Flags used in and + /// + [Flags] + public enum GuitarFret : byte + { + Green = 0x01, + Red = 0x02, + Yellow = 0x04, + Blue = 0x08, + Orange = 0x10 + } + + /// + /// An input report from a guitar. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct GuitarInput + { + public ushort Buttons; + public byte Tilt; + public byte WhammyBar; + public byte PickupSwitch; + public byte UpperFrets; + public byte LowerFrets; + byte unk1; + byte unk2; + byte unk3; + + public bool Green => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Green) != 0; + public bool Red => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Red) != 0; + public bool Yellow => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Yellow) != 0; + public bool Blue => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Blue) != 0; + public bool Orange => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Orange) != 0; + + public bool LowerFretFlag => (Buttons & (ushort)GuitarButton.LowerFretFlag) != 0; + } +} \ No newline at end of file diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index d57e528..2639855 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -237,9 +237,9 @@ private void ParseDrums(DrumInput report) // Pedals device.SetButtonState(Xbox360Button.LeftShoulder, - (report.Buttons & (ushort)DrumInput.Button.KickOne) != 0); + (report.Buttons & (ushort)DrumButton.KickOne) != 0); device.SetButtonState(Xbox360Button.LeftThumb, - (report.Buttons & (ushort)DrumInput.Button.KickTwo) != 0); + (report.Buttons & (ushort)DrumButton.KickTwo) != 0); // Velocities device.SetAxisValue( diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs index 4aaac59..6f42ad6 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/VjoyMapper.cs @@ -217,8 +217,8 @@ private void ParseDrums(DrumInput report) SetButton(VjoyButton.Eight, report.GreenCymbal != 0); // Kick pedals - SetButton(VjoyButton.Five, (report.Buttons & (ushort)DrumInput.Button.KickOne) != 0); - SetButton(VjoyButton.Nine, (report.Buttons & (ushort)DrumInput.Button.KickTwo) != 0); + SetButton(VjoyButton.Five, (report.Buttons & (ushort)DrumButton.KickOne) != 0); + SetButton(VjoyButton.Nine, (report.Buttons & (ushort)DrumButton.KickTwo) != 0); } /// diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index b5906fe..efd25c9 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -108,8 +108,11 @@ + + + + - From 53f678d1a5677eb4784fe7bf90a372592a352561 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 00:50:51 -0700 Subject: [PATCH 109/437] Don't parse chunk message data until after the sequence is complete --- Program/PacketParsing/XboxDevice.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 761a343..b8f5579 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -95,6 +95,7 @@ public unsafe void ParseCommand(ReadOnlySpan commandData) } commandData.CopyTo(chunkBuffer.AsSpan(bufferIndex, commandData.Length)); + return; } } } From d42253e30f482f822dcf42a357973eb818c86ded Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 01:58:32 -0700 Subject: [PATCH 110/437] Add chunk buffer doc comment --- Program/PacketParsing/XboxDevice.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index b8f5579..c5d5735 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -21,6 +21,10 @@ class XboxDevice /// Mapper interface to use. /// private IDeviceMapper deviceMapper; + + /// + /// Buffer used to assemble chunked packets. + /// private byte[] chunkBuffer; /// From ca8d3e44e674daffb7aa0c2472530428bfbc7bad Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 02:02:45 -0700 Subject: [PATCH 111/437] Move header length and sequence ID check to XboxDevice --- Program/PacketParsing/VigemMapper.cs | 18 ------------------ Program/PacketParsing/VjoyMapper.cs | 18 ------------------ Program/PacketParsing/XboxDevice.cs | 24 ++++++++++++++++++++++++ 3 files changed, 24 insertions(+), 36 deletions(-) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 2639855..2c9f975 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -20,8 +20,6 @@ class VigemMapper : IDeviceMapper /// private bool deviceConnected = false; - private byte prevInputSeqCount = 0xFF; - /// /// Creates a new VigemMapper. /// @@ -77,22 +75,6 @@ public unsafe void ParseInput(CommandHeader header, ReadOnlySpan data) return; } - // Ensure lengths match - if (header.DataLength != data.Length) - { - // This is probably a bug, emit a debug message - Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {data.Length}"); - return; - } - - // Don't parse the same report twice - if (header.SequenceCount == prevInputSeqCount) - { - return; - } - - prevInputSeqCount = header.SequenceCount; - int length = header.DataLength; if (length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) { diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs index 6f42ad6..d015b41 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/VjoyMapper.cs @@ -12,8 +12,6 @@ class VjoyMapper : IDeviceMapper private vJoy.JoystickState state = new vJoy.JoystickState(); private uint deviceId = 0; - private byte prevInputSeqCount = 0xFF; - /// /// Creates a new VjoyMapper. /// @@ -47,22 +45,6 @@ public VjoyMapper() /// public unsafe void ParseInput(CommandHeader header, ReadOnlySpan data) { - // Ensure lengths match - if (header.DataLength != data.Length) - { - // This is probably a bug, emit a debug message - Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {data.Length}"); - return; - } - - // Don't parse the same report twice - if (header.SequenceCount == prevInputSeqCount) - { - return; - } - - header.SequenceCount = prevInputSeqCount; - int length = header.DataLength; if (length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) { diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index c5d5735..076eb31 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; @@ -27,6 +28,11 @@ class XboxDevice /// private byte[] chunkBuffer; + /// + /// The previous sequence ID for received command IDs. + /// + private readonly Dictionary previousSequenceIds = new Dictionary(); + /// /// Creates a new XboxDevice with the given device ID and parsing mode. /// @@ -104,6 +110,24 @@ public unsafe void ParseCommand(ReadOnlySpan commandData) } } + // Ensure lengths match + if (header.DataLength != commandData.Length) + { + // This is probably a bug + Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {commandData.Length}"); + return; + } + + // Don't handle the same packet twice + if (!previousSequenceIds.TryGetValue(header.CommandId, out byte previousSequence)) + { + previousSequenceIds.Add(header.CommandId, header.SequenceCount); + } + else if (header.SequenceCount == previousSequence) + { + return; + } + switch (header.CommandId) { case CommandId.Input: From fa0a5b5567c84f525efbbdc5332fa1fb04ae8b64 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 03:10:56 -0700 Subject: [PATCH 112/437] Switch IDeviceMapper and XboxDevice to IDisposable instead of Close() --- Program/PacketParsing/Backends/PcapBackend.cs | 2 +- Program/PacketParsing/IDeviceMapper.cs | 7 +---- Program/PacketParsing/VigemMapper.cs | 28 +++++++++++++------ Program/PacketParsing/VjoyMapper.cs | 14 ++++++++-- Program/PacketParsing/XboxDevice.cs | 19 +++++++++---- 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 3e262f0..ec89329 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -87,7 +87,7 @@ public static void StopCapture() // Clean up devices foreach (XboxDevice device in devices.Values) { - device.Close(); + device.Dispose(); } devices.Clear(); } diff --git a/Program/PacketParsing/IDeviceMapper.cs b/Program/PacketParsing/IDeviceMapper.cs index 13ad670..cb5f2da 100644 --- a/Program/PacketParsing/IDeviceMapper.cs +++ b/Program/PacketParsing/IDeviceMapper.cs @@ -5,16 +5,11 @@ namespace RB4InstrumentMapper.Parsing /// /// Common interface for device mappers. /// - interface IDeviceMapper + interface IDeviceMapper : IDisposable { /// /// Parses an input packet. /// void ParseInput(CommandHeader header, ReadOnlySpan data); - - /// - /// Performs cleanup for the mapper. - /// - void Close(); } } \ No newline at end of file diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 2c9f975..6de048e 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -46,7 +46,7 @@ public VigemMapper() /// ~VigemMapper() { - Close(); + Dispose(false); } /// @@ -69,6 +69,9 @@ void ReceiveUserIndex(object sender, Xbox360FeedbackReceivedEventArgs args) /// public unsafe void ParseInput(CommandHeader header, ReadOnlySpan data) { + if (device == null) + throw new ObjectDisposedException(nameof(device)); + // Don't process if not connected if (!deviceConnected) { @@ -273,15 +276,24 @@ short ByteToVelocityNegative(byte value) /// /// Performs cleanup for the object. /// - public void Close() + public void Dispose() { - // Reset report - try { device.ResetReport(); } catch {} - try { device.SubmitReport(); } catch {} + Dispose(true); + GC.SuppressFinalize(this); + } - // Disconnect device - try { device?.Disconnect(); } catch {} - device = null; + private void Dispose(bool disposing) + { + if (disposing) + { + // Reset report + try { device?.ResetReport(); } catch {} + try { device?.SubmitReport(); } catch {} + + // Disconnect device + try { device?.Disconnect(); } catch {} + device = null; + } } } } diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs index d015b41..bd63d97 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/VjoyMapper.cs @@ -37,7 +37,7 @@ public VjoyMapper() ///
~VjoyMapper() { - Close(); + Dispose(false); } /// @@ -45,6 +45,9 @@ public VjoyMapper() /// public unsafe void ParseInput(CommandHeader header, ReadOnlySpan data) { + if (deviceId == 0) + throw new ObjectDisposedException("this"); + int length = header.DataLength; if (length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) { @@ -206,7 +209,13 @@ private void ParseDrums(DrumInput report) /// /// Performs cleanup for the vJoy mapper. /// - public void Close() + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) { // Reset report state.Reset(); @@ -214,6 +223,7 @@ public void Close() // Free device VjoyClient.ReleaseDevice(deviceId); + deviceId = 0; } } } diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 076eb31..06eb907 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -14,7 +14,7 @@ public enum MappingMode /// /// Interface for Xbox devices. /// - class XboxDevice + class XboxDevice : IDisposable { public static MappingMode MapperMode; @@ -55,7 +55,7 @@ public XboxDevice() ///
~XboxDevice() { - Close(); + Dispose(false); } /// @@ -143,10 +143,19 @@ public unsafe void ParseCommand(ReadOnlySpan commandData) /// /// Performs cleanup for the device. /// - public void Close() + public void Dispose() { - deviceMapper?.Close(); - deviceMapper = null; + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + deviceMapper?.Dispose(); + deviceMapper = null; + } } } } \ No newline at end of file From 504efc73e42bca01e788f27680a91a26cbf0b6a4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 03:30:52 -0700 Subject: [PATCH 113/437] Move detection of input report commands to device mappers --- Program/PacketParsing/IDeviceMapper.cs | 4 ++-- Program/PacketParsing/VigemMapper.cs | 25 ++++++++++++++++++++----- Program/PacketParsing/VjoyMapper.cs | 25 ++++++++++++++++++++----- Program/PacketParsing/XboxDevice.cs | 7 ++----- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/Program/PacketParsing/IDeviceMapper.cs b/Program/PacketParsing/IDeviceMapper.cs index cb5f2da..ae507ab 100644 --- a/Program/PacketParsing/IDeviceMapper.cs +++ b/Program/PacketParsing/IDeviceMapper.cs @@ -8,8 +8,8 @@ namespace RB4InstrumentMapper.Parsing interface IDeviceMapper : IDisposable { /// - /// Parses an input packet. + /// Handles an incoming packet. /// - void ParseInput(CommandHeader header, ReadOnlySpan data); + void HandlePacket(CommandId command, ReadOnlySpan data); } } \ No newline at end of file diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/VigemMapper.cs index 6de048e..f109798 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/VigemMapper.cs @@ -65,25 +65,40 @@ void ReceiveUserIndex(object sender, Xbox360FeedbackReceivedEventArgs args) } /// - /// Parses an input report. + /// Handles an incoming packet. /// - public unsafe void ParseInput(CommandHeader header, ReadOnlySpan data) + public void HandlePacket(CommandId command, ReadOnlySpan data) { if (device == null) throw new ObjectDisposedException(nameof(device)); + switch (command) + { + case CommandId.Input: + ParseInput(data); + break; + + default: + break; + } + } + + /// + /// Parses an input report. + /// + public unsafe void ParseInput(ReadOnlySpan data) + { // Don't process if not connected if (!deviceConnected) { return; } - int length = header.DataLength; - if (length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) + if (data.Length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) { ParseGuitar(guitarReport); } - else if (length == sizeof(DrumInput) && MemoryMarshal.TryRead(data, out DrumInput drumReport)) + else if (data.Length == sizeof(DrumInput) && MemoryMarshal.TryRead(data, out DrumInput drumReport)) { ParseDrums(drumReport); } diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/VjoyMapper.cs index bd63d97..0ad7785 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/VjoyMapper.cs @@ -41,19 +41,34 @@ public VjoyMapper() } /// - /// Parses an input report. + /// Handles an incoming packet. /// - public unsafe void ParseInput(CommandHeader header, ReadOnlySpan data) + public void HandlePacket(CommandId command, ReadOnlySpan data) { if (deviceId == 0) throw new ObjectDisposedException("this"); - int length = header.DataLength; - if (length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) + switch (command) + { + case CommandId.Input: + ParseInput(data); + break; + + default: + break; + } + } + + /// + /// Parses an input report. + /// + public unsafe void ParseInput(ReadOnlySpan data) + { + if (data.Length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) { ParseGuitar(guitarReport); } - else if (length == sizeof(DrumInput) && MemoryMarshal.TryRead(data, out DrumInput drumReport)) + else if (data.Length == sizeof(DrumInput) && MemoryMarshal.TryRead(data, out DrumInput drumReport)) { ParseDrums(drumReport); } diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 06eb907..3c78e89 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -130,12 +130,9 @@ public unsafe void ParseCommand(ReadOnlySpan commandData) switch (header.CommandId) { - case CommandId.Input: - deviceMapper.ParseInput(header, commandData.Slice(sizeof(CommandHeader))); - break; - default: - // Don't do anything with unrecognized command IDs + // Hand off unrecognized commands to the mapper + deviceMapper.HandlePacket(header.CommandId, commandData); break; } } From 7d2bce70ed2853e5d3a6bde518ad02c08cd4a501 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 20:31:53 -0700 Subject: [PATCH 114/437] Rename current mappers to Fallback, move to Mappers folder --- .../{VigemMapper.cs => Mappers/FallbackVigemMapper.cs} | 6 +++--- .../{VjoyMapper.cs => Mappers/FallbackVjoyMapper.cs} | 6 +++--- Program/PacketParsing/XboxDevice.cs | 4 ++-- Program/RB4InstrumentMapper.csproj | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename Program/PacketParsing/{VigemMapper.cs => Mappers/FallbackVigemMapper.cs} (98%) rename Program/PacketParsing/{VjoyMapper.cs => Mappers/FallbackVjoyMapper.cs} (98%) diff --git a/Program/PacketParsing/VigemMapper.cs b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs similarity index 98% rename from Program/PacketParsing/VigemMapper.cs rename to Program/PacketParsing/Mappers/FallbackVigemMapper.cs index f109798..28e716a 100644 --- a/Program/PacketParsing/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs @@ -8,7 +8,7 @@ namespace RB4InstrumentMapper.Parsing { - class VigemMapper : IDeviceMapper + class FallbackVigemMapper : IDeviceMapper { /// /// The device to map to. @@ -23,7 +23,7 @@ class VigemMapper : IDeviceMapper /// /// Creates a new VigemMapper. /// - public VigemMapper() + public FallbackVigemMapper() { device = VigemClient.CreateDevice(); device.FeedbackReceived += ReceiveUserIndex; @@ -44,7 +44,7 @@ public VigemMapper() /// /// Performs cleanup on object finalization. /// - ~VigemMapper() + ~FallbackVigemMapper() { Dispose(false); } diff --git a/Program/PacketParsing/VjoyMapper.cs b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs similarity index 98% rename from Program/PacketParsing/VjoyMapper.cs rename to Program/PacketParsing/Mappers/FallbackVjoyMapper.cs index 0ad7785..4a3265f 100644 --- a/Program/PacketParsing/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs @@ -7,7 +7,7 @@ namespace RB4InstrumentMapper.Parsing { - class VjoyMapper : IDeviceMapper + class FallbackVjoyMapper : IDeviceMapper { private vJoy.JoystickState state = new vJoy.JoystickState(); private uint deviceId = 0; @@ -15,7 +15,7 @@ class VjoyMapper : IDeviceMapper /// /// Creates a new VjoyMapper. /// - public VjoyMapper() + public FallbackVjoyMapper() { deviceId = VjoyClient.GetNextAvailableID(); if (deviceId == 0) @@ -35,7 +35,7 @@ public VjoyMapper() /// /// Performs cleanup on object finalization. /// - ~VjoyMapper() + ~FallbackVjoyMapper() { Dispose(false); } diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 3c78e89..3d87c42 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -41,11 +41,11 @@ public XboxDevice() switch (MapperMode) { case MappingMode.ViGEmBus: - deviceMapper = new VigemMapper(); + deviceMapper = new FallbackVigemMapper(); break; case MappingMode.vJoy: - deviceMapper = new VjoyMapper(); + deviceMapper = new FallbackVjoyMapper(); break; } } diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index efd25c9..9c2cdd7 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -108,6 +108,8 @@ + + @@ -116,8 +118,6 @@ - - From 2e09f14e1d97e7a09f3818bb54f7170197cbdf90 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 21:12:49 -0700 Subject: [PATCH 115/437] Minor cleanup to parsing exceptions - Remove ParseException - Add VjoyException - Don't wrap exceptions thrown by ViGEmBus things - Catch exceptions in general when making new devices, not just ParseException --- Program/PacketParsing/Backends/PcapBackend.cs | 2 +- .../Mappers/FallbackVigemMapper.cs | 13 +----------- .../Mappers/FallbackVjoyMapper.cs | 4 ++-- Program/PacketParsing/ParseException.cs | 20 ------------------- Program/RB4InstrumentMapper.csproj | 2 +- Program/Vjoy/VjoyException.cs | 20 +++++++++++++++++++ 6 files changed, 25 insertions(+), 36 deletions(-) delete mode 100644 Program/PacketParsing/ParseException.cs create mode 100644 Program/Vjoy/VjoyException.cs diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index ec89329..736f408 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -125,7 +125,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) { device = new XboxDevice(); } - catch (ParseException ex) + catch (Exception ex) { canHandleNewDevices = false; Console.WriteLine("Device limit reached, or an error occured when creating virtual device. No more devices will be registered."); diff --git a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs index 28e716a..d4a63a9 100644 --- a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; -using Nefarius.ViGEm.Client.Exceptions; using Nefarius.ViGEm.Client.Targets; using Nefarius.ViGEm.Client.Targets.Xbox360; using RB4InstrumentMapper.Vigem; @@ -27,17 +26,7 @@ public FallbackVigemMapper() { device = VigemClient.CreateDevice(); device.FeedbackReceived += ReceiveUserIndex; - - try - { - device.Connect(); - } - catch (VigemNoFreeSlotException ex) - { - device = null; - throw new ParseException("ViGEmBus device slots are full.", ex); - } - + device.Connect(); device.AutoSubmitReport = false; } diff --git a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs index 4a3265f..9d27eab 100644 --- a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs @@ -20,12 +20,12 @@ public FallbackVjoyMapper() deviceId = VjoyClient.GetNextAvailableID(); if (deviceId == 0) { - throw new ParseException("No new vJoy devices are available."); + throw new VjoyException("No vJoy devices are available."); } if (!VjoyClient.AcquireDevice(deviceId)) { - throw new ParseException($"Could not claim vJoy device {deviceId}."); + throw new VjoyException($"Could not claim vJoy device {deviceId}."); } state.bDevice = (byte)deviceId; diff --git a/Program/PacketParsing/ParseException.cs b/Program/PacketParsing/ParseException.cs deleted file mode 100644 index f31bc17..0000000 --- a/Program/PacketParsing/ParseException.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace RB4InstrumentMapper.Parsing -{ - /// - /// Used to wrap or create exceptions that should be handled as part of parsing. - /// - class ParseException : Exception - { - public ParseException() - : base() {} - - public ParseException(string message) - : base(message) {} - - public ParseException(string message, Exception innerException) - : base(message, innerException) {} - - } -} \ No newline at end of file diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 9c2cdd7..1b649f6 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -116,7 +116,6 @@ - @@ -124,6 +123,7 @@ + MSBuild:Compile diff --git a/Program/Vjoy/VjoyException.cs b/Program/Vjoy/VjoyException.cs new file mode 100644 index 0000000..a14866e --- /dev/null +++ b/Program/Vjoy/VjoyException.cs @@ -0,0 +1,20 @@ +using System; + +namespace RB4InstrumentMapper.Vjoy +{ + /// + /// A vJoy exception. + /// + class VjoyException : Exception + { + public VjoyException() + : base() {} + + public VjoyException(string message) + : base(message) {} + + public VjoyException(string message, Exception innerException) + : base(message, innerException) {} + + } +} \ No newline at end of file From e891841f3a36e519d04b48db443e335101838503 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 21:42:24 -0700 Subject: [PATCH 116/437] Move Vjoy SetButton method to VjoyExtensions --- .../Mappers/FallbackVjoyMapper.cs | 56 +++++++------------ Program/Vjoy/VjoyExtensions.cs | 17 ++++++ 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs index 9d27eab..9becae7 100644 --- a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs @@ -82,32 +82,16 @@ public unsafe void ParseInput(ReadOnlySpan data) VjoyClient.UpdateDevice(deviceId, ref state); } - /// - /// Sets the state of the specified button. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected void SetButton(VjoyButton button, bool set) - { - if (set) - { - state.Buttons |= (uint)button; - } - else - { - state.Buttons &= (uint)~button; - } - } - /// /// Parses common button data from an input report. /// private void ParseCoreButtons(GamepadButton buttons) { // Menu - SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); + state.SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); // Options - SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); + state.SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); VjoyPoV direction; if ((buttons & GamepadButton.DpadUp) != 0) @@ -169,11 +153,11 @@ private void ParseGuitar(GuitarInput report) // Buttons ParseCoreButtons((GamepadButton)report.Buttons); - SetButton(VjoyButton.One, report.Green); - SetButton(VjoyButton.Two, report.Red); - SetButton(VjoyButton.Three, report.Yellow); - SetButton(VjoyButton.Four, report.Blue); - SetButton(VjoyButton.Five, report.Orange); + state.SetButton(VjoyButton.One, report.Green); + state.SetButton(VjoyButton.Two, report.Red); + state.SetButton(VjoyButton.Three, report.Yellow); + state.SetButton(VjoyButton.Four, report.Blue); + state.SetButton(VjoyButton.Five, report.Orange); // Whammy // Value ranges from 0 (not pressed) to 255 (fully pressed) @@ -200,25 +184,25 @@ private void ParseDrums(DrumInput report) ParseCoreButtons(buttons); // Face buttons - SetButton(VjoyButton.Four, (buttons & GamepadButton.A) != 0); - SetButton(VjoyButton.One, (buttons & GamepadButton.B) != 0); - SetButton(VjoyButton.Three, (buttons & GamepadButton.X) != 0); - SetButton(VjoyButton.Two, (buttons & GamepadButton.Y) != 0); + state.SetButton(VjoyButton.Four, (buttons & GamepadButton.A) != 0); + state.SetButton(VjoyButton.One, (buttons & GamepadButton.B) != 0); + state.SetButton(VjoyButton.Three, (buttons & GamepadButton.X) != 0); + state.SetButton(VjoyButton.Two, (buttons & GamepadButton.Y) != 0); // Pads - SetButton(VjoyButton.One, report.RedPad != 0); - SetButton(VjoyButton.Two, report.YellowPad != 0); - SetButton(VjoyButton.Three, report.BluePad != 0); - SetButton(VjoyButton.Four, report.GreenPad != 0); + state.SetButton(VjoyButton.One, report.RedPad != 0); + state.SetButton(VjoyButton.Two, report.YellowPad != 0); + state.SetButton(VjoyButton.Three, report.BluePad != 0); + state.SetButton(VjoyButton.Four, report.GreenPad != 0); // Cymbals - SetButton(VjoyButton.Six, report.YellowCymbal != 0); - SetButton(VjoyButton.Seven, report.BlueCymbal != 0); - SetButton(VjoyButton.Eight, report.GreenCymbal != 0); + state.SetButton(VjoyButton.Six, report.YellowCymbal != 0); + state.SetButton(VjoyButton.Seven, report.BlueCymbal != 0); + state.SetButton(VjoyButton.Eight, report.GreenCymbal != 0); // Kick pedals - SetButton(VjoyButton.Five, (report.Buttons & (ushort)DrumButton.KickOne) != 0); - SetButton(VjoyButton.Nine, (report.Buttons & (ushort)DrumButton.KickTwo) != 0); + state.SetButton(VjoyButton.Five, (report.Buttons & (ushort)DrumButton.KickOne) != 0); + state.SetButton(VjoyButton.Nine, (report.Buttons & (ushort)DrumButton.KickTwo) != 0); } /// diff --git a/Program/Vjoy/VjoyExtensions.cs b/Program/Vjoy/VjoyExtensions.cs index 976c44d..f7cb2d8 100644 --- a/Program/Vjoy/VjoyExtensions.cs +++ b/Program/Vjoy/VjoyExtensions.cs @@ -1,9 +1,26 @@ +using System.Runtime.CompilerServices; using vJoyInterfaceWrap; namespace RB4InstrumentMapper.Vjoy { public static class VjoyExtensions { + /// + /// Sets the state of the specified button. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetButton(this vJoy.JoystickState state, VjoyButton button, bool set) + { + if (set) + { + state.Buttons |= (uint)button; + } + else + { + state.Buttons &= (uint)~button; + } + } + /// /// Resets the values of this state. /// From b8fee208397dfb2148aca687095c5f3825503e24 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 21:54:13 -0700 Subject: [PATCH 117/437] Create base Vigem/VjoyMapper classes --- .../Mappers/FallbackVigemMapper.cs | 118 +++------------ .../Mappers/FallbackVjoyMapper.cs | 142 +++--------------- Program/PacketParsing/Mappers/VigemMapper.cs | 93 ++++++++++++ Program/PacketParsing/Mappers/VjoyMapper.cs | 128 ++++++++++++++++ Program/RB4InstrumentMapper.csproj | 2 + 5 files changed, 263 insertions(+), 220 deletions(-) create mode 100644 Program/PacketParsing/Mappers/VigemMapper.cs create mode 100644 Program/PacketParsing/Mappers/VjoyMapper.cs diff --git a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs index d4a63a9..a8f79a7 100644 --- a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs @@ -1,66 +1,23 @@ using System; -using System.Diagnostics; using System.Runtime.InteropServices; -using Nefarius.ViGEm.Client.Targets; using Nefarius.ViGEm.Client.Targets.Xbox360; -using RB4InstrumentMapper.Vigem; namespace RB4InstrumentMapper.Parsing { - class FallbackVigemMapper : IDeviceMapper + /// + /// The ViGEmBus mapper used when device type could not be determined. Maps based on report length. + /// + class FallbackVigemMapper : VigemMapper { - /// - /// The device to map to. - /// - private IXbox360Controller device; - - /// - /// Whether or not feedback has been received to indicate that the device has connected. - /// - private bool deviceConnected = false; - - /// - /// Creates a new VigemMapper. - /// - public FallbackVigemMapper() - { - device = VigemClient.CreateDevice(); - device.FeedbackReceived += ReceiveUserIndex; - device.Connect(); - device.AutoSubmitReport = false; - } - - /// - /// Performs cleanup on object finalization. - /// - ~FallbackVigemMapper() - { - Dispose(false); - } - - /// - /// Temporary event handler for logging the user index of a ViGEm device. - /// - void ReceiveUserIndex(object sender, Xbox360FeedbackReceivedEventArgs args) + public FallbackVigemMapper() : base() { - // Device has connected - deviceConnected = true; - - // Log the user index - Console.WriteLine($"Created new ViGEmBus device with user index {args.LedNumber}"); - - // Unregister the event handler - (sender as IXbox360Controller).FeedbackReceived -= ReceiveUserIndex; } /// /// Handles an incoming packet. /// - public void HandlePacket(CommandId command, ReadOnlySpan data) + protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) { - if (device == null) - throw new ObjectDisposedException(nameof(device)); - switch (command) { case CommandId.Input: @@ -75,14 +32,8 @@ public void HandlePacket(CommandId command, ReadOnlySpan data) /// /// Parses an input report. /// - public unsafe void ParseInput(ReadOnlySpan data) + private unsafe void ParseInput(ReadOnlySpan data) { - // Don't process if not connected - if (!deviceConnected) - { - return; - } - if (data.Length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) { ParseGuitar(guitarReport); @@ -93,7 +44,7 @@ public unsafe void ParseInput(ReadOnlySpan data) } else { - // Report is not valid + // Not handled return; } @@ -102,13 +53,13 @@ public unsafe void ParseInput(ReadOnlySpan data) } /// - /// Parses common button data from an input report. + /// Parses guitar input data from an input report. /// - private void ParseCoreButtons(GamepadButton buttons) + private void ParseGuitar(GuitarInput report) { - // Menu + // Menu and Options + var buttons = (GamepadButton)report.Buttons; device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); - // Options device.SetButtonState(Xbox360Button.Back, (buttons & GamepadButton.Options) != 0); // Dpad @@ -117,17 +68,6 @@ private void ParseCoreButtons(GamepadButton buttons) device.SetButtonState(Xbox360Button.Left, (buttons & GamepadButton.DpadLeft) != 0); device.SetButtonState(Xbox360Button.Right, (buttons & GamepadButton.DpadRight) != 0); - // Other buttons are not mapped here since they may have specific uses - } - - /// - /// Parses guitar input data from an input report. - /// - private void ParseGuitar(GuitarInput report) - { - // Buttons - ParseCoreButtons((GamepadButton)report.Buttons); - // Frets device.SetButtonState(Xbox360Button.A, report.Green); device.SetButtonState(Xbox360Button.B, report.Red); @@ -160,9 +100,16 @@ private void ParseGuitar(GuitarInput report) /// private void ParseDrums(DrumInput report) { - // Buttons + // Menu and Options var buttons = (GamepadButton)report.Buttons; - ParseCoreButtons(buttons); + device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); + device.SetButtonState(Xbox360Button.Back, (buttons & GamepadButton.Options) != 0); + + // Dpad + device.SetButtonState(Xbox360Button.Up, (buttons & GamepadButton.DpadUp) != 0); + device.SetButtonState(Xbox360Button.Down, (buttons & GamepadButton.DpadDown) != 0); + device.SetButtonState(Xbox360Button.Left, (buttons & GamepadButton.DpadLeft) != 0); + device.SetButtonState(Xbox360Button.Right, (buttons & GamepadButton.DpadRight) != 0); // Pads and cymbals byte redPad = report.RedPad; @@ -276,28 +223,5 @@ short ByteToVelocityNegative(byte value) ); } } - - /// - /// Performs cleanup for the object. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - // Reset report - try { device?.ResetReport(); } catch {} - try { device?.SubmitReport(); } catch {} - - // Disconnect device - try { device?.Disconnect(); } catch {} - device = null; - } - } } } diff --git a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs index 9becae7..77dc049 100644 --- a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs @@ -1,53 +1,23 @@ using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using RB4InstrumentMapper.Vjoy; -using vJoyInterfaceWrap; namespace RB4InstrumentMapper.Parsing { - class FallbackVjoyMapper : IDeviceMapper + /// + /// The vJoy mapper used when device type could not be determined. Maps based on report length. + /// + class FallbackVjoyMapper : VjoyMapper { - private vJoy.JoystickState state = new vJoy.JoystickState(); - private uint deviceId = 0; - - /// - /// Creates a new VjoyMapper. - /// - public FallbackVjoyMapper() - { - deviceId = VjoyClient.GetNextAvailableID(); - if (deviceId == 0) - { - throw new VjoyException("No vJoy devices are available."); - } - - if (!VjoyClient.AcquireDevice(deviceId)) - { - throw new VjoyException($"Could not claim vJoy device {deviceId}."); - } - - state.bDevice = (byte)deviceId; - Console.WriteLine($"Acquired vJoy device with ID of {deviceId}"); - } - - /// - /// Performs cleanup on object finalization. - /// - ~FallbackVjoyMapper() + public FallbackVjoyMapper() : base() { - Dispose(false); } /// /// Handles an incoming packet. /// - public void HandlePacket(CommandId command, ReadOnlySpan data) + protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) { - if (deviceId == 0) - throw new ObjectDisposedException("this"); - switch (command) { case CommandId.Input: @@ -74,7 +44,7 @@ public unsafe void ParseInput(ReadOnlySpan data) } else { - // Report is not valid + // Not handled return; } @@ -83,75 +53,17 @@ public unsafe void ParseInput(ReadOnlySpan data) } /// - /// Parses common button data from an input report. + /// Parses guitar input data from an input report. /// - private void ParseCoreButtons(GamepadButton buttons) + private void ParseGuitar(GuitarInput report) { - // Menu + // Menu and Options + var buttons = (GamepadButton)report.Buttons; state.SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); - - // Options state.SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); - VjoyPoV direction; - if ((buttons & GamepadButton.DpadUp) != 0) - { - if ((buttons & GamepadButton.DpadLeft) != 0) - { - direction = VjoyPoV.UpLeft; - } - else if ((buttons & GamepadButton.DpadRight) != 0) - { - direction = VjoyPoV.UpRight; - } - else - { - direction = VjoyPoV.Up; - } - } - else if ((buttons & GamepadButton.DpadDown) != 0) - { - if ((buttons & GamepadButton.DpadLeft) != 0) - { - direction = VjoyPoV.DownLeft; - } - else if ((buttons & GamepadButton.DpadRight) != 0) - { - direction = VjoyPoV.DownRight; - } - else - { - direction = VjoyPoV.Down; - } - } - else - { - if ((buttons & GamepadButton.DpadLeft) != 0) - { - direction = VjoyPoV.Left; - } - else if ((buttons & GamepadButton.DpadRight) != 0) - { - direction = VjoyPoV.Right; - } - else - { - direction = VjoyPoV.Neutral; - } - } - - state.bHats = (uint)direction; - - // Other buttons are not mapped here since they may have specific uses - } - - /// - /// Parses guitar input data from an input report. - /// - private void ParseGuitar(GuitarInput report) - { - // Buttons - ParseCoreButtons((GamepadButton)report.Buttons); + // D-pad + ParseDpad(buttons); state.SetButton(VjoyButton.One, report.Green); state.SetButton(VjoyButton.Two, report.Red); @@ -179,9 +91,13 @@ private void ParseGuitar(GuitarInput report) /// private void ParseDrums(DrumInput report) { - // Buttons + // Menu and Options var buttons = (GamepadButton)report.Buttons; - ParseCoreButtons(buttons); + state.SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); + state.SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); + + // D-pad + ParseDpad(buttons); // Face buttons state.SetButton(VjoyButton.Four, (buttons & GamepadButton.A) != 0); @@ -204,25 +120,5 @@ private void ParseDrums(DrumInput report) state.SetButton(VjoyButton.Five, (report.Buttons & (ushort)DrumButton.KickOne) != 0); state.SetButton(VjoyButton.Nine, (report.Buttons & (ushort)DrumButton.KickTwo) != 0); } - - /// - /// Performs cleanup for the vJoy mapper. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - // Reset report - state.Reset(); - VjoyClient.UpdateDevice(deviceId, ref state); - - // Free device - VjoyClient.ReleaseDevice(deviceId); - deviceId = 0; - } } } diff --git a/Program/PacketParsing/Mappers/VigemMapper.cs b/Program/PacketParsing/Mappers/VigemMapper.cs new file mode 100644 index 0000000..53b04aa --- /dev/null +++ b/Program/PacketParsing/Mappers/VigemMapper.cs @@ -0,0 +1,93 @@ +using System; +using Nefarius.ViGEm.Client.Targets; +using Nefarius.ViGEm.Client.Targets.Xbox360; +using RB4InstrumentMapper.Vigem; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// A mapper that maps to a ViGEmBus device. + /// + abstract class VigemMapper : IDeviceMapper + { + /// + /// The device to map to. + /// + protected IXbox360Controller device; + + /// + /// Whether or not the emulated Xbox 360 controller has connected fully. + /// + protected bool deviceConnected = false; + + public VigemMapper() + { + device = VigemClient.CreateDevice(); + device.FeedbackReceived += DeviceConnected; + device.Connect(); + device.AutoSubmitReport = false; + } + + /// + /// Performs cleanup on object finalization. + /// + ~VigemMapper() + { + Dispose(false); + } + + /// + /// Temporary event handler for logging the user index of a ViGEm device. + /// + private void DeviceConnected(object sender, Xbox360FeedbackReceivedEventArgs args) + { + // Device has connected + deviceConnected = true; + + // Log the user index + Console.WriteLine($"Created new ViGEmBus device with user index {args.LedNumber}"); + + // Unregister the event handler + (sender as IXbox360Controller).FeedbackReceived -= DeviceConnected; + } + + /// + /// Handles an incoming packet. + /// + public void HandlePacket(CommandId command, ReadOnlySpan data) + { + if (device == null) + throw new ObjectDisposedException(nameof(device)); + + if (!deviceConnected) + return; + + OnPacketReceived(command, data); + } + + protected abstract void OnPacketReceived(CommandId command, ReadOnlySpan data); + + /// + /// Performs cleanup for the object. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + // Reset report + try { device?.ResetReport(); } catch {} + try { device?.SubmitReport(); } catch {} + + // Disconnect device + try { device?.Disconnect(); } catch {} + device = null; + } + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/VjoyMapper.cs b/Program/PacketParsing/Mappers/VjoyMapper.cs new file mode 100644 index 0000000..82d6dca --- /dev/null +++ b/Program/PacketParsing/Mappers/VjoyMapper.cs @@ -0,0 +1,128 @@ +using System; +using RB4InstrumentMapper.Vjoy; +using vJoyInterfaceWrap; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// A mapper that maps to a vJoy device. + /// + abstract class VjoyMapper : IDeviceMapper + { + protected vJoy.JoystickState state = new vJoy.JoystickState(); + protected uint deviceId = 0; + + public VjoyMapper() + { + deviceId = VjoyClient.GetNextAvailableID(); + if (deviceId == 0) + { + throw new VjoyException("No vJoy devices are available."); + } + + if (!VjoyClient.AcquireDevice(deviceId)) + { + throw new VjoyException($"Could not claim vJoy device {deviceId}."); + } + + state.bDevice = (byte)deviceId; + Console.WriteLine($"Acquired vJoy device with ID of {deviceId}"); + } + + /// + /// Performs cleanup on object finalization. + /// + ~VjoyMapper() + { + Dispose(false); + } + + /// + /// Handles an incoming packet. + /// + public void HandlePacket(CommandId command, ReadOnlySpan data) + { + if (deviceId == 0) + throw new ObjectDisposedException("this"); + + OnPacketReceived(command, data); + } + + protected abstract void OnPacketReceived(CommandId command, ReadOnlySpan data); + + /// + /// Parses the state of the d-pad. + /// + protected static void ParseDpad(ref vJoy.JoystickState state, GamepadButton buttons) + { + VjoyPoV direction; + if ((buttons & GamepadButton.DpadUp) != 0) + { + if ((buttons & GamepadButton.DpadLeft) != 0) + { + direction = VjoyPoV.UpLeft; + } + else if ((buttons & GamepadButton.DpadRight) != 0) + { + direction = VjoyPoV.UpRight; + } + else + { + direction = VjoyPoV.Up; + } + } + else if ((buttons & GamepadButton.DpadDown) != 0) + { + if ((buttons & GamepadButton.DpadLeft) != 0) + { + direction = VjoyPoV.DownLeft; + } + else if ((buttons & GamepadButton.DpadRight) != 0) + { + direction = VjoyPoV.DownRight; + } + else + { + direction = VjoyPoV.Down; + } + } + else + { + if ((buttons & GamepadButton.DpadLeft) != 0) + { + direction = VjoyPoV.Left; + } + else if ((buttons & GamepadButton.DpadRight) != 0) + { + direction = VjoyPoV.Right; + } + else + { + direction = VjoyPoV.Neutral; + } + } + + state.bHats = (uint)direction; + } + + /// + /// Performs cleanup for the vJoy mapper. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + // Reset report + state.Reset(); + VjoyClient.UpdateDevice(deviceId, ref state); + + // Free device + VjoyClient.ReleaseDevice(deviceId); + deviceId = 0; + } + } +} diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 1b649f6..00b75dc 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -110,6 +110,8 @@ + + From 75a43ad1648ebfb523ca2c22a99ab6b5a2d20b85 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 23:01:17 -0700 Subject: [PATCH 118/437] Create separate mappers for each combination of guitar, drums, vJoy, and ViGEmBus --- .../PacketParsing/Mappers/DrumsVigemMapper.cs | 185 ++++++++++++++++++ .../PacketParsing/Mappers/DrumsVjoyMapper.cs | 82 ++++++++ .../Mappers/FallbackVigemMapper.cs | 182 +---------------- .../Mappers/FallbackVjoyMapper.cs | 73 +------ .../Mappers/GuitarVigemMapper.cs | 81 ++++++++ .../PacketParsing/Mappers/GuitarVjoyMapper.cs | 81 ++++++++ Program/RB4InstrumentMapper.csproj | 4 + 7 files changed, 442 insertions(+), 246 deletions(-) create mode 100644 Program/PacketParsing/Mappers/DrumsVigemMapper.cs create mode 100644 Program/PacketParsing/Mappers/DrumsVjoyMapper.cs create mode 100644 Program/PacketParsing/Mappers/GuitarVigemMapper.cs create mode 100644 Program/PacketParsing/Mappers/GuitarVjoyMapper.cs diff --git a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs new file mode 100644 index 0000000..ad80e67 --- /dev/null +++ b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs @@ -0,0 +1,185 @@ +using System; +using System.Runtime.InteropServices; +using Nefarius.ViGEm.Client.Targets; +using Nefarius.ViGEm.Client.Targets.Xbox360; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Maps drumkit inputs to a ViGEmBus device. + /// + class DrumsVigemMapper : VigemMapper + { + public DrumsVigemMapper() : base() + { + } + + /// + /// Handles an incoming packet. + /// + protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + { + switch (command) + { + case CommandId.Input: + ParseInput(data); + break; + + default: + break; + } + } + + // The previous state of the yellow/blue cymbals + int previousDpadCymbals; + // The current state of the d-pad mask from the hit yellow/blue cymbals + int dpadMask; + + /// + /// Parses an input report. + /// + private unsafe void ParseInput(ReadOnlySpan data) + { + if (data.Length != sizeof(DrumInput) || !MemoryMarshal.TryRead(data, out DrumInput drumReport)) + return; + + HandleReport(device, drumReport, ref previousDpadCymbals, ref dpadMask); + + // Send data + device.SubmitReport(); + } + + /// + /// Maps drumkit input data to an Xbox 360 controller. + /// + internal static void HandleReport(IXbox360Controller device, in DrumInput report, ref int previousDpadCymbals, ref int dpadMask) + { + // Constants for the d-pad masks + const int yellowBit = 0x01; + const int blueBit = 0x02; + + // Menu and Options + var buttons = (GamepadButton)report.Buttons; + device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); + device.SetButtonState(Xbox360Button.Back, (buttons & GamepadButton.Options) != 0); + + // Dpad + device.SetButtonState(Xbox360Button.Up, (buttons & GamepadButton.DpadUp) != 0); + device.SetButtonState(Xbox360Button.Down, (buttons & GamepadButton.DpadDown) != 0); + device.SetButtonState(Xbox360Button.Left, (buttons & GamepadButton.DpadLeft) != 0); + device.SetButtonState(Xbox360Button.Right, (buttons & GamepadButton.DpadRight) != 0); + + // Pads and cymbals + byte redPad = report.RedPad; + byte yellowPad = report.YellowPad; + byte bluePad = report.BluePad; + byte greenPad = report.GreenPad; + + byte yellowCym = report.YellowCymbal; + byte blueCym = report.BlueCymbal; + byte greenCym = report.GreenCymbal; + + // Yellow and blue cymbal trigger d-pad up and down respectively on the RB2/3 kit we're emulating + // However, they only trigger one or the other, not both at the same time, so we need to mimic that + int cymbalMask = (yellowCym != 0 ? yellowBit : 0) | (blueCym != 0 ? blueBit : 0); + if (cymbalMask != previousDpadCymbals) + { + if (cymbalMask == 0) + dpadMask = 0; + + // This could probably be done more simply, but this works + if (dpadMask != 0) + { + // D-pad is already set + // Only remove the set value + if ((cymbalMask & yellowBit) == 0) + dpadMask &= ~yellowBit; + else if ((cymbalMask & blueBit) == 0) + dpadMask &= ~blueBit; + } + + // Explicitly check this so that if the d-pad is cleared but the other cymbal is still active, + // it will get set to that cymbal's d-pad + if (dpadMask == 0) + { + // D-pad is not set + // If both cymbals are hit at the same time, yellow takes priority + if ((cymbalMask & yellowBit) != 0) + dpadMask |= yellowBit; + else if ((cymbalMask & blueBit) != 0) + dpadMask |= blueBit; + } + + previousDpadCymbals = cymbalMask; + } + + device.SetButtonState(Xbox360Button.Up, ((dpadMask & yellowBit) != 0) || ((buttons & GamepadButton.DpadUp) != 0)); + device.SetButtonState(Xbox360Button.Down, ((dpadMask & blueBit) != 0) || ((buttons & GamepadButton.DpadDown) != 0)); + + // Color flags + device.SetButtonState(Xbox360Button.B, (redPad != 0) || ((buttons & GamepadButton.B) != 0)); + device.SetButtonState(Xbox360Button.Y, ((yellowPad | yellowCym) != 0) || ((buttons & GamepadButton.Y) != 0)); + device.SetButtonState(Xbox360Button.X, ((bluePad | blueCym) != 0) || ((buttons & GamepadButton.X) != 0)); + device.SetButtonState(Xbox360Button.A, ((greenPad | greenCym) != 0) || ((buttons & GamepadButton.A) != 0)); + + // Pad flag + device.SetButtonState(Xbox360Button.RightThumb, + (redPad | yellowPad | bluePad | greenPad) != 0); + // Cymbal flag + device.SetButtonState(Xbox360Button.RightShoulder, + (yellowCym | blueCym | greenCym) != 0); + + // Pedals + device.SetButtonState(Xbox360Button.LeftShoulder, + (report.Buttons & (ushort)DrumButton.KickOne) != 0); + device.SetButtonState(Xbox360Button.LeftThumb, + (report.Buttons & (ushort)DrumButton.KickTwo) != 0); + + // Velocities + device.SetAxisValue( + Xbox360Axis.LeftThumbX, + ByteToVelocity(redPad) + ); + device.SetAxisValue( + Xbox360Axis.LeftThumbY, + ByteToVelocityNegative((byte)(yellowPad | yellowCym)) + ); + device.SetAxisValue( + Xbox360Axis.RightThumbX, + ByteToVelocity((byte)(bluePad | blueCym)) + ); + device.SetAxisValue( + Xbox360Axis.RightThumbY, + ByteToVelocityNegative((byte)(greenPad | greenCym)) + ); + + /// + /// Scales a byte to a drums velocity value. + /// + short ByteToVelocity(byte value) + { + // Scale the value to fill the byte + value = (byte)(value * 0x11); + + return (short)( + // Bitwise invert to flip the value, then shift down one to exclude the sign bit + (~value.ScaleToUInt16()) >> 1 + ); + } + + /// + /// Scales a byte to a negative drums velocity value. + /// + short ByteToVelocityNegative(byte value) + { + // Scale the value to fill the byte + value = (byte)(value * 0x11); + + return (short)( + // Bitwise invert to flip the value, then shift down one to exclude the sign bit, then add our own + ((~value.ScaleToUInt16()) >> 1) | 0x8000 + ); + } + } + } +} diff --git a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs b/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs new file mode 100644 index 0000000..cff1bec --- /dev/null +++ b/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs @@ -0,0 +1,82 @@ +using System; +using System.Runtime.InteropServices; +using RB4InstrumentMapper.Vjoy; +using vJoyInterfaceWrap; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Maps drumkit inputs to a vJoy device. + /// + class DrumsVjoyMapper : VjoyMapper + { + public DrumsVjoyMapper() : base() + { + } + + /// + /// Handles an incoming packet. + /// + protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + { + switch (command) + { + case CommandId.Input: + ParseInput(data); + break; + + default: + break; + } + } + + /// + /// Parses an input report. + /// + public unsafe void ParseInput(ReadOnlySpan data) + { + if (data.Length != sizeof(DrumInput) || !MemoryMarshal.TryRead(data, out DrumInput guitarReport)) + return; + + HandleReport(ref state, guitarReport); + + // Send data + VjoyClient.UpdateDevice(deviceId, ref state); + } + + /// + /// Maps drumkit input data to a vJoy device. + /// + internal static void HandleReport(ref vJoy.JoystickState state, DrumInput report) + { + // Menu and Options + var buttons = (GamepadButton)report.Buttons; + state.SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); + state.SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); + + // D-pad + ParseDpad(ref state, buttons); + + // Face buttons + state.SetButton(VjoyButton.Four, (buttons & GamepadButton.A) != 0); + state.SetButton(VjoyButton.One, (buttons & GamepadButton.B) != 0); + state.SetButton(VjoyButton.Three, (buttons & GamepadButton.X) != 0); + state.SetButton(VjoyButton.Two, (buttons & GamepadButton.Y) != 0); + + // Pads + state.SetButton(VjoyButton.One, report.RedPad != 0); + state.SetButton(VjoyButton.Two, report.YellowPad != 0); + state.SetButton(VjoyButton.Three, report.BluePad != 0); + state.SetButton(VjoyButton.Four, report.GreenPad != 0); + + // Cymbals + state.SetButton(VjoyButton.Six, report.YellowCymbal != 0); + state.SetButton(VjoyButton.Seven, report.BlueCymbal != 0); + state.SetButton(VjoyButton.Eight, report.GreenCymbal != 0); + + // Kick pedals + state.SetButton(VjoyButton.Five, (report.Buttons & (ushort)DrumButton.KickOne) != 0); + state.SetButton(VjoyButton.Nine, (report.Buttons & (ushort)DrumButton.KickTwo) != 0); + } + } +} diff --git a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs index a8f79a7..41e2dc3 100644 --- a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs @@ -1,6 +1,5 @@ using System; using System.Runtime.InteropServices; -using Nefarius.ViGEm.Client.Targets.Xbox360; namespace RB4InstrumentMapper.Parsing { @@ -29,6 +28,11 @@ protected override void OnPacketReceived(CommandId command, ReadOnlySpan d } } + // The previous state of the yellow/blue cymbals + int previousDpadCymbals; + // The current state of the d-pad mask from the hit yellow/blue cymbals + int dpadMask; + /// /// Parses an input report. /// @@ -36,11 +40,11 @@ private unsafe void ParseInput(ReadOnlySpan data) { if (data.Length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) { - ParseGuitar(guitarReport); + GuitarVigemMapper.HandleReport(device, guitarReport); } else if (data.Length == sizeof(DrumInput) && MemoryMarshal.TryRead(data, out DrumInput drumReport)) { - ParseDrums(drumReport); + DrumsVigemMapper.HandleReport(device, drumReport, ref previousDpadCymbals, ref dpadMask); } else { @@ -51,177 +55,5 @@ private unsafe void ParseInput(ReadOnlySpan data) // Send data device.SubmitReport(); } - - /// - /// Parses guitar input data from an input report. - /// - private void ParseGuitar(GuitarInput report) - { - // Menu and Options - var buttons = (GamepadButton)report.Buttons; - device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); - device.SetButtonState(Xbox360Button.Back, (buttons & GamepadButton.Options) != 0); - - // Dpad - device.SetButtonState(Xbox360Button.Up, (buttons & GamepadButton.DpadUp) != 0); - device.SetButtonState(Xbox360Button.Down, (buttons & GamepadButton.DpadDown) != 0); - device.SetButtonState(Xbox360Button.Left, (buttons & GamepadButton.DpadLeft) != 0); - device.SetButtonState(Xbox360Button.Right, (buttons & GamepadButton.DpadRight) != 0); - - // Frets - device.SetButtonState(Xbox360Button.A, report.Green); - device.SetButtonState(Xbox360Button.B, report.Red); - device.SetButtonState(Xbox360Button.Y, report.Yellow); - device.SetButtonState(Xbox360Button.X, report.Blue); - device.SetButtonState(Xbox360Button.LeftShoulder, report.Orange); - - // Lower fret flag - device.SetButtonState(Xbox360Button.LeftThumb, report.LowerFretFlag); - - // Whammy - device.SetAxisValue(Xbox360Axis.RightThumbX, report.WhammyBar.ScaleToInt16()); - // Tilt - device.SetAxisValue(Xbox360Axis.RightThumbY, report.Tilt.ScaleToInt16()); - // Pickup Switch - device.SetSliderValue(Xbox360Slider.LeftTrigger, report.PickupSwitch); - } - - // Constants for masks below - const int yellowBit = 0x01; - const int blueBit = 0x02; - - // The previous state of the yellow/blue cymbals - int previousDpadCymbals; - // The current state of the d-pad mask from the hit yellow/blue cymbals - int dpadMask; - - /// - /// Parses drums input data from an input report. - /// - private void ParseDrums(DrumInput report) - { - // Menu and Options - var buttons = (GamepadButton)report.Buttons; - device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); - device.SetButtonState(Xbox360Button.Back, (buttons & GamepadButton.Options) != 0); - - // Dpad - device.SetButtonState(Xbox360Button.Up, (buttons & GamepadButton.DpadUp) != 0); - device.SetButtonState(Xbox360Button.Down, (buttons & GamepadButton.DpadDown) != 0); - device.SetButtonState(Xbox360Button.Left, (buttons & GamepadButton.DpadLeft) != 0); - device.SetButtonState(Xbox360Button.Right, (buttons & GamepadButton.DpadRight) != 0); - - // Pads and cymbals - byte redPad = report.RedPad; - byte yellowPad = report.YellowPad; - byte bluePad = report.BluePad; - byte greenPad = report.GreenPad; - - byte yellowCym = report.YellowCymbal; - byte blueCym = report.BlueCymbal; - byte greenCym = report.GreenCymbal; - - // Yellow and blue cymbal trigger d-pad up and down respectively on the RB2/3 kit we're emulating - // However, they only trigger one or the other, not both at the same time, so we need to mimic that - int cymbalMask = (yellowCym != 0 ? yellowBit : 0) | (blueCym != 0 ? blueBit : 0); - if (cymbalMask != previousDpadCymbals) - { - if (cymbalMask == 0) - dpadMask = 0; - - // This could probably be done more simply, but this works - if (dpadMask != 0) - { - // D-pad is already set - // Only remove the set value - if ((cymbalMask & yellowBit) == 0) - dpadMask &= ~yellowBit; - else if ((cymbalMask & blueBit) == 0) - dpadMask &= ~blueBit; - } - - // Explicitly check this so that if the d-pad is cleared but the other cymbal is still active, - // it will get set to that cymbal's d-pad - if (dpadMask == 0) - { - // D-pad is not set - // If both cymbals are hit at the same time, yellow takes priority - if ((cymbalMask & yellowBit) != 0) - dpadMask |= yellowBit; - else if ((cymbalMask & blueBit) != 0) - dpadMask |= blueBit; - } - - previousDpadCymbals = cymbalMask; - } - - device.SetButtonState(Xbox360Button.Up, ((dpadMask & yellowBit) != 0) || ((buttons & GamepadButton.DpadUp) != 0)); - device.SetButtonState(Xbox360Button.Down, ((dpadMask & blueBit) != 0) || ((buttons & GamepadButton.DpadDown) != 0)); - - // Color flags - device.SetButtonState(Xbox360Button.B, (redPad != 0) || ((buttons & GamepadButton.B) != 0)); - device.SetButtonState(Xbox360Button.Y, ((yellowPad | yellowCym) != 0) || ((buttons & GamepadButton.Y) != 0)); - device.SetButtonState(Xbox360Button.X, ((bluePad | blueCym) != 0) || ((buttons & GamepadButton.X) != 0)); - device.SetButtonState(Xbox360Button.A, ((greenPad | greenCym) != 0) || ((buttons & GamepadButton.A) != 0)); - - // Pad flag - device.SetButtonState(Xbox360Button.RightThumb, - (redPad | yellowPad | bluePad | greenPad) != 0); - // Cymbal flag - device.SetButtonState(Xbox360Button.RightShoulder, - (yellowCym | blueCym | greenCym) != 0); - - // Pedals - device.SetButtonState(Xbox360Button.LeftShoulder, - (report.Buttons & (ushort)DrumButton.KickOne) != 0); - device.SetButtonState(Xbox360Button.LeftThumb, - (report.Buttons & (ushort)DrumButton.KickTwo) != 0); - - // Velocities - device.SetAxisValue( - Xbox360Axis.LeftThumbX, - ByteToVelocity(redPad) - ); - device.SetAxisValue( - Xbox360Axis.LeftThumbY, - ByteToVelocityNegative((byte)(yellowPad | yellowCym)) - ); - device.SetAxisValue( - Xbox360Axis.RightThumbX, - ByteToVelocity((byte)(bluePad | blueCym)) - ); - device.SetAxisValue( - Xbox360Axis.RightThumbY, - ByteToVelocityNegative((byte)(greenPad | greenCym)) - ); - - /// - /// Scales a byte to a drums velocity value. - /// - short ByteToVelocity(byte value) - { - // Scale the value to fill the byte - value = (byte)(value * 0x11); - - return (short)( - // Bitwise invert to flip the value, then shift down one to exclude the sign bit - (~value.ScaleToUInt16()) >> 1 - ); - } - - /// - /// Scales a byte to a negative drums velocity value. - /// - short ByteToVelocityNegative(byte value) - { - // Scale the value to fill the byte - value = (byte)(value * 0x11); - - return (short)( - // Bitwise invert to flip the value, then shift down one to exclude the sign bit, then add our own - ((~value.ScaleToUInt16()) >> 1) | 0x8000 - ); - } - } } } diff --git a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs index 77dc049..5e2ab6f 100644 --- a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs @@ -36,11 +36,11 @@ public unsafe void ParseInput(ReadOnlySpan data) { if (data.Length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) { - ParseGuitar(guitarReport); + GuitarVjoyMapper.HandleReport(ref state, guitarReport); } else if (data.Length == sizeof(DrumInput) && MemoryMarshal.TryRead(data, out DrumInput drumReport)) { - ParseDrums(drumReport); + DrumsVjoyMapper.HandleReport(ref state, drumReport); } else { @@ -51,74 +51,5 @@ public unsafe void ParseInput(ReadOnlySpan data) // Send data VjoyClient.UpdateDevice(deviceId, ref state); } - - /// - /// Parses guitar input data from an input report. - /// - private void ParseGuitar(GuitarInput report) - { - // Menu and Options - var buttons = (GamepadButton)report.Buttons; - state.SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); - state.SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); - - // D-pad - ParseDpad(buttons); - - state.SetButton(VjoyButton.One, report.Green); - state.SetButton(VjoyButton.Two, report.Red); - state.SetButton(VjoyButton.Three, report.Yellow); - state.SetButton(VjoyButton.Four, report.Blue); - state.SetButton(VjoyButton.Five, report.Orange); - - // Whammy - // Value ranges from 0 (not pressed) to 255 (fully pressed) - state.AxisY = report.WhammyBar.ScaleToInt32(); - - // Tilt - // Value ranges from 0 to 255 - // It seems to have a threshold of around 0x70 though, - // after a certain point values will get floored to 0 - state.AxisZ = report.Tilt.ScaleToInt32(); - - // Pickup switch - // Reported values are 0x00, 0x10, 0x20, 0x30, and 0x40 (ranges from 0 to 64) - state.AxisX = report.PickupSwitch.ScaleToInt32(); - } - - /// - /// Parses drums input data from an input report. - /// - private void ParseDrums(DrumInput report) - { - // Menu and Options - var buttons = (GamepadButton)report.Buttons; - state.SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); - state.SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); - - // D-pad - ParseDpad(buttons); - - // Face buttons - state.SetButton(VjoyButton.Four, (buttons & GamepadButton.A) != 0); - state.SetButton(VjoyButton.One, (buttons & GamepadButton.B) != 0); - state.SetButton(VjoyButton.Three, (buttons & GamepadButton.X) != 0); - state.SetButton(VjoyButton.Two, (buttons & GamepadButton.Y) != 0); - - // Pads - state.SetButton(VjoyButton.One, report.RedPad != 0); - state.SetButton(VjoyButton.Two, report.YellowPad != 0); - state.SetButton(VjoyButton.Three, report.BluePad != 0); - state.SetButton(VjoyButton.Four, report.GreenPad != 0); - - // Cymbals - state.SetButton(VjoyButton.Six, report.YellowCymbal != 0); - state.SetButton(VjoyButton.Seven, report.BlueCymbal != 0); - state.SetButton(VjoyButton.Eight, report.GreenCymbal != 0); - - // Kick pedals - state.SetButton(VjoyButton.Five, (report.Buttons & (ushort)DrumButton.KickOne) != 0); - state.SetButton(VjoyButton.Nine, (report.Buttons & (ushort)DrumButton.KickTwo) != 0); - } } } diff --git a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs b/Program/PacketParsing/Mappers/GuitarVigemMapper.cs new file mode 100644 index 0000000..d657e3b --- /dev/null +++ b/Program/PacketParsing/Mappers/GuitarVigemMapper.cs @@ -0,0 +1,81 @@ +using System; +using System.Runtime.InteropServices; +using Nefarius.ViGEm.Client.Targets; +using Nefarius.ViGEm.Client.Targets.Xbox360; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Maps guitar inputs to a ViGEmBus device. + /// + class GuitarVigemMapper : VigemMapper + { + public GuitarVigemMapper() : base() + { + } + + /// + /// Handles an incoming packet. + /// + protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + { + switch (command) + { + case CommandId.Input: + ParseInput(data); + break; + + default: + break; + } + } + + /// + /// Parses an input report. + /// + private unsafe void ParseInput(ReadOnlySpan data) + { + if (data.Length != sizeof(GuitarInput) || !MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) + return; + + HandleReport(device, guitarReport); + + // Send data + device.SubmitReport(); + } + + /// + /// Maps guitar input data to an Xbox 360 controller. + /// + internal static void HandleReport(IXbox360Controller device, in GuitarInput report) + { + // Menu and Options + var buttons = (GamepadButton)report.Buttons; + device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); + device.SetButtonState(Xbox360Button.Back, (buttons & GamepadButton.Options) != 0); + + // Dpad + device.SetButtonState(Xbox360Button.Up, (buttons & GamepadButton.DpadUp) != 0); + device.SetButtonState(Xbox360Button.Down, (buttons & GamepadButton.DpadDown) != 0); + device.SetButtonState(Xbox360Button.Left, (buttons & GamepadButton.DpadLeft) != 0); + device.SetButtonState(Xbox360Button.Right, (buttons & GamepadButton.DpadRight) != 0); + + // Frets + device.SetButtonState(Xbox360Button.A, report.Green); + device.SetButtonState(Xbox360Button.B, report.Red); + device.SetButtonState(Xbox360Button.Y, report.Yellow); + device.SetButtonState(Xbox360Button.X, report.Blue); + device.SetButtonState(Xbox360Button.LeftShoulder, report.Orange); + + // Lower fret flag + device.SetButtonState(Xbox360Button.LeftThumb, report.LowerFretFlag); + + // Whammy + device.SetAxisValue(Xbox360Axis.RightThumbX, report.WhammyBar.ScaleToInt16()); + // Tilt + device.SetAxisValue(Xbox360Axis.RightThumbY, report.Tilt.ScaleToInt16()); + // Pickup Switch + device.SetSliderValue(Xbox360Slider.LeftTrigger, report.PickupSwitch); + } + } +} diff --git a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs new file mode 100644 index 0000000..eaf3571 --- /dev/null +++ b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs @@ -0,0 +1,81 @@ +using System; +using System.Runtime.InteropServices; +using RB4InstrumentMapper.Vjoy; +using vJoyInterfaceWrap; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Maps guitar inputs to a vJoy device. + /// + class GuitarVjoyMapper : VjoyMapper + { + public GuitarVjoyMapper() : base() + { + } + + /// + /// Handles an incoming packet. + /// + protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + { + switch (command) + { + case CommandId.Input: + ParseInput(data); + break; + + default: + break; + } + } + + /// + /// Parses an input report. + /// + public unsafe void ParseInput(ReadOnlySpan data) + { + if (data.Length != sizeof(GuitarInput) || !MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) + return; + + HandleReport(ref state, guitarReport); + + // Send data + VjoyClient.UpdateDevice(deviceId, ref state); + } + + /// + /// Maps guitar input data to a vJoy device. + /// + internal static void HandleReport(ref vJoy.JoystickState state, GuitarInput report) + { + // Menu and Options + var buttons = (GamepadButton)report.Buttons; + state.SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); + state.SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); + + // D-pad + ParseDpad(ref state, buttons); + + state.SetButton(VjoyButton.One, report.Green); + state.SetButton(VjoyButton.Two, report.Red); + state.SetButton(VjoyButton.Three, report.Yellow); + state.SetButton(VjoyButton.Four, report.Blue); + state.SetButton(VjoyButton.Five, report.Orange); + + // Whammy + // Value ranges from 0 (not pressed) to 255 (fully pressed) + state.AxisY = report.WhammyBar.ScaleToInt32(); + + // Tilt + // Value ranges from 0 to 255 + // It seems to have a threshold of around 0x70 though, + // after a certain point values will get floored to 0 + state.AxisZ = report.Tilt.ScaleToInt32(); + + // Pickup switch + // Reported values are 0x00, 0x10, 0x20, 0x30, and 0x40 (ranges from 0 to 64) + state.AxisX = report.PickupSwitch.ScaleToInt32(); + } + } +} diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 00b75dc..e4f31b7 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -108,8 +108,12 @@ + + + + From 39932700f8d396a52d5356da7250eb957fc55840 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 23:48:50 -0700 Subject: [PATCH 119/437] Move IDeviceMapper to Mappers folder --- Program/PacketParsing/{ => Mappers}/IDeviceMapper.cs | 0 Program/RB4InstrumentMapper.csproj | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename Program/PacketParsing/{ => Mappers}/IDeviceMapper.cs (100%) diff --git a/Program/PacketParsing/IDeviceMapper.cs b/Program/PacketParsing/Mappers/IDeviceMapper.cs similarity index 100% rename from Program/PacketParsing/IDeviceMapper.cs rename to Program/PacketParsing/Mappers/IDeviceMapper.cs diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index e4f31b7..b25de95 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -114,13 +114,13 @@ + - From bd4e6e9ba24f8a5d72c95108a1116073dfeef03d Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 23:50:29 -0700 Subject: [PATCH 120/437] Oh, whoops, forgot to remove this from the project --- Program/RB4InstrumentMapper.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index b25de95..1bbff96 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -121,7 +121,6 @@ - From db1a52b3183e0831b8b8f799650503407da344b2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 6 Mar 2023 23:57:41 -0700 Subject: [PATCH 121/437] Add device GUID constants --- Program/PacketParsing/DeviceGuids.cs | 18 ++++++++++++++++++ Program/RB4InstrumentMapper.csproj | 1 + 2 files changed, 19 insertions(+) create mode 100644 Program/PacketParsing/DeviceGuids.cs diff --git a/Program/PacketParsing/DeviceGuids.cs b/Program/PacketParsing/DeviceGuids.cs new file mode 100644 index 0000000..84c8473 --- /dev/null +++ b/Program/PacketParsing/DeviceGuids.cs @@ -0,0 +1,18 @@ +using System; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Xbox device interface GUIDs. + /// + public static class DeviceGuids + { + public static readonly Guid MadCatzGuitar = Guid.Parse("0D2AE438-7F7D-4933-8693-30FC55018E77"); + public static readonly Guid MadCatzDrumkit = Guid.Parse("06182893-CCE0-4B85-9271-0A10DBAB7E07"); + // public static readonly Guid PdpGuitar = Guid.Parse(""); // Not known yet + public static readonly Guid PdpDrumkit = Guid.Parse("A503F9B0-955E-47C4-A2ED-B1336FA7703E"); + + public static readonly Guid XboxInputDevice = Guid.Parse("9776FF56-9BFD-4581-AD45-B645BBA526D6"); + public static readonly Guid XboxNavigationController = Guid.Parse("B8F31FE7-7386-40E9-A9F8-2F21263ACFB7"); + } +} \ No newline at end of file diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 1bbff96..bb5a52a 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -121,6 +121,7 @@ + From 1bbfd59c7fca9ca88f55025063b10b33869eddf0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 7 Mar 2023 00:36:21 -0700 Subject: [PATCH 122/437] Add device mapper creation factory --- .../PacketParsing/Mappers/MapperFactory.cs | 92 +++++++++++++++++++ Program/PacketParsing/XboxDevice.cs | 11 +-- Program/RB4InstrumentMapper.csproj | 1 + 3 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 Program/PacketParsing/Mappers/MapperFactory.cs diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs new file mode 100644 index 0000000..83b1302 --- /dev/null +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Factory for device mappers. + /// + internal static class MapperFactory + { + // Device interface GUIDs to check when getting the device mapper + private static Dictionary> guidToMapper = new Dictionary>() + { + { DeviceGuids.MadCatzGuitar, GetGuitarMapper }, + // { DeviceGuids.PdpGuitar, GetGuitarMapper }, + { DeviceGuids.MadCatzDrumkit, GetDrumsMapper }, + { DeviceGuids.PdpDrumkit, GetDrumsMapper }, + }; + + // Non-unique interface GUIDs to ignore + private static List guidExclusionList = new List() + { + DeviceGuids.XboxInputDevice, + DeviceGuids.XboxNavigationController, + }; + + public static IDeviceMapper GetMapper(IReadOnlyList interfaceGuids, MappingMode mode) + { + // Get unique interface GUID + Guid interfaceGuid = default; + foreach (var guid in interfaceGuids) + { + if (guidExclusionList.Contains(guid)) + continue; + + if (interfaceGuid != default) + { + Console.WriteLine($"More than one unique interface GUID found! Cannot get specific mapper, using fallback mapper instead."); + Console.WriteLine($"Consider filing a GitHub issue with the GUIDs below so that this can be addressed:"); + foreach (var guid2 in interfaceGuids) + { + Console.WriteLine($"- {guid2}"); + } + return GetFallbackMapper(mode); + } + } + + // Get mapper creation delegate for interface GUID + if (!guidToMapper.TryGetValue(interfaceGuid, out var func)) + { + Console.WriteLine($"Could not get a specific mapper for interface GUID {interfaceGuid}! Using fallback mapper instead."); + Console.WriteLine($"Consider filing a GitHub issue with the GUID above so that it can be assigned the correct mapper."); + return GetFallbackMapper(mode); + } + + return func(mode); + } + + public static IDeviceMapper GetGuitarMapper(MappingMode mode) + { + Console.WriteLine($"Guitar found, creating new {mode} mapper..."); + switch (mode) + { + case MappingMode.ViGEmBus: return new GuitarVigemMapper(); + case MappingMode.vJoy: return new GuitarVjoyMapper(); + default: throw new Exception("Unhandled mapping mode!"); + } + } + + public static IDeviceMapper GetDrumsMapper(MappingMode mode) + { + Console.WriteLine($"Drumkit found, creating new {mode} mapper..."); + switch (mode) + { + case MappingMode.ViGEmBus: return new DrumsVigemMapper(); + case MappingMode.vJoy: return new DrumsVjoyMapper(); + default: throw new Exception("Unhandled mapping mode!"); + } + } + + public static IDeviceMapper GetFallbackMapper(MappingMode mode) + { + Console.WriteLine($"Creating new fallback {mode} mapper..."); + switch (mode) + { + case MappingMode.ViGEmBus: return new DrumsVigemMapper(); + case MappingMode.vJoy: return new DrumsVjoyMapper(); + default: throw new Exception("Unhandled mapping mode!"); + } + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 3d87c42..68da5e2 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -38,16 +38,7 @@ class XboxDevice : IDisposable /// public XboxDevice() { - switch (MapperMode) - { - case MappingMode.ViGEmBus: - deviceMapper = new FallbackVigemMapper(); - break; - - case MappingMode.vJoy: - deviceMapper = new FallbackVjoyMapper(); - break; - } + deviceMapper = MapperFactory.GetFallbackMapper(MapperMode); } /// diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index bb5a52a..73fcf46 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -115,6 +115,7 @@ + From 6267a21790d90d78e7e7ffc9c070ec4256a8b543 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Mar 2023 00:29:04 -0700 Subject: [PATCH 123/437] Add handling of device descriptor message --- .../PacketParsing/Packets/CommandHeader.cs | 1 + .../PacketParsing/Packets/XboxDescriptor.cs | 239 ++++++++++++++++++ Program/PacketParsing/XboxDevice.cs | 21 ++ Program/RB4InstrumentMapper.csproj | 1 + 4 files changed, 262 insertions(+) create mode 100644 Program/PacketParsing/Packets/XboxDescriptor.cs diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index b7972bd..8f6ef61 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -8,6 +8,7 @@ namespace RB4InstrumentMapper.Parsing /// public enum CommandId : byte { + Descriptor = 0x04, Input = 0x20 } diff --git a/Program/PacketParsing/Packets/XboxDescriptor.cs b/Program/PacketParsing/Packets/XboxDescriptor.cs new file mode 100644 index 0000000..8a2145b --- /dev/null +++ b/Program/PacketParsing/Packets/XboxDescriptor.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// The descriptor data of an Xbox One device. + /// A large amount of the descriptor data is ignored, only data necessary for identifying device types is read. + /// + public class XboxDescriptor + { + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Header + { + public ushort HeaderLength; + public int unk1; + public ulong unk2; + public ushort DataLength; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct Offsets + { + public ushort CustomCommands; + public ushort FirmwareVersions; + public ushort AudioFormats; + public ushort OutputCommands; + public ushort InputCommands; + public ushort ClassNames; + public ushort InterfaceGuids; + public ushort HidDescriptor; + public ushort unk1; + public ushort unk2; + public ushort unk3; + } + + public IReadOnlyList ClassNames { get; private set; } + public IReadOnlyList InterfaceGuids { get; private set; } + + public static bool Parse(ReadOnlySpan data, out XboxDescriptor descriptor) + { + descriptor = new XboxDescriptor(); + return descriptor.Parse(data); + } + + private unsafe bool Parse(ReadOnlySpan data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + // Descriptor header size + if (data.Length < sizeof(ushort) + || !MemoryMarshal.TryRead(data, out ushort headerSize)) + { + Debug.Fail($"Couldn't parse descriptor header size! Buffer size: {data.Length}, element size: {sizeof(ushort)}"); + return false; + } + + // Expecting a certain size + if (headerSize != sizeof(Header)) + { + Debug.Fail($"Header size does not match expected size! Expected: {sizeof(Header)}, actual: {headerSize}"); + return false; + } + + // Descriptor header + if (data.Length < headerSize + || !MemoryMarshal.TryRead(data, out Header header)) + { + Debug.Fail($"Couldn't parse descriptor header! Buffer size: {data.Length}, header size: {headerSize}"); + return false; + } + + // Verify buffer size + if (data.Length < header.DataLength) + { + Debug.Fail($"Buffer size is smaller than size listed in header! Buffer size: {data.Length}, listed size: {header.DataLength}"); + return false; + } + Debug.WriteLineIf(data.Length != header.DataLength, $"Buffer size is not the same as size listed in header! Buffer size: {data.Length}, listed size: {header.DataLength}"); + data = data.Slice(header.HeaderLength); + + // Data offsets + if (data.Length < sizeof(Offsets) + || !MemoryMarshal.TryRead(data, out Offsets offsets)) + { + Debug.Fail($"Couldn't parse descriptor offsets! Buffer size: {data.Length}, offsets size: {sizeof(Offsets)}"); + return false; + } + // No slice, offsets are relative to the start of the offsets block + + // Data elements + ClassNames = ParseStrings(data, offsets.ClassNames, nameof(ClassNames)); + InterfaceGuids = ParseElements(data, offsets.InterfaceGuids, nameof(InterfaceGuids)); + + return true; + } + + static bool VerifyOffset(ReadOnlySpan buffer, ushort offset, int elementSize, out byte count, string elementName) + { + // Null offset means no elements are available + if (offset == 0) + { + count = 0; + return false; + } + + // Ensure offset is within bounds + if (buffer.Length <= offset) + { + Debug.Fail($"Offset of {elementName} is greater than size of buffer! Offset: {offset}; Buffer size: {buffer.Length}"); + count = 0; + return false; + } + + // Get number of elements + count = buffer[offset]; + // Zero count means no elements are available + if (count == 0) + { + return false; + } + + // Element size of 0 is used for variable-length types like strings + if (elementSize == 0) + { + // Can't verify bounds here, everything else checks out so treat it as valid + return true; + } + + // Ensure total size of elements is within bounds + var fromElements = buffer.Slice(offset); + int elementsSize = elementSize * count; + if (fromElements.Length < elementsSize) + { + Debug.Fail($"Size of {elementName} is greater than size of buffer from offset! Offset: {offset:X4}; Count: {count}; Element size: {elementSize}; Total length of elements: {elementsSize}; Buffer length from offset: {fromElements.Length}"); + count = 0; + return false; + } + + // Offset is valid + return true; + } + + static unsafe T[] ParseElements(ReadOnlySpan buffer, ushort offset, string elementName, Func customCheck = null) + where T : unmanaged + { + if (!VerifyOffset(buffer, offset, sizeof(T), out byte count, elementName) || count == 0) + { + return null; + } + + // Get data bounds + buffer = buffer.Slice(offset + sizeof(byte), count * sizeof(T)); + // Get element data + if (customCheck == null) + { + // No checks, get everything at once + return MemoryMarshal.Cast(buffer).ToArray(); + } + + // Checks required, go through elements individually + var elements = new T[count]; + for (byte index = 0; index < count; index++) + { + if (!MemoryMarshal.TryRead(buffer, out T element)) + { + Debug.Fail($"Failed to read element from buffer! Buffer size: {buffer.Length}, element size: {sizeof(T)}"); + TruncateArray(ref elements, index); + break; + } + + if (!customCheck(element)) + { + Debug.Fail($"Check for {elementName} failed!"); + TruncateArray(ref elements, index); + break; + } + + elements[index] = element; + buffer = buffer.Slice(sizeof(T)); + } + + return elements; + } + + static unsafe string[] ParseStrings(ReadOnlySpan buffer, ushort offset, string elementName) + { + if (!VerifyOffset(buffer, offset, 0, out byte count, elementName) || count == 0) + { + return null; + } + + var elements = new string[count]; + buffer = buffer.Slice(offset + 1); + for (byte index = 0; index < elements.Length; index++) + { + // Get length + if (!MemoryMarshal.TryRead(buffer, out ushort length)) + { + // Resize array to exclude null elements + TruncateArray(ref elements, index); + break; + } + buffer = buffer.Slice(sizeof(ushort)); + + // Ensure length is within bounds + if (buffer.Length < length) + { + Debug.Fail($"Descriptor string length is greater than buffer size! Index: {index}; String length: {length}; Buffer size: {buffer.Length}"); + // Resize array to exclude null elements + TruncateArray(ref elements, index); + break; + } + + // Parse `length` bytes into a string + // Pointers are more efficient here, `char` is 2 bytes while these strings are 1-byte characters + fixed (byte* ptr = buffer) + { + sbyte* sPtr = (sbyte*)ptr; + elements[index] = new string(sPtr, 0, length); + } + buffer = buffer.Slice(length); + } + + return elements; + } + + static void TruncateArray(ref T[] array, int length) + { + if (length == 0) + array = null; + else + array = array.AsSpan().Slice(0, length).ToArray(); + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 68da5e2..068621b 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -18,6 +18,11 @@ class XboxDevice : IDisposable { public static MappingMode MapperMode; + /// + /// The descriptor of the device. + /// + public XboxDescriptor Descriptor { get; private set; } + /// /// Mapper interface to use. /// @@ -121,6 +126,10 @@ public unsafe void ParseCommand(ReadOnlySpan commandData) switch (header.CommandId) { + case CommandId.Descriptor: + HandleDescriptor(commandData); + break; + default: // Hand off unrecognized commands to the mapper deviceMapper.HandlePacket(header.CommandId, commandData); @@ -128,6 +137,18 @@ public unsafe void ParseCommand(ReadOnlySpan commandData) } } + /// + /// Handles the Xbox One descriptor of the device. + /// + private void HandleDescriptor(ReadOnlySpan data) + { + if (!XboxDescriptor.Parse(data, out var descriptor)) + return; + + Descriptor = descriptor; + deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, MapperMode); + } + /// /// Performs cleanup for the device. /// diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 73fcf46..770deaa 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -122,6 +122,7 @@ + From 87e7a0214289b9bfa14883418dbe4670163d62c2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Mar 2023 01:48:54 -0700 Subject: [PATCH 124/437] Warn if new device was found without receiving its arrival or descriptor message --- Program/PacketParsing/Backends/PcapBackend.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 736f408..b156b0d 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -135,6 +135,15 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) devices.Add(deviceId, device); Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); + + // Check if device was found during its initialization + CommandId command = (CommandId)data[0]; + if (command != CommandId.Arrival && command != CommandId.Descriptor) + { + Console.WriteLine("Warning: This device was not encountered during its initial connection! It will use the fallback mapper instead of one specific to its device interface."); + Console.WriteLine("Consider hitting Start before connecting it to ensure correct behavior."); + // TODO: Figure out how to detect disconnections + } } try From 106d87bd1f3b328bc16aac597bcf1fde1459ed25 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Mar 2023 02:32:21 -0700 Subject: [PATCH 125/437] Handle device arrival message --- .../PacketParsing/Packets/CommandHeader.cs | 1 + .../PacketParsing/Packets/DeviceArrival.cs | 14 ++++++++++++++ Program/PacketParsing/XboxDevice.cs | 19 +++++++++++++++++++ Program/RB4InstrumentMapper.csproj | 1 + 4 files changed, 35 insertions(+) create mode 100644 Program/PacketParsing/Packets/DeviceArrival.cs diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 8f6ef61..953e505 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -8,6 +8,7 @@ namespace RB4InstrumentMapper.Parsing ///
public enum CommandId : byte { + Arrival = 0x02, Descriptor = 0x04, Input = 0x20 } diff --git a/Program/PacketParsing/Packets/DeviceArrival.cs b/Program/PacketParsing/Packets/DeviceArrival.cs new file mode 100644 index 0000000..2225f58 --- /dev/null +++ b/Program/PacketParsing/Packets/DeviceArrival.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct DeviceArrival + { + public ulong SerialNumber; + public ushort VendorId; + public ushort ProductId; + private ulong ignored1; + private ulong ignored2; + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 068621b..133d9d4 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -18,6 +18,9 @@ class XboxDevice : IDisposable { public static MappingMode MapperMode; + public ushort VendorId { get; private set; } + public ushort ProductId { get; private set; } + /// /// The descriptor of the device. /// @@ -126,6 +129,10 @@ public unsafe void ParseCommand(ReadOnlySpan commandData) switch (header.CommandId) { + case CommandId.Arrival: + HandleArrival(commandData); + break; + case CommandId.Descriptor: HandleDescriptor(commandData); break; @@ -137,6 +144,18 @@ public unsafe void ParseCommand(ReadOnlySpan commandData) } } + /// + /// Handles the arrival message of the device. + /// + private unsafe void HandleArrival(ReadOnlySpan data) + { + if (data.Length < sizeof(DeviceArrival) || MemoryMarshal.TryRead(data, out DeviceArrival arrival)) + return; + + VendorId = arrival.VendorId; + ProductId = arrival.ProductId; + } + /// /// Handles the Xbox One descriptor of the device. /// diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 770deaa..08d8c7e 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -119,6 +119,7 @@ + From 2a71a715620bc2e702ae2740990d4f9ebb3cbd68 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Mar 2023 03:21:42 -0700 Subject: [PATCH 126/437] Access modifier tidying I know it doesn't really matter since it'll be accessible elsewhere anyways, but the point is to show where it's intended to be used --- Program/PacketParsing/Backends/PcapBackend.cs | 2 +- Program/PacketParsing/DeviceGuids.cs | 2 +- Program/PacketParsing/Mappers/DrumsVigemMapper.cs | 2 +- Program/PacketParsing/Mappers/DrumsVjoyMapper.cs | 2 +- Program/PacketParsing/Mappers/FallbackVigemMapper.cs | 2 +- Program/PacketParsing/Mappers/FallbackVjoyMapper.cs | 2 +- Program/PacketParsing/Mappers/GuitarVigemMapper.cs | 2 +- Program/PacketParsing/Mappers/GuitarVjoyMapper.cs | 2 +- Program/PacketParsing/Mappers/IDeviceMapper.cs | 2 +- Program/PacketParsing/Mappers/VigemMapper.cs | 2 +- Program/PacketParsing/Mappers/VjoyMapper.cs | 2 +- Program/PacketParsing/Packets/CommandHeader.cs | 6 +++--- Program/PacketParsing/Packets/DeviceArrival.cs | 2 +- Program/PacketParsing/Packets/DrumInput.cs | 4 ++-- Program/PacketParsing/Packets/GamepadButton.cs | 2 +- Program/PacketParsing/Packets/GuitarInput.cs | 6 +++--- Program/PacketParsing/Packets/XboxDescriptor.cs | 8 ++++---- Program/PacketParsing/ParsingUtils.cs | 2 +- Program/PacketParsing/XboxDevice.cs | 2 +- 19 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index b156b0d..534e8da 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -12,7 +12,7 @@ namespace RB4InstrumentMapper.Parsing /// A standard IEEE 802.11 QoS header. ///
[StructLayout(LayoutKind.Sequential, Pack = 1)] - unsafe struct QoSHeader + internal unsafe struct QoSHeader { ushort frameControl; ushort durationId; diff --git a/Program/PacketParsing/DeviceGuids.cs b/Program/PacketParsing/DeviceGuids.cs index 84c8473..c5b9a81 100644 --- a/Program/PacketParsing/DeviceGuids.cs +++ b/Program/PacketParsing/DeviceGuids.cs @@ -5,7 +5,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Xbox device interface GUIDs. /// - public static class DeviceGuids + internal static class DeviceGuids { public static readonly Guid MadCatzGuitar = Guid.Parse("0D2AE438-7F7D-4933-8693-30FC55018E77"); public static readonly Guid MadCatzDrumkit = Guid.Parse("06182893-CCE0-4B85-9271-0A10DBAB7E07"); diff --git a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs index ad80e67..f125449 100644 --- a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs +++ b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs @@ -8,7 +8,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Maps drumkit inputs to a ViGEmBus device. /// - class DrumsVigemMapper : VigemMapper + internal class DrumsVigemMapper : VigemMapper { public DrumsVigemMapper() : base() { diff --git a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs b/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs index cff1bec..f549584 100644 --- a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs @@ -8,7 +8,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Maps drumkit inputs to a vJoy device. /// - class DrumsVjoyMapper : VjoyMapper + internal class DrumsVjoyMapper : VjoyMapper { public DrumsVjoyMapper() : base() { diff --git a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs index 41e2dc3..c4bcf93 100644 --- a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs @@ -6,7 +6,7 @@ namespace RB4InstrumentMapper.Parsing /// /// The ViGEmBus mapper used when device type could not be determined. Maps based on report length. /// - class FallbackVigemMapper : VigemMapper + internal class FallbackVigemMapper : VigemMapper { public FallbackVigemMapper() : base() { diff --git a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs index 5e2ab6f..3751d6e 100644 --- a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs @@ -7,7 +7,7 @@ namespace RB4InstrumentMapper.Parsing /// /// The vJoy mapper used when device type could not be determined. Maps based on report length. /// - class FallbackVjoyMapper : VjoyMapper + internal class FallbackVjoyMapper : VjoyMapper { public FallbackVjoyMapper() : base() { diff --git a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs b/Program/PacketParsing/Mappers/GuitarVigemMapper.cs index d657e3b..05bc3dd 100644 --- a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs +++ b/Program/PacketParsing/Mappers/GuitarVigemMapper.cs @@ -8,7 +8,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Maps guitar inputs to a ViGEmBus device. /// - class GuitarVigemMapper : VigemMapper + internal class GuitarVigemMapper : VigemMapper { public GuitarVigemMapper() : base() { diff --git a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs index eaf3571..0792bba 100644 --- a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs @@ -8,7 +8,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Maps guitar inputs to a vJoy device. /// - class GuitarVjoyMapper : VjoyMapper + internal class GuitarVjoyMapper : VjoyMapper { public GuitarVjoyMapper() : base() { diff --git a/Program/PacketParsing/Mappers/IDeviceMapper.cs b/Program/PacketParsing/Mappers/IDeviceMapper.cs index ae507ab..f5ec0c5 100644 --- a/Program/PacketParsing/Mappers/IDeviceMapper.cs +++ b/Program/PacketParsing/Mappers/IDeviceMapper.cs @@ -5,7 +5,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Common interface for device mappers. /// - interface IDeviceMapper : IDisposable + internal interface IDeviceMapper : IDisposable { /// /// Handles an incoming packet. diff --git a/Program/PacketParsing/Mappers/VigemMapper.cs b/Program/PacketParsing/Mappers/VigemMapper.cs index 53b04aa..bbed9bb 100644 --- a/Program/PacketParsing/Mappers/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/VigemMapper.cs @@ -8,7 +8,7 @@ namespace RB4InstrumentMapper.Parsing /// /// A mapper that maps to a ViGEmBus device. /// - abstract class VigemMapper : IDeviceMapper + internal abstract class VigemMapper : IDeviceMapper { /// /// The device to map to. diff --git a/Program/PacketParsing/Mappers/VjoyMapper.cs b/Program/PacketParsing/Mappers/VjoyMapper.cs index 82d6dca..97205a2 100644 --- a/Program/PacketParsing/Mappers/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/VjoyMapper.cs @@ -7,7 +7,7 @@ namespace RB4InstrumentMapper.Parsing /// /// A mapper that maps to a vJoy device. /// - abstract class VjoyMapper : IDeviceMapper + internal abstract class VjoyMapper : IDeviceMapper { protected vJoy.JoystickState state = new vJoy.JoystickState(); protected uint deviceId = 0; diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 953e505..c5a8465 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -6,7 +6,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Command ID definitions. /// - public enum CommandId : byte + internal enum CommandId : byte { Arrival = 0x02, Descriptor = 0x04, @@ -17,7 +17,7 @@ public enum CommandId : byte /// Command flag definitions. /// [Flags] - public enum CommandFlags : byte + internal enum CommandFlags : byte { None = 0, NeedsAcknowledgement = 0x10, @@ -30,7 +30,7 @@ public enum CommandFlags : byte /// Header data for a message. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] - struct CommandHeader + internal struct CommandHeader { public CommandId CommandId; public CommandFlags Flags; diff --git a/Program/PacketParsing/Packets/DeviceArrival.cs b/Program/PacketParsing/Packets/DeviceArrival.cs index 2225f58..8f60c1f 100644 --- a/Program/PacketParsing/Packets/DeviceArrival.cs +++ b/Program/PacketParsing/Packets/DeviceArrival.cs @@ -3,7 +3,7 @@ namespace RB4InstrumentMapper.Parsing { [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct DeviceArrival + internal struct DeviceArrival { public ulong SerialNumber; public ushort VendorId; diff --git a/Program/PacketParsing/Packets/DrumInput.cs b/Program/PacketParsing/Packets/DrumInput.cs index b0934a8..31be443 100644 --- a/Program/PacketParsing/Packets/DrumInput.cs +++ b/Program/PacketParsing/Packets/DrumInput.cs @@ -8,7 +8,7 @@ namespace RB4InstrumentMapper.Parsing /// Re-definitions for button flags that have specific meanings. ///
[Flags] - public enum DrumButton : ushort + internal enum DrumButton : ushort { // Not used as these are for menu navigation purposes // RedPad = GamepadButton.B, @@ -21,7 +21,7 @@ public enum DrumButton : ushort /// An input report from a drumkit. ///
[StructLayout(LayoutKind.Sequential, Pack = 1)] - struct DrumInput + internal struct DrumInput { /// /// Masks for each pad's value. diff --git a/Program/PacketParsing/Packets/GamepadButton.cs b/Program/PacketParsing/Packets/GamepadButton.cs index 7554240..ff17a68 100644 --- a/Program/PacketParsing/Packets/GamepadButton.cs +++ b/Program/PacketParsing/Packets/GamepadButton.cs @@ -6,7 +6,7 @@ namespace RB4InstrumentMapper.Parsing /// Flag definitions for the buttons bytes. /// [Flags] - public enum GamepadButton : ushort + internal enum GamepadButton : ushort { Sync = 0x0001, Unused = 0x0002, diff --git a/Program/PacketParsing/Packets/GuitarInput.cs b/Program/PacketParsing/Packets/GuitarInput.cs index 2bf6b58..a550e99 100644 --- a/Program/PacketParsing/Packets/GuitarInput.cs +++ b/Program/PacketParsing/Packets/GuitarInput.cs @@ -7,7 +7,7 @@ namespace RB4InstrumentMapper.Parsing /// Re-definitions for button flags that have specific meanings. ///
[Flags] - public enum GuitarButton : ushort + internal enum GuitarButton : ushort { StrumUp = GamepadButton.DpadUp, StrumDown = GamepadButton.DpadDown, @@ -23,7 +23,7 @@ public enum GuitarButton : ushort /// Flags used in and ///
[Flags] - public enum GuitarFret : byte + internal enum GuitarFret : byte { Green = 0x01, Red = 0x02, @@ -36,7 +36,7 @@ public enum GuitarFret : byte /// An input report from a guitar. ///
[StructLayout(LayoutKind.Sequential, Pack = 1)] - struct GuitarInput + internal struct GuitarInput { public ushort Buttons; public byte Tilt; diff --git a/Program/PacketParsing/Packets/XboxDescriptor.cs b/Program/PacketParsing/Packets/XboxDescriptor.cs index 8a2145b..cdd9282 100644 --- a/Program/PacketParsing/Packets/XboxDescriptor.cs +++ b/Program/PacketParsing/Packets/XboxDescriptor.cs @@ -98,7 +98,7 @@ private unsafe bool Parse(ReadOnlySpan data) return true; } - static bool VerifyOffset(ReadOnlySpan buffer, ushort offset, int elementSize, out byte count, string elementName) + private static bool VerifyOffset(ReadOnlySpan buffer, ushort offset, int elementSize, out byte count, string elementName) { // Null offset means no elements are available if (offset == 0) @@ -144,7 +144,7 @@ static bool VerifyOffset(ReadOnlySpan buffer, ushort offset, int elementSi return true; } - static unsafe T[] ParseElements(ReadOnlySpan buffer, ushort offset, string elementName, Func customCheck = null) + private static unsafe T[] ParseElements(ReadOnlySpan buffer, ushort offset, string elementName, Func customCheck = null) where T : unmanaged { if (!VerifyOffset(buffer, offset, sizeof(T), out byte count, elementName) || count == 0) @@ -186,7 +186,7 @@ static unsafe T[] ParseElements(ReadOnlySpan buffer, ushort offset, str return elements; } - static unsafe string[] ParseStrings(ReadOnlySpan buffer, ushort offset, string elementName) + private static unsafe string[] ParseStrings(ReadOnlySpan buffer, ushort offset, string elementName) { if (!VerifyOffset(buffer, offset, 0, out byte count, elementName) || count == 0) { @@ -228,7 +228,7 @@ static unsafe string[] ParseStrings(ReadOnlySpan buffer, ushort offset, st return elements; } - static void TruncateArray(ref T[] array, int length) + private static void TruncateArray(ref T[] array, int length) { if (length == 0) array = null; diff --git a/Program/PacketParsing/ParsingUtils.cs b/Program/PacketParsing/ParsingUtils.cs index b53bae6..5a44e64 100644 --- a/Program/PacketParsing/ParsingUtils.cs +++ b/Program/PacketParsing/ParsingUtils.cs @@ -6,7 +6,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Helper functions for parsing. /// - static class ParsingUtils + internal static class ParsingUtils { // https://en.wikipedia.org/wiki/LEB128 public static bool DecodeLEB128(ReadOnlySpan data, out int result, out int byteLength) diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 133d9d4..248bb68 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -14,7 +14,7 @@ public enum MappingMode /// /// Interface for Xbox devices. /// - class XboxDevice : IDisposable + public class XboxDevice : IDisposable { public static MappingMode MapperMode; From 06e70e1249ee73232a7c387134447b7b19b960cf Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Mar 2023 03:53:57 -0700 Subject: [PATCH 127/437] Handle cases where there are multiple messages in one packet --- Program/PacketParsing/Backends/PcapBackend.cs | 2 +- Program/PacketParsing/XboxDevice.cs | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 534e8da..70d412a 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -148,7 +148,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) try { - device.ParseCommand(data); + device.HandlePacket(data); } catch (ThreadAbortException) { diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 248bb68..7d68461 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -58,21 +58,36 @@ public XboxDevice() } /// - /// Parses command data from a packet. + /// Handles an incoming packet for this device. /// - public unsafe void ParseCommand(ReadOnlySpan commandData) + public unsafe void HandlePacket(ReadOnlySpan data) { - if (!CommandHeader.TryParse(commandData, out var header, out int bytesRead)) + // Some devices may send multiple messages in a single packet, placing them back-to-back + // The header length is very important in these scenarios, as it determines which bytes are part of the message + // and where the next message's header begins. + while (data.Length > 0) { - return; + if (!CommandHeader.TryParse(data, out var header, out int bytesRead) || data.Length < (bytesRead + header.DataLength)) + { + return; + } + var commandData = data.Slice(bytesRead, header.DataLength); + data = data.Slice(bytesRead + header.DataLength); + + HandleMessage(header, commandData); } - commandData = commandData.Slice(bytesRead); + } + /// + /// Parses command data from a packet. + /// + private unsafe void HandleMessage(CommandHeader header, ReadOnlySpan commandData) + { // Chunked packets if ((header.Flags & CommandFlags.ChunkPacket) != 0) { // Get sequence length/index - if (!ParsingUtils.DecodeLEB128(commandData, out int bufferIndex, out bytesRead)) + if (!ParsingUtils.DecodeLEB128(commandData, out int bufferIndex, out int bytesRead)) { return; } From 4db81255556a927a73b0a58c72c68cc9cb32779a Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Mar 2023 04:08:52 -0700 Subject: [PATCH 128/437] Fix mapper factory not correctly getting the unique interface GUID --- Program/PacketParsing/Mappers/MapperFactory.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 83b1302..d6d2d73 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -43,6 +43,8 @@ public static IDeviceMapper GetMapper(IReadOnlyList interfaceGuids, Mappin } return GetFallbackMapper(mode); } + + interfaceGuid = guid; } // Get mapper creation delegate for interface GUID From bcd0db5503217ed06f8f3c4d67f5a5d24e25b2a4 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Mar 2023 04:29:47 -0700 Subject: [PATCH 129/437] Handle keystroke messages --- Program/PacketParsing/Mappers/VigemMapper.cs | 29 ++++++++++++++++++- Program/PacketParsing/Mappers/VjoyMapper.cs | 29 ++++++++++++++++++- .../PacketParsing/Packets/CommandHeader.cs | 1 + Program/PacketParsing/Packets/Keystroke.cs | 23 +++++++++++++++ Program/RB4InstrumentMapper.csproj | 1 + 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 Program/PacketParsing/Packets/Keystroke.cs diff --git a/Program/PacketParsing/Mappers/VigemMapper.cs b/Program/PacketParsing/Mappers/VigemMapper.cs index bbed9bb..4d9bc73 100644 --- a/Program/PacketParsing/Mappers/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/VigemMapper.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Nefarius.ViGEm.Client.Targets; using Nefarius.ViGEm.Client.Targets.Xbox360; using RB4InstrumentMapper.Vigem; @@ -62,11 +63,37 @@ public void HandlePacket(CommandId command, ReadOnlySpan data) if (!deviceConnected) return; - OnPacketReceived(command, data); + switch (command) + { + case CommandId.Keystroke: + HandleKeystroke(data); + break; + + default: + OnPacketReceived(command, data); + break; + } } protected abstract void OnPacketReceived(CommandId command, ReadOnlySpan data); + private unsafe void HandleKeystroke(ReadOnlySpan data) + { + if (data.Length < sizeof(Keystroke)) + return; + + // Multiple keystrokes can be sent in a single message + var keys = MemoryMarshal.Cast(data); + foreach (var key in keys) + { + if ((KeyCode)key.Keycode == KeyCode.LeftWindows) + { + device.SetButtonState(Xbox360Button.Guide, key.Pressed); + device.SubmitReport(); + } + } + } + /// /// Performs cleanup for the object. /// diff --git a/Program/PacketParsing/Mappers/VjoyMapper.cs b/Program/PacketParsing/Mappers/VjoyMapper.cs index 97205a2..01171af 100644 --- a/Program/PacketParsing/Mappers/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/VjoyMapper.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using RB4InstrumentMapper.Vjoy; using vJoyInterfaceWrap; @@ -45,11 +46,37 @@ public void HandlePacket(CommandId command, ReadOnlySpan data) if (deviceId == 0) throw new ObjectDisposedException("this"); - OnPacketReceived(command, data); + switch (command) + { + case CommandId.Keystroke: + HandleKeystroke(data); + break; + + default: + OnPacketReceived(command, data); + break; + } } protected abstract void OnPacketReceived(CommandId command, ReadOnlySpan data); + private unsafe void HandleKeystroke(ReadOnlySpan data) + { + if (data.Length < sizeof(Keystroke)) + return; + + // Multiple keystrokes can be sent in a single message + var keys = MemoryMarshal.Cast(data); + foreach (var key in keys) + { + if ((KeyCode)key.Keycode == KeyCode.LeftWindows) + { + state.SetButton(VjoyButton.Fourteen, key.Pressed); + VjoyClient.UpdateDevice(deviceId, ref state); + } + } + } + /// /// Parses the state of the d-pad. /// diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index c5a8465..ceb0ba0 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -10,6 +10,7 @@ internal enum CommandId : byte { Arrival = 0x02, Descriptor = 0x04, + Keystroke = 0x07, Input = 0x20 } diff --git a/Program/PacketParsing/Packets/Keystroke.cs b/Program/PacketParsing/Packets/Keystroke.cs new file mode 100644 index 0000000..e4ac59a --- /dev/null +++ b/Program/PacketParsing/Packets/Keystroke.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + internal enum KeystrokeFlags + { + Pressed = 0x01, + } + + public enum KeyCode : byte + { + LeftWindows = 0x5B, // Used for the guide button + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct Keystroke + { + public byte Flags; + public byte Keycode; + + public bool Pressed => (Flags & (byte)KeystrokeFlags.Pressed) != 0; + } +} \ No newline at end of file diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 08d8c7e..5030efb 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -123,6 +123,7 @@ + From 7c7536fa888268c61b62f5e8cf2f013306dc2b3f Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Mar 2023 04:34:24 -0700 Subject: [PATCH 130/437] Add option to IDeviceMapper for mapping guide button presses --- Program/PacketParsing/Mappers/IDeviceMapper.cs | 2 ++ Program/PacketParsing/Mappers/VigemMapper.cs | 4 +++- Program/PacketParsing/Mappers/VjoyMapper.cs | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Program/PacketParsing/Mappers/IDeviceMapper.cs b/Program/PacketParsing/Mappers/IDeviceMapper.cs index f5ec0c5..37c5930 100644 --- a/Program/PacketParsing/Mappers/IDeviceMapper.cs +++ b/Program/PacketParsing/Mappers/IDeviceMapper.cs @@ -7,6 +7,8 @@ namespace RB4InstrumentMapper.Parsing ///
internal interface IDeviceMapper : IDisposable { + bool MapGuideButton { get; set; } + /// /// Handles an incoming packet. /// diff --git a/Program/PacketParsing/Mappers/VigemMapper.cs b/Program/PacketParsing/Mappers/VigemMapper.cs index 4d9bc73..abe3606 100644 --- a/Program/PacketParsing/Mappers/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/VigemMapper.cs @@ -11,6 +11,8 @@ namespace RB4InstrumentMapper.Parsing ///
internal abstract class VigemMapper : IDeviceMapper { + public bool MapGuideButton { get; set; } = false; + /// /// The device to map to. /// @@ -79,7 +81,7 @@ public void HandlePacket(CommandId command, ReadOnlySpan data) private unsafe void HandleKeystroke(ReadOnlySpan data) { - if (data.Length < sizeof(Keystroke)) + if (!MapGuideButton || data.Length < sizeof(Keystroke)) return; // Multiple keystrokes can be sent in a single message diff --git a/Program/PacketParsing/Mappers/VjoyMapper.cs b/Program/PacketParsing/Mappers/VjoyMapper.cs index 01171af..b470165 100644 --- a/Program/PacketParsing/Mappers/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/VjoyMapper.cs @@ -10,6 +10,8 @@ namespace RB4InstrumentMapper.Parsing ///
internal abstract class VjoyMapper : IDeviceMapper { + public bool MapGuideButton { get; set; } = false; + protected vJoy.JoystickState state = new vJoy.JoystickState(); protected uint deviceId = 0; @@ -62,7 +64,7 @@ public void HandlePacket(CommandId command, ReadOnlySpan data) private unsafe void HandleKeystroke(ReadOnlySpan data) { - if (data.Length < sizeof(Keystroke)) + if (!MapGuideButton || data.Length < sizeof(Keystroke)) return; // Multiple keystrokes can be sent in a single message From f0136a5540edcc5a2e6efcb27c2f7c06fe0a353d Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Mar 2023 16:48:32 -0700 Subject: [PATCH 131/437] Update packages --- Program/App.config | 6 +++++- Program/RB4InstrumentMapper.csproj | 16 ++++++++-------- Program/packages.config | 8 ++++---- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Program/App.config b/Program/App.config index 346a6f4..a636340 100644 --- a/Program/App.config +++ b/Program/App.config @@ -32,12 +32,16 @@ - + + + + + \ No newline at end of file diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 5030efb..91c0039 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -57,14 +57,14 @@ true - - ..\packages\Nefarius.ViGEm.Client.1.19.199\lib\netstandard2.0\Nefarius.ViGEm.Client.dll + + ..\packages\Nefarius.ViGEm.Client.1.21.256\lib\netstandard2.0\Nefarius.ViGEm.Client.dll - - ..\packages\PacketDotNet.1.4.6\lib\net47\PacketDotNet.dll + + ..\packages\PacketDotNet.1.4.7\lib\net47\PacketDotNet.dll - - ..\packages\SharpPcap.6.2.2\lib\netstandard2.0\SharpPcap.dll + + ..\packages\SharpPcap.6.2.5\lib\netstandard2.0\SharpPcap.dll packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll @@ -79,8 +79,8 @@ packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - packages\System.Text.Encoding.CodePages.6.0.0\lib\net461\System.Text.Encoding.CodePages.dll + + ..\packages\System.Text.Encoding.CodePages.7.0.0\lib\net462\System.Text.Encoding.CodePages.dll False diff --git a/Program/packages.config b/Program/packages.config index b259a51..6be5412 100644 --- a/Program/packages.config +++ b/Program/packages.config @@ -1,11 +1,11 @@  - - - + + + - + \ No newline at end of file From 8ca1a438caf476e65ba15118ad8edd23cb9dd591 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 8 Mar 2023 16:49:34 -0700 Subject: [PATCH 132/437] Fix some incorrect file references in project --- Program/RB4InstrumentMapper.csproj | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 91c0039..6c3f712 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -1,4 +1,4 @@ - + @@ -167,14 +167,13 @@ ResXFileCodeGenerator Resources.Designer.cs - + - SettingsSingleFileGenerator Settings.Designer.cs - + From ea048877fbe817351beb5b91f5ff80da01342d22 Mon Sep 17 00:00:00 2001 From: TheFatBastid Date: Sun, 16 Apr 2023 20:53:37 -0400 Subject: [PATCH 133/437] Added guid from the pdp jaguar guitar Taken from plasticband --- Program/PacketParsing/DeviceGuids.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Program/PacketParsing/DeviceGuids.cs b/Program/PacketParsing/DeviceGuids.cs index c5b9a81..a5a1bd4 100644 --- a/Program/PacketParsing/DeviceGuids.cs +++ b/Program/PacketParsing/DeviceGuids.cs @@ -9,10 +9,10 @@ internal static class DeviceGuids { public static readonly Guid MadCatzGuitar = Guid.Parse("0D2AE438-7F7D-4933-8693-30FC55018E77"); public static readonly Guid MadCatzDrumkit = Guid.Parse("06182893-CCE0-4B85-9271-0A10DBAB7E07"); - // public static readonly Guid PdpGuitar = Guid.Parse(""); // Not known yet + public static readonly Guid PdpGuitar = Guid.Parse("1A266AF6-3A46-45E3-B9B6-0F2C0B2C1EBE"); public static readonly Guid PdpDrumkit = Guid.Parse("A503F9B0-955E-47C4-A2ED-B1336FA7703E"); public static readonly Guid XboxInputDevice = Guid.Parse("9776FF56-9BFD-4581-AD45-B645BBA526D6"); public static readonly Guid XboxNavigationController = Guid.Parse("B8F31FE7-7386-40E9-A9F8-2F21263ACFB7"); } -} \ No newline at end of file +} From 68ede7b205f24ced31c1df4aec4fd33567975a8e Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 12 May 2023 03:26:54 -0600 Subject: [PATCH 134/437] Update readme --- Docs/Images/ProgramScreenshot.png | Bin 70271 -> 54953 bytes README.md | 24 ++++++------------------ 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/Docs/Images/ProgramScreenshot.png b/Docs/Images/ProgramScreenshot.png index c6cf44af62a4d8fed63882facc9077726edf0e2a..b1ef3b431c2bff6799a3f044e5843e7e7071cd64 100644 GIT binary patch literal 54953 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

s2s*fX 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 literal 70271 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 Devices > Bluetooth & other devices 2. Click `Add Bluetooth or other device` and pick the `Everything else` option. @@ -30,8 +30,7 @@ Some guitars/drumkits might not sync properly when using just the sync button. T ## Installation -1. Install [WinPCap](https://www.winpcap.org/install/default.htm). - - While Npcap is newer, it does not seem to work with Xbox One receivers currently, and trying it isn't recommended unless WinPcap doesn't work and none of the other troubleshooting helps. +1. Install [WinPCap](https://www.winpcap.org/install/bin/WinPcap_4_1_3.exe). 2. Install [USBPCap](https://desowin.org/usbpcap/). 3. Install [ViGEmBus](https://github.com/ViGEm/ViGEmBus/releases/latest) (recommended) or [vJoy](https://github.com/jshafer817/vJoy/releases/latest). - If you installed vJoy, configure it: @@ -94,20 +93,9 @@ Additional documentation is available in the [PlasticBand documentation reposito To build this program, you will need: -- Visual Studio (or MSBuild + your code editor of choice) for the program -- [WiX Toolset](https://wixtoolset.org/) for the installer +- Visual Studio (or MSBuild + your code editor of choice). +- [WiX Toolset](https://wixtoolset.org/) if you wish to build the installer. ## License -Copyright (c) 2021 Andreas Schiffler - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. \ No newline at end of file +This program is licensed under the MIT license. See [LICENSE](LICENSE) for details. From cd9a44ef09f43ad666d59d27f55b2840d2ace4c7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 14:03:58 -0600 Subject: [PATCH 135/437] Fix refreshing and auto-detection --- Program/MainWindow/MainWindow.xaml.cs | 36 +++++++++++++++------------ 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 69c732a..e4e6d61 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -38,7 +38,7 @@ public partial class MainWindow : Window ///

/// List of available Pcap devices. /// - private CaptureDeviceList pcapDeviceList = null; + private readonly CaptureDeviceList pcapDeviceList = CaptureDeviceList.Instance; /// /// The selected Pcap device. @@ -224,9 +224,8 @@ private void PopulatePcapDropdown() // Clear combo list pcapDeviceCombo.Items.Clear(); - // Retrieve the device list from the local machine - pcapDeviceList = CaptureDeviceList.Instance; - + // Refresh the device list + pcapDeviceList.Refresh(); if (pcapDeviceList.Count == 0) { Console.WriteLine("No Pcap devices found!"); @@ -616,15 +615,17 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) { MessageBoxResult result; - // Get the list of devices for when receiver is unplugged - CaptureDeviceList deviceList = CaptureDeviceList.Instance; - foreach (var device in deviceList) + // Refresh and check for Xbox One receivers + pcapDeviceList.Refresh(); + bool foundDevice = false; + foreach (var device in pcapDeviceList) { if (!IsXboxOneReceiver(device)) { continue; } + foundDevice = true; result = MessageBox.Show( $"Found Xbox One receiver device: {device.Description}\nPress OK to set this device as your selected Pcap device, or press Cancel to continue with the auto-detection process.", "Auto-Detect Receiver", @@ -649,14 +650,17 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) } } - result = MessageBox.Show( - "No Xbox One receivers could be found through checking device properties.\nYou will now be guided through a second auto-detection process. Press Cancel at any time to cancel the process.", - "Auto-Detect Receiver", - MessageBoxButton.OKCancel - ); - if (result == MessageBoxResult.Cancel) + if (!foundDevice) { - return; + result = MessageBox.Show( + "No Xbox One receivers could be found through checking device properties.\nYou will now be guided through a second auto-detection process. Press Cancel at any time to cancel the process.", + "Auto-Detect Receiver", + MessageBoxButton.OKCancel + ); + if (result == MessageBoxResult.Cancel) + { + return; + } } // Prompt user to unplug their receiver @@ -674,7 +678,7 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) Thread.Sleep(1000); // Get the list of devices for when receiver is unplugged - CaptureDeviceList notPlugged = CaptureDeviceList.Instance; + CaptureDeviceList notPlugged = CaptureDeviceList.New(); // Prompt user to plug in their receiver result = MessageBox.Show( @@ -691,7 +695,7 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) Thread.Sleep(1000); // Get the list of devices for when receiver is plugged in - CaptureDeviceList plugged = CaptureDeviceList.Instance; + CaptureDeviceList plugged = CaptureDeviceList.New(); // Get device names for both not plugged and plugged lists List notPluggedNames = new List(); From 51a3b6540f5aa6c6c92fa3eaaa35e28543429c3e Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 14:05:06 -0600 Subject: [PATCH 136/437] Remove delay in auto-detection process --- Program/MainWindow/MainWindow.xaml.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index e4e6d61..21843d5 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -665,7 +665,7 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) // Prompt user to unplug their receiver result = MessageBox.Show( - "Unplug your receiver, then click OK.\n(A 1-second delay will be taken to ensure that it registers as disconnected.)", + "Unplug your receiver, then click OK.", "Auto-Detect Receiver", MessageBoxButton.OKCancel ); @@ -674,15 +674,12 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) return; } - // Wait for a moment before getting the new list to ensure the device is registered - Thread.Sleep(1000); - // Get the list of devices for when receiver is unplugged CaptureDeviceList notPlugged = CaptureDeviceList.New(); // Prompt user to plug in their receiver result = MessageBox.Show( - "Now plug in your receiver, wait a bit for it to register, then click OK.\n(A 1-second delay will be taken to ensure that it registers as connected.)", + "Now plug in your receiver, wait a bit for it to register, then click OK.", "Auto-Detect Receiver", MessageBoxButton.OKCancel ); @@ -691,9 +688,6 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) return; } - // Wait for a moment before getting the new list to ensure the device is registered - Thread.Sleep(1000); - // Get the list of devices for when receiver is plugged in CaptureDeviceList plugged = CaptureDeviceList.New(); From 037b992df5047b4b440c7e1421377d258c2ac0c7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 14:49:20 -0600 Subject: [PATCH 137/437] Fix Xbox One receiver detection --- Program/MainWindow/MainWindow.xaml.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 21843d5..5268019 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -200,17 +200,14 @@ private void Window_Closed(object sender, EventArgs e) /// private bool IsXboxOneReceiver(ILiveDevice device) { - if (device.Description != null) - { - // TODO: Research if there are any other device names to check for, or other methods to detect receivers - // This won't work anymore if the receiver changes device name down the line - if (device.Description == "MT7612US_RL") - { - return true; - } - } - - return false; + // Depending on the receiver, there are two ways of detection: + // - Description of "MT7612US_RL" + // - Empty device properties + return device.Description == "MT7612US_RL" || + (string.IsNullOrWhiteSpace(device.Description) && + string.IsNullOrWhiteSpace(device.Filter) && + (device.MacAddress == null || device.MacAddress.GetAddressBytes() == null || + device.MacAddress.GetAddressBytes().Length == 0)); } /// From b4e16bb7a5f035cceee12b2378b07e3ea834ca5e Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 19:12:04 -0600 Subject: [PATCH 138/437] Set PDB type to portable for VS Code debugging --- Program/RB4InstrumentMapper.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 6c3f712..2c48187 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -20,7 +20,7 @@ AnyCPU true - full + portable false bin\Debug\ DEBUG;TRACE @@ -40,7 +40,7 @@ true bin\x64\Debug\ DEBUG;TRACE - full + portable x64 7.3 prompt From 8e3fabcd8d7326a6d2524c0893b36a2b061eac9c Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 19:13:09 -0600 Subject: [PATCH 139/437] Create PcapDeviceExtensions file --- Program/MainWindow/MainWindow.xaml.cs | 21 +++------------------ Program/MainWindow/PcapDeviceExtensions.cs | 22 ++++++++++++++++++++++ Program/RB4InstrumentMapper.csproj | 1 + 3 files changed, 26 insertions(+), 18 deletions(-) create mode 100644 Program/MainWindow/PcapDeviceExtensions.cs diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 5268019..03e3323 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -195,21 +195,6 @@ private void Window_Closed(object sender, EventArgs e) VigemClient.Dispose(); } - /// - /// Determines whether or not a device is an Xbox One receiver. - /// - private bool IsXboxOneReceiver(ILiveDevice device) - { - // Depending on the receiver, there are two ways of detection: - // - Description of "MT7612US_RL" - // - Empty device properties - return device.Description == "MT7612US_RL" || - (string.IsNullOrWhiteSpace(device.Description) && - string.IsNullOrWhiteSpace(device.Filter) && - (device.MacAddress == null || device.MacAddress.GetAddressBytes() == null || - device.MacAddress.GetAddressBytes().Length == 0)); - } - /// /// Populates the Pcap device combo. /// @@ -255,7 +240,7 @@ private void PopulatePcapDropdown() string itemName = pcapComboBoxItemName + itemNumber; bool isSelected = device.Name.Equals(currentPcapSelection) || device.Name.Equals(pcapSelectedDevice?.Name); - if (isSelected || (string.IsNullOrEmpty(currentPcapSelection) && IsXboxOneReceiver(device))) + if (isSelected || (string.IsNullOrEmpty(currentPcapSelection) && device.IsXboxOneReceiver())) { pcapSelectedDevice = device; } @@ -617,7 +602,7 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) bool foundDevice = false; foreach (var device in pcapDeviceList) { - if (!IsXboxOneReceiver(device)) + if (!device.IsXboxOneReceiver()) { continue; } diff --git a/Program/MainWindow/PcapDeviceExtensions.cs b/Program/MainWindow/PcapDeviceExtensions.cs new file mode 100644 index 0000000..7b7a75f --- /dev/null +++ b/Program/MainWindow/PcapDeviceExtensions.cs @@ -0,0 +1,22 @@ +using SharpPcap; + +namespace RB4InstrumentMapper +{ + public static class PcapDeviceExtensions + { + /// + /// Determines whether or not a capture device is an Xbox One receiver. + /// + public static bool IsXboxOneReceiver(this ILiveDevice device) + { + // Depending on the receiver, there are two ways of detection: + // - Description of "MT7612US_RL" + // - Empty device properties + return device.Description == "MT7612US_RL" || + (string.IsNullOrWhiteSpace(device.Description) && + string.IsNullOrWhiteSpace(device.Filter) && + (device.MacAddress == null || device.MacAddress.GetAddressBytes() == null || + device.MacAddress.GetAddressBytes().Length == 0)); + } + } +} \ No newline at end of file diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 2c48187..ee4261d 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -129,6 +129,7 @@ + From 06c6b89af8d4ff1e9becc3d43ed15832f65688c3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 19:13:31 -0600 Subject: [PATCH 140/437] Fix display name being empty in some places on some receivers --- Program/MainWindow/MainWindow.xaml.cs | 23 +++++----------------- Program/MainWindow/PcapDeviceExtensions.cs | 13 ++++++++++++ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 03e3323..4a58646 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -218,25 +218,12 @@ private void PopulatePcapDropdown() string currentPcapSelection = Properties.Settings.Default.pcapDevice; // Populate combo and print the list - StringBuilder sb = new StringBuilder(); for (int i = 0; i < pcapDeviceList.Count; i++) { ILiveDevice device = pcapDeviceList[i]; string itemNumber = $"{i + 1}"; - sb.Clear(); - sb.Append($"{itemNumber}. "); - if (device.Description != null) - { - sb.Append(device.Description); - sb.Append($" ({device.Name})"); - } - else - { - sb.Append(device.Name); - } - - string deviceName = sb.ToString(); + string deviceName = $"{itemNumber}. {device.GetDisplayName()}"; string itemName = pcapComboBoxItemName + itemNumber; bool isSelected = device.Name.Equals(currentPcapSelection) || device.Name.Equals(pcapSelectedDevice?.Name); @@ -379,7 +366,7 @@ private void StartCapture() // Start capture PcapBackend.LogPackets = packetDebug; PcapBackend.StartCapture(pcapSelectedDevice); - Console.WriteLine($"Listening on {pcapSelectedDevice.Description}..."); + Console.WriteLine($"Listening on {pcapSelectedDevice.GetDisplayName()}..."); } /// @@ -448,7 +435,7 @@ private void pcapDeviceCombo_SelectionChanged(object sender, SelectionChangedEve // Assign device pcapSelectedDevice = pcapDeviceList[pcapDeviceIndex]; - Console.WriteLine($"Selected Pcap device {pcapSelectedDevice.Description}"); + Console.WriteLine($"Selected Pcap device {pcapSelectedDevice.GetDisplayName()}"); // Enable start button SetStartButtonEnabled(); @@ -609,7 +596,7 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) foundDevice = true; result = MessageBox.Show( - $"Found Xbox One receiver device: {device.Description}\nPress OK to set this device as your selected Pcap device, or press Cancel to continue with the auto-detection process.", + $"Found Xbox One receiver device: {device.GetDisplayName()}\nPress OK to set this device as your selected Pcap device, or press Cancel to continue with the auto-detection process.", "Auto-Detect Receiver", MessageBoxButton.OKCancel ); diff --git a/Program/MainWindow/PcapDeviceExtensions.cs b/Program/MainWindow/PcapDeviceExtensions.cs index 7b7a75f..62a2d4c 100644 --- a/Program/MainWindow/PcapDeviceExtensions.cs +++ b/Program/MainWindow/PcapDeviceExtensions.cs @@ -4,6 +4,19 @@ namespace RB4InstrumentMapper { public static class PcapDeviceExtensions { + /// + /// Gets the display name for a capture device. + /// + public static string GetDisplayName(this ILiveDevice device) + { + if (!string.IsNullOrWhiteSpace(device.Description)) + { + return $"{device.Description} ({device.Name})"; + } + + return device.Name; + } + /// /// Determines whether or not a capture device is an Xbox One receiver. /// From 7fa37491d2a61b3d6a9ead3e731ba3f81a2f8fac Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 19:34:12 -0600 Subject: [PATCH 141/437] Adjust packet debug logging to split QoS header and packet data --- Program/PacketParsing/Backends/PcapBackend.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 70d412a..4a8c76d 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -97,13 +97,19 @@ public static void StopCapture() /// private static unsafe void OnPacketArrival(object sender, PacketCapture packet) { - // Read out receiver header - var data = packet.Data; - if (data.Length < sizeof(QoSHeader) || !MemoryMarshal.TryRead(data, out QoSHeader header)) + // Ignore invalid packets or packets with only a header + if (packet.Data.Length <= sizeof(QoSHeader)) + { + return; + } + + // Split header and packet data, and read header out + var headerData = packet.Data.Slice(0, sizeof(QoSHeader)); + var packetData = packet.Data.Slice(sizeof(QoSHeader)); + if (!MemoryMarshal.TryRead(packet.Data, out QoSHeader header)) { return; } - data = data.Slice(sizeof(QoSHeader)); // Ensure type and subtype are Data, QoS Data respectively // Other frame types are irrelevant for our purposes @@ -137,7 +143,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); // Check if device was found during its initialization - CommandId command = (CommandId)data[0]; + CommandId command = (CommandId)packetData[0]; if (command != CommandId.Arrival && command != CommandId.Descriptor) { Console.WriteLine("Warning: This device was not encountered during its initial connection! It will use the fallback mapper instead of one specific to its device interface."); @@ -148,7 +154,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) try { - device.HandlePacket(data); + device.HandlePacket(packetData); } catch (ThreadAbortException) { @@ -168,8 +174,8 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) // Debugging (if enabled) if (LogPackets) { - RawCapture raw = packet.GetPacket(); - string packetLogString = raw.Timeval.Date.ToString("yyyy-MM-dd hh:mm:ss.fff") + $" [{raw.PacketLength}] " + ParsingHelpers.ByteArrayToHexString(raw.Data);; + string packetLogString = $"{packet.Header.Timeval.Date:yyyy-MM-dd hh:mm:ss.fff} [{packet.Data.Length}] " + + $"{BitConverter.ToString(headerData.ToArray())} | {BitConverter.ToString(packetData.ToArray())}"; Console.WriteLine(packetLogString); Logging.Packet_WriteLine(packetLogString); } From a6f0bf8a8a6235dc35960e3b8f43c8d53014b272 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 19:37:19 -0600 Subject: [PATCH 142/437] Fix LEB128 decoding --- Program/PacketParsing/ParsingUtils.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Program/PacketParsing/ParsingUtils.cs b/Program/PacketParsing/ParsingUtils.cs index 5a44e64..aff8733 100644 --- a/Program/PacketParsing/ParsingUtils.cs +++ b/Program/PacketParsing/ParsingUtils.cs @@ -21,15 +21,14 @@ public static bool DecodeLEB128(ReadOnlySpan data, out int result, out int // Decode variable-length length value // Sequence length is limited to 4 bytes - byte value = data[0]; - for (int index = 0; - (index < data.Length) && (index < sizeof(int)) && ((value & 0x80) != 0); - index++) + byte value; + do { - value = data[index]; - result |= (value & 0x7F) << (index * 7); + value = data[byteLength]; + result |= (value & 0x7F) << (byteLength * 7); byteLength++; } + while ((value & 0x80) != 0 && byteLength < sizeof(int)); // Detect length sequences longer than 4 bytes if ((value & 0x80) != 0) From e62dd42e4940b2ba7cc10440398ff094824d0d2f Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 23:00:08 -0600 Subject: [PATCH 143/437] Rewrite chunk sequence handling Cleans up and fixes issues with the original implementation --- Program/PacketParsing/XboxDevice.cs | 139 +++++++++++++++++++++------- 1 file changed, 103 insertions(+), 36 deletions(-) diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 7d68461..3e7a9cd 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -67,14 +67,36 @@ public unsafe void HandlePacket(ReadOnlySpan data) // and where the next message's header begins. while (data.Length > 0) { - if (!CommandHeader.TryParse(data, out var header, out int bytesRead) || data.Length < (bytesRead + header.DataLength)) + // Command header + if (!CommandHeader.TryParse(data, out var header, out int headerLength)) { return; } - var commandData = data.Slice(bytesRead, header.DataLength); - data = data.Slice(bytesRead + header.DataLength); + int messageLength = headerLength + header.DataLength; + // Chunked messages + if ((header.Flags & CommandFlags.ChunkPacket) != 0) + { + if (!ParsingUtils.DecodeLEB128(data.Slice(headerLength), out int _, out int indexLength)) + { + return; + } + + messageLength += indexLength; + } + + // Verify bounds + if (data.Length < messageLength) + { + return; + } + + var messageData = data.Slice(0, messageLength); + var commandData = messageData.Slice(headerLength); // Chunk index is not removed here, as message handling needs it HandleMessage(header, commandData); + + // Progress to next message + data = data.Slice(messageData.Length); } } @@ -86,48 +108,19 @@ private unsafe void HandleMessage(CommandHeader header, ReadOnlySpan comma // Chunked packets if ((header.Flags & CommandFlags.ChunkPacket) != 0) { - // Get sequence length/index - if (!ParsingUtils.DecodeLEB128(commandData, out int bufferIndex, out int bytesRead)) + if (!ProcessPacketChunk(header, ref commandData)) { + // Chunk is ongoing or there was an error return; } - commandData = commandData.Slice(bytesRead); - // Do nothing with chunks of length 0 - if (bufferIndex > 0) - { - // Buffer index equalling buffer length signals the end of the sequence - if (chunkBuffer != null && bufferIndex >= chunkBuffer.Length) - { - Debug.Assert(commandData.Length == 0); - commandData = chunkBuffer; - } - else - { - if ((header.Flags & CommandFlags.ChunkStart) != 0) - { - Debug.Assert(chunkBuffer == null); - // Buffer index is the total size of the buffer on the starting packet - chunkBuffer = new byte[bufferIndex]; - } - - Debug.Assert(chunkBuffer != null); - Debug.Assert((bufferIndex + commandData.Length) >= chunkBuffer.Length); - if (chunkBuffer == null || ((bufferIndex + commandData.Length) >= chunkBuffer.Length)) - { - return; - } - - commandData.CopyTo(chunkBuffer.AsSpan(bufferIndex, commandData.Length)); - return; - } - } + header.DataLength = commandData.Length; + header.Flags &= ~(CommandFlags.ChunkPacket | CommandFlags.ChunkStart); } // Ensure lengths match if (header.DataLength != commandData.Length) { - // This is probably a bug Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {commandData.Length}"); return; } @@ -159,6 +152,80 @@ private unsafe void HandleMessage(CommandHeader header, ReadOnlySpan comma } } + private unsafe bool ProcessPacketChunk(CommandHeader header, ref ReadOnlySpan chunkData) + { + // Get sequence length/index + if (!ParsingUtils.DecodeLEB128(chunkData, out int bufferIndex, out int bytesRead)) + { + return false; + } + chunkData = chunkData.Slice(bytesRead); + + // Verify packet length + if (header.DataLength != chunkData.Length) + { + Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {chunkData.Length}"); + return false; + } + + // Do nothing with chunks of length 0 + if (bufferIndex <= 0) + { + // Chunked packets with a length of 0 are valid and have been observed with Elite controllers + bool emptySequence = bufferIndex == 0; + Debug.Assert(emptySequence, $"Negative buffer index {bufferIndex}!"); + return emptySequence; + } + + // Start of the chunk sequence + if (chunkBuffer == null || (header.Flags & CommandFlags.ChunkStart) != 0) + { + // Safety check + if ((header.Flags & CommandFlags.ChunkStart) == 0) + { + Debug.Fail("Invalid chunk sequence start! No chunk buffer exists, expected a chunk start packet"); + return false; + } + + // Buffer index is the total size of the buffer on the starting packet + chunkBuffer = new byte[bufferIndex]; + bufferIndex = 0; + } + + // Buffer index equalling buffer length signals the end of the sequence + if (bufferIndex >= chunkBuffer.Length) + { + // Safety checks + if (bufferIndex > chunkBuffer.Length) + { + Debug.Fail("Invalid chunk sequence end! Buffer index is beyond the end of the chunk buffer"); + return false; + } + + if (chunkData.Length != 0) + { + Debug.Fail("Invalid chunk sequence end! Data was provided beyond the end of the buffer"); + return false; + } + + // Send off finished chunk buffer + chunkData = chunkBuffer; + chunkBuffer = null; + return true; + } + + // Verify chunk data bounds + if ((bufferIndex + chunkData.Length) > chunkBuffer.Length) + { + Debug.Fail($"Invalid chunk sequence! Data was provided beyond the end of the buffer"); + return false; + } + + // Copy data to buffer + chunkData.CopyTo(chunkBuffer.AsSpan(bufferIndex, chunkData.Length)); + return false; + } + /// /// Handles the arrival message of the device. /// From 4a3ca450405467e3f2901f078850d672e790525b Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 23:40:13 -0600 Subject: [PATCH 144/437] Uncomment PDP guitar entry in device mapper factory --- Program/PacketParsing/Mappers/MapperFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index d6d2d73..c8fa0da 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -12,7 +12,7 @@ internal static class MapperFactory private static Dictionary> guidToMapper = new Dictionary>() { { DeviceGuids.MadCatzGuitar, GetGuitarMapper }, - // { DeviceGuids.PdpGuitar, GetGuitarMapper }, + { DeviceGuids.PdpGuitar, GetGuitarMapper }, { DeviceGuids.MadCatzDrumkit, GetDrumsMapper }, { DeviceGuids.PdpDrumkit, GetDrumsMapper }, }; From 69d8e49cb2a19a2d5018c719876d7d569cf6e8f7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 20 Jun 2023 23:40:56 -0600 Subject: [PATCH 145/437] Check explicitly for if interface GUID couldn't be determined --- Program/PacketParsing/Mappers/MapperFactory.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index c8fa0da..7f96b98 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -47,6 +47,12 @@ public static IDeviceMapper GetMapper(IReadOnlyList interfaceGuids, Mappin interfaceGuid = guid; } + if (interfaceGuid == default) + { + Console.WriteLine($"Could not find interface GUID for device! Using fallback mapper instead."); + return GetFallbackMapper(mode); + } + // Get mapper creation delegate for interface GUID if (!guidToMapper.TryGetValue(interfaceGuid, out var func)) { From f0c895703f8cf1ce4ac5093d96ec9df65b80b886 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 21 Jun 2023 02:44:06 -0600 Subject: [PATCH 146/437] Fix drums mapper being used instead of the fallback mapper --- Program/PacketParsing/Mappers/MapperFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 7f96b98..de84080 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -91,8 +91,8 @@ public static IDeviceMapper GetFallbackMapper(MappingMode mode) Console.WriteLine($"Creating new fallback {mode} mapper..."); switch (mode) { - case MappingMode.ViGEmBus: return new DrumsVigemMapper(); - case MappingMode.vJoy: return new DrumsVjoyMapper(); + case MappingMode.ViGEmBus: return new FallbackVigemMapper(); + case MappingMode.vJoy: return new FallbackVjoyMapper(); default: throw new Exception("Unhandled mapping mode!"); } } From 116549f0dd90d05d98be9e68afc2b1c538e39af1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 21 Jun 2023 04:23:24 -0600 Subject: [PATCH 147/437] Ignore all unsupported interface GUIDs instead of using an exclusion list --- Program/PacketParsing/Mappers/MapperFactory.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index de84080..5b43fdb 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -17,20 +17,13 @@ internal static class MapperFactory { DeviceGuids.PdpDrumkit, GetDrumsMapper }, }; - // Non-unique interface GUIDs to ignore - private static List guidExclusionList = new List() - { - DeviceGuids.XboxInputDevice, - DeviceGuids.XboxNavigationController, - }; - public static IDeviceMapper GetMapper(IReadOnlyList interfaceGuids, MappingMode mode) { // Get unique interface GUID Guid interfaceGuid = default; foreach (var guid in interfaceGuids) { - if (guidExclusionList.Contains(guid)) + if (!guidToMapper.ContainsKey(guid)) continue; if (interfaceGuid != default) @@ -50,6 +43,11 @@ public static IDeviceMapper GetMapper(IReadOnlyList interfaceGuids, Mappin if (interfaceGuid == default) { Console.WriteLine($"Could not find interface GUID for device! Using fallback mapper instead."); + Console.WriteLine($"Consider filing a GitHub issue with the GUIDs below so that this can be addressed:"); + foreach (var guid2 in interfaceGuids) + { + Console.WriteLine($"- {guid2}"); + } return GetFallbackMapper(mode); } From 7f7742abe32e3e34c2fe82bfd7c2f81769ddae48 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 21 Jun 2023 04:42:54 -0600 Subject: [PATCH 148/437] Add gamepad mapper for debugging purposes --- Program/PacketParsing/DeviceGuids.cs | 1 + .../Mappers/FallbackVigemMapper.cs | 6 ++ .../Mappers/FallbackVjoyMapper.cs | 6 ++ .../Mappers/GamepadVigemMapper.cs | 89 +++++++++++++++++++ .../Mappers/GamepadVjoyMapper.cs | 87 ++++++++++++++++++ .../PacketParsing/Mappers/MapperFactory.cs | 16 ++++ .../PacketParsing/Packets/GamepadButton.cs | 28 ------ Program/PacketParsing/Packets/GamepadInput.cs | 68 ++++++++++++++ Program/PacketParsing/ParsingUtils.cs | 11 +++ Program/RB4InstrumentMapper.csproj | 4 +- 10 files changed, 287 insertions(+), 29 deletions(-) create mode 100644 Program/PacketParsing/Mappers/GamepadVigemMapper.cs create mode 100644 Program/PacketParsing/Mappers/GamepadVjoyMapper.cs delete mode 100644 Program/PacketParsing/Packets/GamepadButton.cs create mode 100644 Program/PacketParsing/Packets/GamepadInput.cs diff --git a/Program/PacketParsing/DeviceGuids.cs b/Program/PacketParsing/DeviceGuids.cs index a5a1bd4..7a97683 100644 --- a/Program/PacketParsing/DeviceGuids.cs +++ b/Program/PacketParsing/DeviceGuids.cs @@ -14,5 +14,6 @@ internal static class DeviceGuids public static readonly Guid XboxInputDevice = Guid.Parse("9776FF56-9BFD-4581-AD45-B645BBA526D6"); public static readonly Guid XboxNavigationController = Guid.Parse("B8F31FE7-7386-40E9-A9F8-2F21263ACFB7"); + public static readonly Guid XboxGamepad = Guid.Parse("082E402C-07DF-45E1-A5AB-A3127AF197B5"); } } diff --git a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs index c4bcf93..c97282d 100644 --- a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs @@ -46,6 +46,12 @@ private unsafe void ParseInput(ReadOnlySpan data) { DrumsVigemMapper.HandleReport(device, drumReport, ref previousDpadCymbals, ref dpadMask); } +#if DEBUG + else if (data.Length == sizeof(GamepadInput) && MemoryMarshal.TryRead(data, out GamepadInput gamepadReport)) + { + GamepadVigemMapper.HandleReport(device, gamepadReport); + } +#endif else { // Not handled diff --git a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs index 3751d6e..6a19062 100644 --- a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs @@ -42,6 +42,12 @@ public unsafe void ParseInput(ReadOnlySpan data) { DrumsVjoyMapper.HandleReport(ref state, drumReport); } +#if DEBUG + else if (data.Length == sizeof(GamepadInput) && MemoryMarshal.TryRead(data, out GamepadInput gamepadReport)) + { + GamepadVjoyMapper.HandleReport(ref state, gamepadReport); + } +#endif else { // Not handled diff --git a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs b/Program/PacketParsing/Mappers/GamepadVigemMapper.cs new file mode 100644 index 0000000..8b3a15c --- /dev/null +++ b/Program/PacketParsing/Mappers/GamepadVigemMapper.cs @@ -0,0 +1,89 @@ +using System; +using System.Runtime.InteropServices; +using Nefarius.ViGEm.Client.Targets; +using Nefarius.ViGEm.Client.Targets.Xbox360; + +#if DEBUG + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Maps gamepad inputs to a ViGEmBus device. + /// + internal class GamepadVigemMapper : VigemMapper + { + public GamepadVigemMapper() : base() + { + } + + /// + /// Handles an incoming packet. + /// + protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + { + switch (command) + { + case CommandId.Input: + ParseInput(data); + break; + + default: + break; + } + } + + /// + /// Parses an input report. + /// + private unsafe void ParseInput(ReadOnlySpan data) + { + if (data.Length < sizeof(GamepadInput) || !MemoryMarshal.TryRead(data, out GamepadInput gamepadReport)) + return; + + HandleReport(device, gamepadReport); + + // Send data + device.SubmitReport(); + } + + /// + /// Maps gamepad input data to an Xbox 360 controller. + /// + internal static void HandleReport(IXbox360Controller device, in GamepadInput report) + { + // Face buttons + device.SetButtonState(Xbox360Button.A, report.A); + device.SetButtonState(Xbox360Button.B, report.B); + device.SetButtonState(Xbox360Button.X, report.X); + device.SetButtonState(Xbox360Button.Y, report.Y); + + // Dpad + device.SetButtonState(Xbox360Button.Up, report.DpadUp); + device.SetButtonState(Xbox360Button.Down, report.DpadDown); + device.SetButtonState(Xbox360Button.Left, report.DpadLeft); + device.SetButtonState(Xbox360Button.Right, report.DpadRight); + + // Dpad + device.SetButtonState(Xbox360Button.LeftShoulder, report.LeftBumper); + device.SetButtonState(Xbox360Button.RightShoulder, report.RightBumper); + device.SetButtonState(Xbox360Button.LeftThumb, report.LeftStickPress); + device.SetButtonState(Xbox360Button.RightThumb, report.RightStickPress); + + // Menu and Options + device.SetButtonState(Xbox360Button.Start, report.Menu); + device.SetButtonState(Xbox360Button.Back, report.Options); + + // Sticks + device.SetAxisValue(Xbox360Axis.LeftThumbX, report.LeftStickX); + device.SetAxisValue(Xbox360Axis.LeftThumbY, report.LeftStickY); + device.SetAxisValue(Xbox360Axis.RightThumbX, report.RightStickX); + device.SetAxisValue(Xbox360Axis.RightThumbY, report.RightStickY); + + // Triggers + device.SetSliderValue(Xbox360Slider.LeftTrigger, (byte)(report.LeftTrigger >> 2)); + device.SetSliderValue(Xbox360Slider.RightTrigger, (byte)(report.RightTrigger >> 2)); + } + } +} + +#endif \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs b/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs new file mode 100644 index 0000000..3e3e238 --- /dev/null +++ b/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs @@ -0,0 +1,87 @@ +using System; +using System.Runtime.InteropServices; +using RB4InstrumentMapper.Vjoy; +using vJoyInterfaceWrap; + +#if DEBUG + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Maps gamepad inputs to a vJoy device. + /// + internal class GamepadVjoyMapper : VjoyMapper + { + public GamepadVjoyMapper() : base() + { + } + + /// + /// Handles an incoming packet. + /// + protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + { + switch (command) + { + case CommandId.Input: + ParseInput(data); + break; + + default: + break; + } + } + + /// + /// Parses an input report. + /// + public unsafe void ParseInput(ReadOnlySpan data) + { + if (data.Length < sizeof(GamepadInput) || !MemoryMarshal.TryRead(data, out GamepadInput gamepadReport)) + return; + + HandleReport(ref state, gamepadReport); + + // Send data + VjoyClient.UpdateDevice(deviceId, ref state); + } + + /// + /// Maps gamepad input data to a vJoy device. + /// + internal static void HandleReport(ref vJoy.JoystickState state, GamepadInput report) + { + // Buttons and axes are mapped the same way as they display in joy.cpl when used normally + + // Buttons + state.SetButton(VjoyButton.One, report.A); + state.SetButton(VjoyButton.Two, report.B); + state.SetButton(VjoyButton.Three, report.X); + state.SetButton(VjoyButton.Four, report.Y); + + state.SetButton(VjoyButton.Five, report.LeftBumper); + state.SetButton(VjoyButton.Six, report.RightBumper); + + state.SetButton(VjoyButton.Seven, report.Options); + state.SetButton(VjoyButton.Eight, report.Menu); + + state.SetButton(VjoyButton.Nine, report.LeftStickPress); + state.SetButton(VjoyButton.Ten, report.RightStickPress); + + // D-pad + ParseDpad(ref state, (GamepadButton)report.Buttons); + + // Sticks + state.AxisX = report.LeftStickX.ScaleToInt32(); + state.AxisY = report.LeftStickY.ScaleToInt32(); + state.AxisXRot = report.RightStickX.ScaleToInt32(); + state.AxisYRot = report.RightStickY.ScaleToInt32(); + + // Triggers + // These are both combined into a single axis + state.AxisZ = (report.RightTrigger - report.LeftTrigger) * 0x40100401; // Special scaling, since the triggers are 10-bit values + } + } +} + +#endif \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 5b43fdb..4ff4ff2 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -15,6 +15,9 @@ internal static class MapperFactory { DeviceGuids.PdpGuitar, GetGuitarMapper }, { DeviceGuids.MadCatzDrumkit, GetDrumsMapper }, { DeviceGuids.PdpDrumkit, GetDrumsMapper }, +#if DEBUG + { DeviceGuids.XboxGamepad, GetGamepadMapper }, +#endif }; public static IDeviceMapper GetMapper(IReadOnlyList interfaceGuids, MappingMode mode) @@ -62,6 +65,19 @@ public static IDeviceMapper GetMapper(IReadOnlyList interfaceGuids, Mappin return func(mode); } +#if DEBUG + public static IDeviceMapper GetGamepadMapper(MappingMode mode) + { + Console.WriteLine($"Ganepad found, creating new {mode} mapper..."); + switch (mode) + { + case MappingMode.ViGEmBus: return new GamepadVigemMapper(); + case MappingMode.vJoy: return new GamepadVjoyMapper(); + default: throw new Exception("Unhandled mapping mode!"); + } + } +#endif + public static IDeviceMapper GetGuitarMapper(MappingMode mode) { Console.WriteLine($"Guitar found, creating new {mode} mapper..."); diff --git a/Program/PacketParsing/Packets/GamepadButton.cs b/Program/PacketParsing/Packets/GamepadButton.cs deleted file mode 100644 index ff17a68..0000000 --- a/Program/PacketParsing/Packets/GamepadButton.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace RB4InstrumentMapper.Parsing -{ - /// - /// Flag definitions for the buttons bytes. - /// - [Flags] - internal enum GamepadButton : ushort - { - Sync = 0x0001, - Unused = 0x0002, - Menu = 0x0004, - Options = 0x0008, - A = 0x0010, - B = 0x0020, - X = 0x0040, - Y = 0x0080, - DpadUp = 0x0100, - DpadDown = 0x0200, - DpadLeft = 0x0400, - DpadRight = 0x0800, - LeftBumper = 0x1000, - RightBumper = 0x2000, - LeftStickPress = 0x4000, - RightStickPress = 0x8000 - } -} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/GamepadInput.cs b/Program/PacketParsing/Packets/GamepadInput.cs new file mode 100644 index 0000000..88a1b53 --- /dev/null +++ b/Program/PacketParsing/Packets/GamepadInput.cs @@ -0,0 +1,68 @@ +using System; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Flag definitions for the buttons bytes. + /// + [Flags] + internal enum GamepadButton : ushort + { + Sync = 0x0001, + Unused = 0x0002, + Menu = 0x0004, + Options = 0x0008, + A = 0x0010, + B = 0x0020, + X = 0x0040, + Y = 0x0080, + DpadUp = 0x0100, + DpadDown = 0x0200, + DpadLeft = 0x0400, + DpadRight = 0x0800, + LeftBumper = 0x1000, + RightBumper = 0x2000, + LeftStickPress = 0x4000, + RightStickPress = 0x8000 + } + +#if DEBUG + + /// + /// An input report from a drumkit. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct GamepadInput + { + public const ushort TriggerMax = 0x03FF; + + public bool A => (Buttons & (ushort)GamepadButton.A) != 0; + public bool B => (Buttons & (ushort)GamepadButton.B) != 0; + public bool X => (Buttons & (ushort)GamepadButton.X) != 0; + public bool Y => (Buttons & (ushort)GamepadButton.Y) != 0; + + public bool DpadUp => (Buttons & (ushort)GamepadButton.DpadUp) != 0; + public bool DpadDown => (Buttons & (ushort)GamepadButton.DpadDown) != 0; + public bool DpadLeft => (Buttons & (ushort)GamepadButton.DpadLeft) != 0; + public bool DpadRight => (Buttons & (ushort)GamepadButton.DpadRight) != 0; + + public bool LeftBumper => (Buttons & (ushort)GamepadButton.LeftBumper) != 0; + public bool RightBumper => (Buttons & (ushort)GamepadButton.RightBumper) != 0; + public bool LeftStickPress => (Buttons & (ushort)GamepadButton.LeftStickPress) != 0; + public bool RightStickPress => (Buttons & (ushort)GamepadButton.RightStickPress) != 0; + + public bool Menu => (Buttons & (ushort)GamepadButton.Menu) != 0; + public bool Options => (Buttons & (ushort)GamepadButton.Options) != 0; + + public ushort Buttons; + public ushort LeftTrigger; + public ushort RightTrigger; + public short LeftStickX; + public short LeftStickY; + public short RightStickX; + public short RightStickY; + } + +#endif +} \ No newline at end of file diff --git a/Program/PacketParsing/ParsingUtils.cs b/Program/PacketParsing/ParsingUtils.cs index aff8733..496f0c0 100644 --- a/Program/PacketParsing/ParsingUtils.cs +++ b/Program/PacketParsing/ParsingUtils.cs @@ -52,6 +52,17 @@ public static int ScaleToInt32(this byte input) return (int)((input * 0x01010101) ^ 0x80000000); } + /// + /// Scales this short to an int. + /// + public static int ScaleToInt32(this short input) + { + // Duplicate the input value to the higher 16-bit regions by multiplying by a number with the + // first bit of each region set to 1 + // Shorts already + return input * 0x00010001; + } + /// /// Scales this byte to a uint. /// diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index ee4261d..7d06c0c 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -112,6 +112,8 @@ + + @@ -121,7 +123,7 @@ - + From 65299ad770c64fe3364e9066807d89874b5f1359 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 21 Jun 2023 20:42:54 -0600 Subject: [PATCH 149/437] Fix various vJoy mapping issues Buttons did not work at all lol Axis mapping was also busted, wasn't aware of the 0-0x8000 range --- .../PacketParsing/Mappers/GamepadVjoyMapper.cs | 11 +++++------ .../PacketParsing/Mappers/GuitarVjoyMapper.cs | 6 +++--- Program/PacketParsing/Mappers/VjoyMapper.cs | 16 ++++++++++++++++ Program/Vjoy/VjoyExtensions.cs | 4 ++-- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs b/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs index 3e3e238..c67e3c5 100644 --- a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs @@ -71,15 +71,14 @@ internal static void HandleReport(ref vJoy.JoystickState state, GamepadInput rep // D-pad ParseDpad(ref state, (GamepadButton)report.Buttons); - // Sticks - state.AxisX = report.LeftStickX.ScaleToInt32(); - state.AxisY = report.LeftStickY.ScaleToInt32(); - state.AxisXRot = report.RightStickX.ScaleToInt32(); - state.AxisYRot = report.RightStickY.ScaleToInt32(); + // Left stick + SetAxis(ref state.AxisX, report.LeftStickX); + SetAxisInverted(ref state.AxisY, report.LeftStickY); // Triggers // These are both combined into a single axis - state.AxisZ = (report.RightTrigger - report.LeftTrigger) * 0x40100401; // Special scaling, since the triggers are 10-bit values + int triggerAxis = (report.LeftTrigger - report.RightTrigger) * 0x20; + SetAxis(ref state.AxisZ, (short)triggerAxis); } } } diff --git a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs index 0792bba..7835ffc 100644 --- a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs @@ -65,17 +65,17 @@ internal static void HandleReport(ref vJoy.JoystickState state, GuitarInput repo // Whammy // Value ranges from 0 (not pressed) to 255 (fully pressed) - state.AxisY = report.WhammyBar.ScaleToInt32(); + SetAxis(ref state.AxisY, report.WhammyBar); // Tilt // Value ranges from 0 to 255 // It seems to have a threshold of around 0x70 though, // after a certain point values will get floored to 0 - state.AxisZ = report.Tilt.ScaleToInt32(); + SetAxis(ref state.AxisZ, report.Tilt); // Pickup switch // Reported values are 0x00, 0x10, 0x20, 0x30, and 0x40 (ranges from 0 to 64) - state.AxisX = report.PickupSwitch.ScaleToInt32(); + state.AxisX = report.PickupSwitch * 0x200; } } } diff --git a/Program/PacketParsing/Mappers/VjoyMapper.cs b/Program/PacketParsing/Mappers/VjoyMapper.cs index b470165..17c814c 100644 --- a/Program/PacketParsing/Mappers/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/VjoyMapper.cs @@ -79,6 +79,22 @@ private unsafe void HandleKeystroke(ReadOnlySpan data) } } + // vJoy axes range from 0x0000 to 0x8000, but are exposed as full ints for some reason + protected static void SetAxis(ref int axisField, byte value) + { + axisField = (value * 0x0101) >> 1; + } + + protected static void SetAxis(ref int axisField, short value) + { + axisField = ((ushort)value ^ 0x8000) >> 1; + } + + protected static void SetAxisInverted(ref int axisField, short value) + { + axisField = 0x8000 - (((ushort)value ^ 0x8000) >> 1); + } + /// /// Parses the state of the d-pad. /// diff --git a/Program/Vjoy/VjoyExtensions.cs b/Program/Vjoy/VjoyExtensions.cs index f7cb2d8..96824dc 100644 --- a/Program/Vjoy/VjoyExtensions.cs +++ b/Program/Vjoy/VjoyExtensions.cs @@ -9,7 +9,7 @@ public static class VjoyExtensions /// Sets the state of the specified button. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetButton(this vJoy.JoystickState state, VjoyButton button, bool set) + public static void SetButton(ref this vJoy.JoystickState state, VjoyButton button, bool set) { if (set) { @@ -24,7 +24,7 @@ public static void SetButton(this vJoy.JoystickState state, VjoyButton button, b /// /// Resets the values of this state. /// - public static void Reset(this vJoy.JoystickState state) + public static void Reset(ref this vJoy.JoystickState state) { // Only reset the values we use state.Buttons = (uint)VjoyButton.None; From f33dee99ba9eb8a28fba293e9b31f77dd64f0429 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 21 Jun 2023 22:10:02 -0600 Subject: [PATCH 150/437] Fix more than one device mapper being created on device connection --- Program/PacketParsing/Backends/PcapBackend.cs | 9 -------- Program/PacketParsing/XboxDevice.cs | 21 ++++++++++++------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 4a8c76d..fc9dfe9 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -141,15 +141,6 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) devices.Add(deviceId, device); Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); - - // Check if device was found during its initialization - CommandId command = (CommandId)packetData[0]; - if (command != CommandId.Arrival && command != CommandId.Descriptor) - { - Console.WriteLine("Warning: This device was not encountered during its initial connection! It will use the fallback mapper instead of one specific to its device interface."); - Console.WriteLine("Consider hitting Start before connecting it to ensure correct behavior."); - // TODO: Figure out how to detect disconnections - } } try diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 3e7a9cd..6db8f39 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -41,14 +41,6 @@ public class XboxDevice : IDisposable ///
private readonly Dictionary previousSequenceIds = new Dictionary(); - /// - /// Creates a new XboxDevice with the given device ID and parsing mode. - /// - public XboxDevice() - { - deviceMapper = MapperFactory.GetFallbackMapper(MapperMode); - } - /// /// Performs cleanup on object finalization. /// @@ -146,6 +138,13 @@ private unsafe void HandleMessage(CommandHeader header, ReadOnlySpan comma break; default: + if (deviceMapper == null) + { + Console.WriteLine("Warning: This device was not encountered during its initial connection! It will use the fallback mapper instead of one specific to its device interface."); + Console.WriteLine("Consider hitting Start before connecting it to ensure correct behavior."); + deviceMapper = MapperFactory.GetFallbackMapper(MapperMode); + } + // Hand off unrecognized commands to the mapper deviceMapper.HandlePacket(header.CommandId, commandData); break; @@ -231,6 +230,9 @@ private unsafe bool ProcessPacketChunk(CommandHeader header, ref ReadOnlySpan private unsafe void HandleArrival(ReadOnlySpan data) { + if (VendorId != 0 || ProductId != 0) + return; + if (data.Length < sizeof(DeviceArrival) || MemoryMarshal.TryRead(data, out DeviceArrival arrival)) return; @@ -243,6 +245,9 @@ private unsafe void HandleArrival(ReadOnlySpan data) ///
private void HandleDescriptor(ReadOnlySpan data) { + if (Descriptor != null) + return; + if (!XboxDescriptor.Parse(data, out var descriptor)) return; From 9fec5dc6fc8e3bd52fb080b6ab2cd97c6d286120 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 21 Jun 2023 23:01:32 -0600 Subject: [PATCH 151/437] Fix device limit detection --- Program/PacketParsing/Backends/PcapBackend.cs | 19 ++++------- .../PacketParsing/DeviceCreationException.cs | 22 +++++++++++++ .../PacketParsing/Mappers/MapperFactory.cs | 32 +++++++++++++------ Program/RB4InstrumentMapper.csproj | 3 +- 4 files changed, 54 insertions(+), 22 deletions(-) create mode 100644 Program/PacketParsing/DeviceCreationException.cs diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index fc9dfe9..5734536 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -127,18 +127,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) return; } - try - { - device = new XboxDevice(); - } - catch (Exception ex) - { - canHandleNewDevices = false; - Console.WriteLine("Device limit reached, or an error occured when creating virtual device. No more devices will be registered."); - Console.WriteLine($"Exception: {ex.GetFirstLine()}"); - return; - } - + device = new XboxDevice(); devices.Add(deviceId, device); Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); } @@ -152,6 +141,12 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) // Don't log thread aborts, just return return; } + catch (DeviceCreationException ex) + { + canHandleNewDevices = false; + Console.WriteLine("Virtual device limit reached, or an error occured when creating one. No more devices will be registered."); + Console.WriteLine($"Exception: {ex.GetFirstLine()}"); + } catch (Exception ex) { Console.WriteLine($"Error while handling packet: {ex.GetFirstLine()}"); diff --git a/Program/PacketParsing/DeviceCreationException.cs b/Program/PacketParsing/DeviceCreationException.cs new file mode 100644 index 0000000..a537686 --- /dev/null +++ b/Program/PacketParsing/DeviceCreationException.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + public class DeviceCreationException : Exception + { + public DeviceCreationException() : base() + { + } + + public DeviceCreationException(string message) : base(message) + { + } + + public DeviceCreationException(string message, Exception inner) : base(message, inner) + { + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 4ff4ff2..b7d1f54 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -62,11 +62,18 @@ public static IDeviceMapper GetMapper(IReadOnlyList interfaceGuids, Mappin return GetFallbackMapper(mode); } - return func(mode); + try + { + return func(mode); + } + catch (Exception ex) + { + throw new DeviceCreationException("Failed to create mapper for device!", ex); + } } #if DEBUG - public static IDeviceMapper GetGamepadMapper(MappingMode mode) + private static IDeviceMapper GetGamepadMapper(MappingMode mode) { Console.WriteLine($"Ganepad found, creating new {mode} mapper..."); switch (mode) @@ -78,7 +85,7 @@ public static IDeviceMapper GetGamepadMapper(MappingMode mode) } #endif - public static IDeviceMapper GetGuitarMapper(MappingMode mode) + private static IDeviceMapper GetGuitarMapper(MappingMode mode) { Console.WriteLine($"Guitar found, creating new {mode} mapper..."); switch (mode) @@ -89,7 +96,7 @@ public static IDeviceMapper GetGuitarMapper(MappingMode mode) } } - public static IDeviceMapper GetDrumsMapper(MappingMode mode) + private static IDeviceMapper GetDrumsMapper(MappingMode mode) { Console.WriteLine($"Drumkit found, creating new {mode} mapper..."); switch (mode) @@ -102,12 +109,19 @@ public static IDeviceMapper GetDrumsMapper(MappingMode mode) public static IDeviceMapper GetFallbackMapper(MappingMode mode) { - Console.WriteLine($"Creating new fallback {mode} mapper..."); - switch (mode) + try { - case MappingMode.ViGEmBus: return new FallbackVigemMapper(); - case MappingMode.vJoy: return new FallbackVjoyMapper(); - default: throw new Exception("Unhandled mapping mode!"); + Console.WriteLine($"Creating new fallback {mode} mapper..."); + switch (mode) + { + case MappingMode.ViGEmBus: return new FallbackVigemMapper(); + case MappingMode.vJoy: return new FallbackVjoyMapper(); + default: throw new Exception("Unhandled mapping mode!"); + } + } + catch (Exception ex) + { + throw new DeviceCreationException("Failed to create fallback mapper for device!", ex); } } } diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 7d06c0c..94e9ab6 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -1,4 +1,4 @@ - + @@ -128,6 +128,7 @@ + From a708af93a9457a0129c10f780dc9b0a61f18af23 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 21 Jun 2023 23:12:33 -0600 Subject: [PATCH 152/437] Fix packet capture not stopping correctly on error --- Program/MainWindow/MainWindow.xaml.cs | 7 +++++++ Program/PacketParsing/Backends/PcapBackend.cs | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 4a58646..7dac76f 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -365,10 +365,17 @@ private void StartCapture() // Start capture PcapBackend.LogPackets = packetDebug; + PcapBackend.OnCaptureStop += OnCaptureStopped; PcapBackend.StartCapture(pcapSelectedDevice); Console.WriteLine($"Listening on {pcapSelectedDevice.GetDisplayName()}..."); } + private void OnCaptureStopped() + { + PcapBackend.OnCaptureStop -= OnCaptureStopped; + uiDispatcher.Invoke(StopCapture); + } + /// /// Stops packet capture/mapping and resets Pcap/controller objects. /// diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 5734536..1a17eb2 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -153,7 +153,8 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) Logging.Main_WriteException(ex, "Context: Unhandled error during packet handling"); // Stop capture - OnCaptureStop.Invoke(); + StopCapture(); + OnCaptureStop?.Invoke(); return; } From 0d9bba8adf540eda641e9f89dbcdc4872ffe23ad Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 21 Jun 2023 23:47:28 -0600 Subject: [PATCH 153/437] Properly handle the client number in device packets --- .../PacketParsing/Packets/CommandHeader.cs | 4 +- Program/PacketParsing/XboxClient.cs | 209 ++++++++++++++++++ Program/PacketParsing/XboxDevice.cs | 204 ++--------------- Program/RB4InstrumentMapper.csproj | 3 +- 4 files changed, 227 insertions(+), 193 deletions(-) create mode 100644 Program/PacketParsing/XboxClient.cs diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index ceb0ba0..37a995f 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -35,6 +35,7 @@ internal struct CommandHeader { public CommandId CommandId; public CommandFlags Flags; + public int Client; public byte SequenceCount; public int DataLength; @@ -55,7 +56,8 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o header = new CommandHeader() { CommandId = (CommandId)data[0], - Flags = (CommandFlags)data[1], + Flags = (CommandFlags)(data[1] & 0xF0), + Client = data[1] & 0x0F, SequenceCount = data[2], DataLength = dataLength }; diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs new file mode 100644 index 0000000..b13eb03 --- /dev/null +++ b/Program/PacketParsing/XboxClient.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// A logical client on an Xbox device. + /// + internal class XboxClient : IDisposable + { + public ushort VendorId { get; private set; } + public ushort ProductId { get; private set; } + + /// + /// The descriptor of the client. + /// + public XboxDescriptor Descriptor { get; private set; } + + private IDeviceMapper deviceMapper; + + private byte[] chunkBuffer; + private readonly Dictionary previousSequenceIds = new Dictionary(); + + ~XboxClient() + { + Dispose(false); + } + + /// + /// Parses command data from a packet. + /// + internal unsafe void HandleMessage(CommandHeader header, ReadOnlySpan commandData) + { + // Chunked packets + if ((header.Flags & CommandFlags.ChunkPacket) != 0) + { + if (!ProcessPacketChunk(header, ref commandData)) + { + // Chunk is ongoing or there was an error + return; + } + + header.DataLength = commandData.Length; + header.Flags &= ~(CommandFlags.ChunkPacket | CommandFlags.ChunkStart); + } + + // Ensure lengths match + if (header.DataLength != commandData.Length) + { + Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {commandData.Length}"); + return; + } + + // Don't handle the same packet twice + if (!previousSequenceIds.TryGetValue(header.CommandId, out byte previousSequence)) + { + previousSequenceIds.Add(header.CommandId, header.SequenceCount); + } + else if (header.SequenceCount == previousSequence) + { + return; + } + + switch (header.CommandId) + { + case CommandId.Arrival: + HandleArrival(commandData); + break; + + case CommandId.Descriptor: + HandleDescriptor(commandData); + break; + + default: + if (deviceMapper == null) + { + Console.WriteLine("Warning: This device was not encountered during its initial connection! It will use the fallback mapper instead of one specific to its device interface."); + Console.WriteLine("Consider hitting Start before connecting it to ensure correct behavior."); + deviceMapper = MapperFactory.GetFallbackMapper(XboxDevice.MapperMode); + } + + // Hand off unrecognized commands to the mapper + deviceMapper.HandlePacket(header.CommandId, commandData); + break; + } + } + + private unsafe bool ProcessPacketChunk(CommandHeader header, ref ReadOnlySpan chunkData) + { + // Get sequence length/index + if (!ParsingUtils.DecodeLEB128(chunkData, out int bufferIndex, out int bytesRead)) + { + return false; + } + chunkData = chunkData.Slice(bytesRead); + + // Verify packet length + if (header.DataLength != chunkData.Length) + { + Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {chunkData.Length}"); + return false; + } + + // Do nothing with chunks of length 0 + if (bufferIndex <= 0) + { + // Chunked packets with a length of 0 are valid and have been observed with Elite controllers + bool emptySequence = bufferIndex == 0; + Debug.Assert(emptySequence, $"Negative buffer index {bufferIndex}!"); + return emptySequence; + } + + // Start of the chunk sequence + if (chunkBuffer == null || (header.Flags & CommandFlags.ChunkStart) != 0) + { + // Safety check + if ((header.Flags & CommandFlags.ChunkStart) == 0) + { + Debug.Fail("Invalid chunk sequence start! No chunk buffer exists, expected a chunk start packet"); + return false; + } + + // Buffer index is the total size of the buffer on the starting packet + chunkBuffer = new byte[bufferIndex]; + bufferIndex = 0; + } + + // Buffer index equalling buffer length signals the end of the sequence + if (bufferIndex >= chunkBuffer.Length) + { + // Safety checks + if (bufferIndex > chunkBuffer.Length) + { + Debug.Fail("Invalid chunk sequence end! Buffer index is beyond the end of the chunk buffer"); + return false; + } + + if (chunkData.Length != 0) + { + Debug.Fail("Invalid chunk sequence end! Data was provided beyond the end of the buffer"); + return false; + } + + // Send off finished chunk buffer + chunkData = chunkBuffer; + chunkBuffer = null; + return true; + } + + // Verify chunk data bounds + if ((bufferIndex + chunkData.Length) > chunkBuffer.Length) + { + Debug.Fail($"Invalid chunk sequence! Data was provided beyond the end of the buffer"); + return false; + } + + // Copy data to buffer + chunkData.CopyTo(chunkBuffer.AsSpan(bufferIndex, chunkData.Length)); + return false; + } + + /// + /// Handles the arrival message of the device. + /// + private unsafe void HandleArrival(ReadOnlySpan data) + { + if (VendorId != 0 || ProductId != 0) + return; + + if (data.Length < sizeof(DeviceArrival) || MemoryMarshal.TryRead(data, out DeviceArrival arrival)) + return; + + VendorId = arrival.VendorId; + ProductId = arrival.ProductId; + } + + /// + /// Handles the Xbox One descriptor of the device. + /// + private void HandleDescriptor(ReadOnlySpan data) + { + if (Descriptor != null) + return; + + if (!XboxDescriptor.Parse(data, out var descriptor)) + return; + + Descriptor = descriptor; + deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, XboxDevice.MapperMode); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + deviceMapper?.Dispose(); + deviceMapper = null; + } + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 6db8f39..16e9b63 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.InteropServices; namespace RB4InstrumentMapper.Parsing { @@ -12,38 +10,17 @@ public enum MappingMode } /// - /// Interface for Xbox devices. + /// An Xbox device. /// public class XboxDevice : IDisposable { public static MappingMode MapperMode; - public ushort VendorId { get; private set; } - public ushort ProductId { get; private set; } - - /// - /// The descriptor of the device. - /// - public XboxDescriptor Descriptor { get; private set; } - /// - /// Mapper interface to use. + /// The clients currently on the device. /// - private IDeviceMapper deviceMapper; + private readonly Dictionary clients = new Dictionary(); - /// - /// Buffer used to assemble chunked packets. - /// - private byte[] chunkBuffer; - - /// - /// The previous sequence ID for received command IDs. - /// - private readonly Dictionary previousSequenceIds = new Dictionary(); - - /// - /// Performs cleanup on object finalization. - /// ~XboxDevice() { Dispose(false); @@ -85,174 +62,17 @@ public unsafe void HandlePacket(ReadOnlySpan data) var messageData = data.Slice(0, messageLength); var commandData = messageData.Slice(headerLength); // Chunk index is not removed here, as message handling needs it - HandleMessage(header, commandData); - - // Progress to next message - data = data.Slice(messageData.Length); - } - } - - /// - /// Parses command data from a packet. - /// - private unsafe void HandleMessage(CommandHeader header, ReadOnlySpan commandData) - { - // Chunked packets - if ((header.Flags & CommandFlags.ChunkPacket) != 0) - { - if (!ProcessPacketChunk(header, ref commandData)) - { - // Chunk is ongoing or there was an error - return; - } - - header.DataLength = commandData.Length; - header.Flags &= ~(CommandFlags.ChunkPacket | CommandFlags.ChunkStart); - } - - // Ensure lengths match - if (header.DataLength != commandData.Length) - { - Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {commandData.Length}"); - return; - } - - // Don't handle the same packet twice - if (!previousSequenceIds.TryGetValue(header.CommandId, out byte previousSequence)) - { - previousSequenceIds.Add(header.CommandId, header.SequenceCount); - } - else if (header.SequenceCount == previousSequence) - { - return; - } - - switch (header.CommandId) - { - case CommandId.Arrival: - HandleArrival(commandData); - break; - - case CommandId.Descriptor: - HandleDescriptor(commandData); - break; - - default: - if (deviceMapper == null) - { - Console.WriteLine("Warning: This device was not encountered during its initial connection! It will use the fallback mapper instead of one specific to its device interface."); - Console.WriteLine("Consider hitting Start before connecting it to ensure correct behavior."); - deviceMapper = MapperFactory.GetFallbackMapper(MapperMode); - } - - // Hand off unrecognized commands to the mapper - deviceMapper.HandlePacket(header.CommandId, commandData); - break; - } - } - - private unsafe bool ProcessPacketChunk(CommandHeader header, ref ReadOnlySpan chunkData) - { - // Get sequence length/index - if (!ParsingUtils.DecodeLEB128(chunkData, out int bufferIndex, out int bytesRead)) - { - return false; - } - chunkData = chunkData.Slice(bytesRead); - - // Verify packet length - if (header.DataLength != chunkData.Length) - { - Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {chunkData.Length}"); - return false; - } - - // Do nothing with chunks of length 0 - if (bufferIndex <= 0) - { - // Chunked packets with a length of 0 are valid and have been observed with Elite controllers - bool emptySequence = bufferIndex == 0; - Debug.Assert(emptySequence, $"Negative buffer index {bufferIndex}!"); - return emptySequence; - } - // Start of the chunk sequence - if (chunkBuffer == null || (header.Flags & CommandFlags.ChunkStart) != 0) - { - // Safety check - if ((header.Flags & CommandFlags.ChunkStart) == 0) + if (!clients.TryGetValue(header.Client, out var client)) { - Debug.Fail("Invalid chunk sequence start! No chunk buffer exists, expected a chunk start packet"); - return false; + client = new XboxClient(); + clients.Add(header.Client, client); } + client.HandleMessage(header, commandData); - // Buffer index is the total size of the buffer on the starting packet - chunkBuffer = new byte[bufferIndex]; - bufferIndex = 0; - } - - // Buffer index equalling buffer length signals the end of the sequence - if (bufferIndex >= chunkBuffer.Length) - { - // Safety checks - if (bufferIndex > chunkBuffer.Length) - { - Debug.Fail("Invalid chunk sequence end! Buffer index is beyond the end of the chunk buffer"); - return false; - } - - if (chunkData.Length != 0) - { - Debug.Fail("Invalid chunk sequence end! Data was provided beyond the end of the buffer"); - return false; - } - - // Send off finished chunk buffer - chunkData = chunkBuffer; - chunkBuffer = null; - return true; - } - - // Verify chunk data bounds - if ((bufferIndex + chunkData.Length) > chunkBuffer.Length) - { - Debug.Fail($"Invalid chunk sequence! Data was provided beyond the end of the buffer"); - return false; + // Progress to next message + data = data.Slice(messageData.Length); } - - // Copy data to buffer - chunkData.CopyTo(chunkBuffer.AsSpan(bufferIndex, chunkData.Length)); - return false; - } - - /// - /// Handles the arrival message of the device. - /// - private unsafe void HandleArrival(ReadOnlySpan data) - { - if (VendorId != 0 || ProductId != 0) - return; - - if (data.Length < sizeof(DeviceArrival) || MemoryMarshal.TryRead(data, out DeviceArrival arrival)) - return; - - VendorId = arrival.VendorId; - ProductId = arrival.ProductId; - } - - /// - /// Handles the Xbox One descriptor of the device. - /// - private void HandleDescriptor(ReadOnlySpan data) - { - if (Descriptor != null) - return; - - if (!XboxDescriptor.Parse(data, out var descriptor)) - return; - - Descriptor = descriptor; - deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, MapperMode); } /// @@ -268,8 +88,10 @@ private void Dispose(bool disposing) { if (disposing) { - deviceMapper?.Dispose(); - deviceMapper = null; + foreach (var client in clients.Values) + { + client.Dispose(); + } } } } diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 94e9ab6..e01c368 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -1,4 +1,4 @@ - + @@ -130,6 +130,7 @@ + From d75a79db863c52df26e7f0480f87064837f656fc Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 22 Jun 2023 03:15:37 -0600 Subject: [PATCH 154/437] Further rework device limit detection and handling --- Program/PacketParsing/Backends/PcapBackend.cs | 14 +--- .../PacketParsing/DeviceCreationException.cs | 22 ------ .../PacketParsing/Mappers/MapperFactory.cs | 68 ++++++++----------- Program/PacketParsing/XboxClient.cs | 7 +- Program/RB4InstrumentMapper.csproj | 1 - Program/Vigem/VigemClient.cs | 16 ++++- Program/Vjoy/VjoyClient.cs | 2 + 7 files changed, 52 insertions(+), 78 deletions(-) delete mode 100644 Program/PacketParsing/DeviceCreationException.cs diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 1a17eb2..51dd235 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -47,7 +47,6 @@ public static class PcapBackend private static ILiveDevice captureDevice = null; private static readonly Dictionary devices = new Dictionary(); - private static bool canHandleNewDevices = true; public static event Action OnCaptureStop; @@ -122,14 +121,9 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) ulong deviceId = header.DeviceId; if (!devices.TryGetValue(deviceId, out var device)) { - if (!canHandleNewDevices) - { - return; - } - device = new XboxDevice(); devices.Add(deviceId, device); - Console.WriteLine($"Encountered new device with ID {deviceId.ToString("X12")}"); + Console.WriteLine($"Encountered new device with ID {deviceId:X12}"); } try @@ -141,12 +135,6 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) // Don't log thread aborts, just return return; } - catch (DeviceCreationException ex) - { - canHandleNewDevices = false; - Console.WriteLine("Virtual device limit reached, or an error occured when creating one. No more devices will be registered."); - Console.WriteLine($"Exception: {ex.GetFirstLine()}"); - } catch (Exception ex) { Console.WriteLine($"Error while handling packet: {ex.GetFirstLine()}"); diff --git a/Program/PacketParsing/DeviceCreationException.cs b/Program/PacketParsing/DeviceCreationException.cs deleted file mode 100644 index a537686..0000000 --- a/Program/PacketParsing/DeviceCreationException.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Runtime.InteropServices; - -namespace RB4InstrumentMapper.Parsing -{ - public class DeviceCreationException : Exception - { - public DeviceCreationException() : base() - { - } - - public DeviceCreationException(string message) : base(message) - { - } - - public DeviceCreationException(string message, Exception inner) : base(message, inner) - { - } - } -} \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index b7d1f54..3ccfe65 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using RB4InstrumentMapper.Vigem; +using RB4InstrumentMapper.Vjoy; namespace RB4InstrumentMapper.Parsing { @@ -62,66 +64,52 @@ public static IDeviceMapper GetMapper(IReadOnlyList interfaceGuids, Mappin return GetFallbackMapper(mode); } - try - { - return func(mode); - } - catch (Exception ex) - { - throw new DeviceCreationException("Failed to create mapper for device!", ex); - } + return func(mode); } #if DEBUG private static IDeviceMapper GetGamepadMapper(MappingMode mode) - { - Console.WriteLine($"Ganepad found, creating new {mode} mapper..."); - switch (mode) - { - case MappingMode.ViGEmBus: return new GamepadVigemMapper(); - case MappingMode.vJoy: return new GamepadVjoyMapper(); - default: throw new Exception("Unhandled mapping mode!"); - } - } + => GetMapper(mode, $"Created new {mode} gamepad mapper"); #endif private static IDeviceMapper GetGuitarMapper(MappingMode mode) - { - Console.WriteLine($"Guitar found, creating new {mode} mapper..."); - switch (mode) - { - case MappingMode.ViGEmBus: return new GuitarVigemMapper(); - case MappingMode.vJoy: return new GuitarVjoyMapper(); - default: throw new Exception("Unhandled mapping mode!"); - } - } + => GetMapper(mode, $"Created new {mode} guitar mapper"); private static IDeviceMapper GetDrumsMapper(MappingMode mode) - { - Console.WriteLine($"Drumkit found, creating new {mode} mapper..."); - switch (mode) - { - case MappingMode.ViGEmBus: return new DrumsVigemMapper(); - case MappingMode.vJoy: return new DrumsVjoyMapper(); - default: throw new Exception("Unhandled mapping mode!"); - } - } + => GetMapper(mode, $"Created new {mode} drumkit mapper"); public static IDeviceMapper GetFallbackMapper(MappingMode mode) + => GetMapper(mode, $"Created new fallback {mode} mapper"); + + private static IDeviceMapper GetMapper(MappingMode mode, string creationMessage) + where TVigem : class, IDeviceMapper, new() + where TVjoy : class, IDeviceMapper, new() { try { - Console.WriteLine($"Creating new fallback {mode} mapper..."); + IDeviceMapper mapper; switch (mode) { - case MappingMode.ViGEmBus: return new FallbackVigemMapper(); - case MappingMode.vJoy: return new FallbackVjoyMapper(); - default: throw new Exception("Unhandled mapping mode!"); + case MappingMode.ViGEmBus: + mapper = VigemClient.AreDevicesAvailable ? new TVigem() : null; + break; + case MappingMode.vJoy: + mapper = VjoyClient.AreDevicesAvailable ? new TVjoy() : null; + // Check if all devices have been used + if (mapper != null && !VjoyClient.AreDevicesAvailable) + Console.WriteLine("vJoy device limit reached, no new devices will be handled."); + break; + default: throw new NotImplementedException($"Unhandled mapping mode {mode}!"); } + + if (mapper != null) + Console.WriteLine(creationMessage); + return mapper; } catch (Exception ex) { - throw new DeviceCreationException("Failed to create fallback mapper for device!", ex); + Console.WriteLine($"Failed to create mapper for device: {ex.GetFirstLine()}"); + return null; } } } diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index b13eb03..676db1e 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -76,9 +76,14 @@ internal unsafe void HandleMessage(CommandHeader header, ReadOnlySpan comm default: if (deviceMapper == null) { + deviceMapper = MapperFactory.GetFallbackMapper(XboxDevice.MapperMode); + if (deviceMapper == null) + { + return; + } + Console.WriteLine("Warning: This device was not encountered during its initial connection! It will use the fallback mapper instead of one specific to its device interface."); Console.WriteLine("Consider hitting Start before connecting it to ensure correct behavior."); - deviceMapper = MapperFactory.GetFallbackMapper(XboxDevice.MapperMode); } // Hand off unrecognized commands to the mapper diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index e01c368..c2480b8 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -128,7 +128,6 @@ - diff --git a/Program/Vigem/VigemClient.cs b/Program/Vigem/VigemClient.cs index e61f7ce..66996d6 100644 --- a/Program/Vigem/VigemClient.cs +++ b/Program/Vigem/VigemClient.cs @@ -19,6 +19,12 @@ public static class VigemClient /// public static bool Initialized => client != null; + /// + /// Whether or not new devices can be created. + /// + public static bool AreDevicesAvailable => Initialized && canCreateDevices; + private static bool canCreateDevices; + public static bool TryInitialize() { if (client != null) @@ -44,7 +50,15 @@ public static IXbox360Controller CreateDevice() if (!Initialized) throw new ObjectDisposedException(nameof(client), "ViGEmBus client is disposed or not initialized yet!"); - return client.CreateXbox360Controller(0x1BAD, 0x0719); + try + { + return client.CreateXbox360Controller(0x1BAD, 0x0719); + } + catch + { + canCreateDevices = false; + throw; + } } // Rock Band Guitar: USB\VID_1BAD&PID_0719&IG_00 XUSB\TYPE_00\SUB_86\VEN_1BAD\REV_0002 // Rock Band Drums: USB\VID_1BAD&PID_0719&IG_02 XUSB\TYPE_00\SUB_88\VEN_1BAD\REV_0002 diff --git a/Program/Vjoy/VjoyClient.cs b/Program/Vjoy/VjoyClient.cs index 443a435..50362a0 100644 --- a/Program/Vjoy/VjoyClient.cs +++ b/Program/Vjoy/VjoyClient.cs @@ -15,6 +15,8 @@ public static class VjoyClient public static string Product => client.GetvJoyProductString(); public static string SerialNumber => client.GetvJoySerialNumberString(); + public static bool AreDevicesAvailable => Enabled && GetNextAvailableID() > 0; + public static bool DriverMatch(out uint libraryVersion, out uint driverVersion) { libraryVersion = 0; From 9b04bee83f46f2ac2da7a7099c9c68dcca28109e Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 22 Jun 2023 03:16:05 -0600 Subject: [PATCH 155/437] Log packets *before* processing them --- Program/PacketParsing/Backends/PcapBackend.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 51dd235..3ada81f 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -117,6 +117,15 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) return; } + // Debugging (if enabled) + if (LogPackets) + { + string packetLogString = $"{packet.Header.Timeval.Date:yyyy-MM-dd hh:mm:ss.fff} [{packet.Data.Length}] " + + $"{BitConverter.ToString(headerData.ToArray())} | {BitConverter.ToString(packetData.ToArray())}"; + Console.WriteLine(packetLogString); + Logging.Packet_WriteLine(packetLogString); + } + // Check if device ID has been encountered yet ulong deviceId = header.DeviceId; if (!devices.TryGetValue(deviceId, out var device)) @@ -145,15 +154,6 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) OnCaptureStop?.Invoke(); return; } - - // Debugging (if enabled) - if (LogPackets) - { - string packetLogString = $"{packet.Header.Timeval.Date:yyyy-MM-dd hh:mm:ss.fff} [{packet.Data.Length}] " + - $"{BitConverter.ToString(headerData.ToArray())} | {BitConverter.ToString(packetData.ToArray())}"; - Console.WriteLine(packetLogString); - Logging.Packet_WriteLine(packetLogString); - } } } } \ No newline at end of file From 1de282356ac6aa35b3273b406180fabf1c124aa1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 22 Jun 2023 03:16:15 -0600 Subject: [PATCH 156/437] Ignore certain command IDs --- Program/PacketParsing/Packets/CommandHeader.cs | 5 ++++- Program/PacketParsing/XboxClient.cs | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 37a995f..6bb10d5 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -8,10 +8,13 @@ namespace RB4InstrumentMapper.Parsing ///
internal enum CommandId : byte { + Acknowledgement = 0x01, Arrival = 0x02, Descriptor = 0x04, + Authentication = 0x06, Keystroke = 0x07, - Input = 0x20 + SerialNumber = 0x1E, + Input = 0x20, } /// diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 676db1e..64dfd4a 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -65,6 +65,12 @@ internal unsafe void HandleMessage(CommandHeader header, ReadOnlySpan comm switch (header.CommandId) { + // Commands to ignore + case CommandId.Acknowledgement: + case CommandId.Authentication: + case CommandId.SerialNumber: + break; + case CommandId.Arrival: HandleArrival(commandData); break; From 874aebce4dbcc31693279cc23cdc63ed1715da2b Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 22 Jun 2023 03:16:26 -0600 Subject: [PATCH 157/437] Add XboxResult enum for better result handling/details --- Program/PacketParsing/Backends/PcapBackend.cs | 13 +++- .../PacketParsing/Mappers/DrumsVigemMapper.cs | 12 ++-- .../PacketParsing/Mappers/DrumsVjoyMapper.cs | 12 ++-- .../Mappers/FallbackVigemMapper.cs | 12 ++-- .../Mappers/FallbackVjoyMapper.cs | 12 ++-- .../Mappers/GamepadVigemMapper.cs | 12 ++-- .../Mappers/GamepadVjoyMapper.cs | 12 ++-- .../Mappers/GuitarVigemMapper.cs | 12 ++-- .../PacketParsing/Mappers/GuitarVjoyMapper.cs | 12 ++-- .../PacketParsing/Mappers/IDeviceMapper.cs | 2 +- Program/PacketParsing/Mappers/VigemMapper.cs | 23 ++++--- Program/PacketParsing/Mappers/VjoyMapper.cs | 21 +++--- Program/PacketParsing/XboxClient.cs | 64 ++++++++++--------- Program/PacketParsing/XboxDevice.cs | 30 +++++++-- 14 files changed, 146 insertions(+), 103 deletions(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 3ada81f..83eb6d8 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -137,7 +137,18 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) try { - device.HandlePacket(packetData); + var result = device.HandlePacket(packetData); + switch (result) + { + case XboxResult.InvalidMessage: + if (LogPackets) + { + string invalidMessage = $"Invalid packet received!"; + Console.WriteLine(invalidMessage); + Logging.Packet_WriteLine(invalidMessage); + } + break; + } } catch (ThreadAbortException) { diff --git a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs index f125449..65344bc 100644 --- a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs +++ b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs @@ -17,16 +17,15 @@ public DrumsVigemMapper() : base() /// /// Handles an incoming packet. /// - protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) { switch (command) { case CommandId.Input: - ParseInput(data); - break; + return ParseInput(data); default: - break; + return XboxResult.Success; } } @@ -38,15 +37,16 @@ protected override void OnPacketReceived(CommandId command, ReadOnlySpan d /// /// Parses an input report. /// - private unsafe void ParseInput(ReadOnlySpan data) + private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length != sizeof(DrumInput) || !MemoryMarshal.TryRead(data, out DrumInput drumReport)) - return; + return XboxResult.InvalidMessage; HandleReport(device, drumReport, ref previousDpadCymbals, ref dpadMask); // Send data device.SubmitReport(); + return XboxResult.Success; } /// diff --git a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs b/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs index f549584..506f57e 100644 --- a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs @@ -17,31 +17,31 @@ public DrumsVjoyMapper() : base() /// /// Handles an incoming packet. /// - protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) { switch (command) { case CommandId.Input: - ParseInput(data); - break; + return ParseInput(data); default: - break; + return XboxResult.Success; } } /// /// Parses an input report. /// - public unsafe void ParseInput(ReadOnlySpan data) + public unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length != sizeof(DrumInput) || !MemoryMarshal.TryRead(data, out DrumInput guitarReport)) - return; + return XboxResult.InvalidMessage; HandleReport(ref state, guitarReport); // Send data VjoyClient.UpdateDevice(deviceId, ref state); + return XboxResult.Success; } /// diff --git a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs index c97282d..e561833 100644 --- a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs @@ -15,16 +15,15 @@ public FallbackVigemMapper() : base() /// /// Handles an incoming packet. /// - protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) { switch (command) { case CommandId.Input: - ParseInput(data); - break; + return ParseInput(data); default: - break; + return XboxResult.Success; } } @@ -36,7 +35,7 @@ protected override void OnPacketReceived(CommandId command, ReadOnlySpan d /// /// Parses an input report. /// - private unsafe void ParseInput(ReadOnlySpan data) + private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) { @@ -55,11 +54,12 @@ private unsafe void ParseInput(ReadOnlySpan data) else { // Not handled - return; + return XboxResult.Success; } // Send data device.SubmitReport(); + return XboxResult.Success; } } } diff --git a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs index 6a19062..7851a0d 100644 --- a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs @@ -16,23 +16,22 @@ public FallbackVjoyMapper() : base() /// /// Handles an incoming packet. /// - protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) { switch (command) { case CommandId.Input: - ParseInput(data); - break; + return ParseInput(data); default: - break; + return XboxResult.Success; } } /// /// Parses an input report. /// - public unsafe void ParseInput(ReadOnlySpan data) + public unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) { @@ -51,11 +50,12 @@ public unsafe void ParseInput(ReadOnlySpan data) else { // Not handled - return; + return XboxResult.Success; } // Send data VjoyClient.UpdateDevice(deviceId, ref state); + return XboxResult.Success; } } } diff --git a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs b/Program/PacketParsing/Mappers/GamepadVigemMapper.cs index 8b3a15c..d37610b 100644 --- a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs +++ b/Program/PacketParsing/Mappers/GamepadVigemMapper.cs @@ -19,31 +19,31 @@ public GamepadVigemMapper() : base() /// /// Handles an incoming packet. /// - protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) { switch (command) { case CommandId.Input: - ParseInput(data); - break; + return ParseInput(data); default: - break; + return XboxResult.Success; } } /// /// Parses an input report. /// - private unsafe void ParseInput(ReadOnlySpan data) + private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length < sizeof(GamepadInput) || !MemoryMarshal.TryRead(data, out GamepadInput gamepadReport)) - return; + return XboxResult.InvalidMessage; HandleReport(device, gamepadReport); // Send data device.SubmitReport(); + return XboxResult.Success; } /// diff --git a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs b/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs index c67e3c5..d580372 100644 --- a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs @@ -19,31 +19,31 @@ public GamepadVjoyMapper() : base() /// /// Handles an incoming packet. /// - protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) { switch (command) { case CommandId.Input: - ParseInput(data); - break; + return ParseInput(data); default: - break; + return XboxResult.Success; } } /// /// Parses an input report. /// - public unsafe void ParseInput(ReadOnlySpan data) + public unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length < sizeof(GamepadInput) || !MemoryMarshal.TryRead(data, out GamepadInput gamepadReport)) - return; + return XboxResult.InvalidMessage; HandleReport(ref state, gamepadReport); // Send data VjoyClient.UpdateDevice(deviceId, ref state); + return XboxResult.Success; } /// diff --git a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs b/Program/PacketParsing/Mappers/GuitarVigemMapper.cs index 05bc3dd..42df711 100644 --- a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs +++ b/Program/PacketParsing/Mappers/GuitarVigemMapper.cs @@ -17,31 +17,31 @@ public GuitarVigemMapper() : base() /// /// Handles an incoming packet. /// - protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) { switch (command) { case CommandId.Input: - ParseInput(data); - break; + return ParseInput(data); default: - break; + return XboxResult.Success; } } /// /// Parses an input report. /// - private unsafe void ParseInput(ReadOnlySpan data) + private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length != sizeof(GuitarInput) || !MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) - return; + return XboxResult.InvalidMessage; HandleReport(device, guitarReport); // Send data device.SubmitReport(); + return XboxResult.Success; } /// diff --git a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs index 7835ffc..2dec946 100644 --- a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs @@ -17,31 +17,31 @@ public GuitarVjoyMapper() : base() /// /// Handles an incoming packet. /// - protected override void OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) { switch (command) { case CommandId.Input: - ParseInput(data); - break; + return ParseInput(data); default: - break; + return XboxResult.Success; } } /// /// Parses an input report. /// - public unsafe void ParseInput(ReadOnlySpan data) + public unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length != sizeof(GuitarInput) || !MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) - return; + return XboxResult.InvalidMessage; HandleReport(ref state, guitarReport); // Send data VjoyClient.UpdateDevice(deviceId, ref state); + return XboxResult.Success; } /// diff --git a/Program/PacketParsing/Mappers/IDeviceMapper.cs b/Program/PacketParsing/Mappers/IDeviceMapper.cs index 37c5930..401da9e 100644 --- a/Program/PacketParsing/Mappers/IDeviceMapper.cs +++ b/Program/PacketParsing/Mappers/IDeviceMapper.cs @@ -12,6 +12,6 @@ internal interface IDeviceMapper : IDisposable /// /// Handles an incoming packet. /// - void HandlePacket(CommandId command, ReadOnlySpan data); + XboxResult HandlePacket(CommandId command, ReadOnlySpan data); } } \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/VigemMapper.cs b/Program/PacketParsing/Mappers/VigemMapper.cs index abe3606..46c0a3c 100644 --- a/Program/PacketParsing/Mappers/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/VigemMapper.cs @@ -57,32 +57,33 @@ private void DeviceConnected(object sender, Xbox360FeedbackReceivedEventArgs arg /// /// Handles an incoming packet. /// - public void HandlePacket(CommandId command, ReadOnlySpan data) + public XboxResult HandlePacket(CommandId command, ReadOnlySpan data) { if (device == null) throw new ObjectDisposedException(nameof(device)); if (!deviceConnected) - return; + return XboxResult.Pending; switch (command) { case CommandId.Keystroke: - HandleKeystroke(data); - break; + return HandleKeystroke(data); default: - OnPacketReceived(command, data); - break; + return OnPacketReceived(command, data); } } - protected abstract void OnPacketReceived(CommandId command, ReadOnlySpan data); + protected abstract XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data); - private unsafe void HandleKeystroke(ReadOnlySpan data) + private unsafe XboxResult HandleKeystroke(ReadOnlySpan data) { - if (!MapGuideButton || data.Length < sizeof(Keystroke)) - return; + if (!MapGuideButton) + return XboxResult.Success; + + if (data.Length < sizeof(Keystroke)) + return XboxResult.InvalidMessage; // Multiple keystrokes can be sent in a single message var keys = MemoryMarshal.Cast(data); @@ -94,6 +95,8 @@ private unsafe void HandleKeystroke(ReadOnlySpan data) device.SubmitReport(); } } + + return XboxResult.Success; } /// diff --git a/Program/PacketParsing/Mappers/VjoyMapper.cs b/Program/PacketParsing/Mappers/VjoyMapper.cs index 17c814c..65afc0a 100644 --- a/Program/PacketParsing/Mappers/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/VjoyMapper.cs @@ -43,7 +43,7 @@ public VjoyMapper() /// /// Handles an incoming packet. /// - public void HandlePacket(CommandId command, ReadOnlySpan data) + public XboxResult HandlePacket(CommandId command, ReadOnlySpan data) { if (deviceId == 0) throw new ObjectDisposedException("this"); @@ -51,21 +51,22 @@ public void HandlePacket(CommandId command, ReadOnlySpan data) switch (command) { case CommandId.Keystroke: - HandleKeystroke(data); - break; + return HandleKeystroke(data); default: - OnPacketReceived(command, data); - break; + return OnPacketReceived(command, data); } } - protected abstract void OnPacketReceived(CommandId command, ReadOnlySpan data); + protected abstract XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data); - private unsafe void HandleKeystroke(ReadOnlySpan data) + private unsafe XboxResult HandleKeystroke(ReadOnlySpan data) { - if (!MapGuideButton || data.Length < sizeof(Keystroke)) - return; + if (!MapGuideButton) + return XboxResult.Success; + + if (data.Length < sizeof(Keystroke)) + return XboxResult.InvalidMessage; // Multiple keystrokes can be sent in a single message var keys = MemoryMarshal.Cast(data); @@ -77,6 +78,8 @@ private unsafe void HandleKeystroke(ReadOnlySpan data) VjoyClient.UpdateDevice(deviceId, ref state); } } + + return XboxResult.Success; } // vJoy axes range from 0x0000 to 0x8000, but are exposed as full ints for some reason diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 64dfd4a..e33f40b 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -31,15 +31,19 @@ internal class XboxClient : IDisposable /// /// Parses command data from a packet. /// - internal unsafe void HandleMessage(CommandHeader header, ReadOnlySpan commandData) + internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan commandData) { // Chunked packets if ((header.Flags & CommandFlags.ChunkPacket) != 0) { - if (!ProcessPacketChunk(header, ref commandData)) + var chunkResult = ProcessPacketChunk(header, ref commandData); + switch (chunkResult) { - // Chunk is ongoing or there was an error - return; + case XboxResult.Success: + break; + case XboxResult.Pending: // Chunk is unfinished + default: // Error handling the chunk + return chunkResult; } header.DataLength = commandData.Length; @@ -50,7 +54,7 @@ internal unsafe void HandleMessage(CommandHeader header, ReadOnlySpan comm if (header.DataLength != commandData.Length) { Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {commandData.Length}"); - return; + return XboxResult.InvalidMessage; } // Don't handle the same packet twice @@ -60,7 +64,7 @@ internal unsafe void HandleMessage(CommandHeader header, ReadOnlySpan comm } else if (header.SequenceCount == previousSequence) { - return; + return XboxResult.Success; } switch (header.CommandId) @@ -72,12 +76,10 @@ internal unsafe void HandleMessage(CommandHeader header, ReadOnlySpan comm break; case CommandId.Arrival: - HandleArrival(commandData); - break; + return HandleArrival(commandData); case CommandId.Descriptor: - HandleDescriptor(commandData); - break; + return HandleDescriptor(commandData); default: if (deviceMapper == null) @@ -85,7 +87,8 @@ internal unsafe void HandleMessage(CommandHeader header, ReadOnlySpan comm deviceMapper = MapperFactory.GetFallbackMapper(XboxDevice.MapperMode); if (deviceMapper == null) { - return; + // No more devices available, do nothing + return XboxResult.Success; } Console.WriteLine("Warning: This device was not encountered during its initial connection! It will use the fallback mapper instead of one specific to its device interface."); @@ -93,17 +96,18 @@ internal unsafe void HandleMessage(CommandHeader header, ReadOnlySpan comm } // Hand off unrecognized commands to the mapper - deviceMapper.HandlePacket(header.CommandId, commandData); - break; + return deviceMapper.HandlePacket(header.CommandId, commandData); } + + return XboxResult.Success; } - private unsafe bool ProcessPacketChunk(CommandHeader header, ref ReadOnlySpan chunkData) + private unsafe XboxResult ProcessPacketChunk(CommandHeader header, ref ReadOnlySpan chunkData) { // Get sequence length/index if (!ParsingUtils.DecodeLEB128(chunkData, out int bufferIndex, out int bytesRead)) { - return false; + return XboxResult.InvalidMessage; } chunkData = chunkData.Slice(bytesRead); @@ -111,7 +115,7 @@ private unsafe bool ProcessPacketChunk(CommandHeader header, ref ReadOnlySpan chunkBuffer.Length) { Debug.Fail("Invalid chunk sequence end! Buffer index is beyond the end of the chunk buffer"); - return false; + return XboxResult.InvalidMessage; } if (chunkData.Length != 0) { Debug.Fail("Invalid chunk sequence end! Data was provided beyond the end of the buffer"); - return false; + return XboxResult.InvalidMessage; } // Send off finished chunk buffer chunkData = chunkBuffer; chunkBuffer = null; - return true; + return XboxResult.Success; } // Verify chunk data bounds if ((bufferIndex + chunkData.Length) > chunkBuffer.Length) { Debug.Fail($"Invalid chunk sequence! Data was provided beyond the end of the buffer"); - return false; + return XboxResult.InvalidMessage; } // Copy data to buffer chunkData.CopyTo(chunkBuffer.AsSpan(bufferIndex, chunkData.Length)); - return false; + return XboxResult.Pending; } /// /// Handles the arrival message of the device. /// - private unsafe void HandleArrival(ReadOnlySpan data) + private unsafe XboxResult HandleArrival(ReadOnlySpan data) { if (VendorId != 0 || ProductId != 0) - return; + return XboxResult.Success; if (data.Length < sizeof(DeviceArrival) || MemoryMarshal.TryRead(data, out DeviceArrival arrival)) - return; + return XboxResult.InvalidMessage; VendorId = arrival.VendorId; ProductId = arrival.ProductId; + return XboxResult.Success; } /// /// Handles the Xbox One descriptor of the device. /// - private void HandleDescriptor(ReadOnlySpan data) + private XboxResult HandleDescriptor(ReadOnlySpan data) { if (Descriptor != null) - return; + return XboxResult.Success; if (!XboxDescriptor.Parse(data, out var descriptor)) - return; + return XboxResult.InvalidMessage; Descriptor = descriptor; deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, XboxDevice.MapperMode); + return XboxResult.Success; } public void Dispose() diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 16e9b63..263af01 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -9,6 +9,14 @@ public enum MappingMode vJoy = 2 } + public enum XboxResult + { + Success, + Pending, + Disconnected, + InvalidMessage, + } + /// /// An Xbox device. /// @@ -29,7 +37,7 @@ public class XboxDevice : IDisposable /// /// Handles an incoming packet for this device. /// - public unsafe void HandlePacket(ReadOnlySpan data) + public unsafe XboxResult HandlePacket(ReadOnlySpan data) { // Some devices may send multiple messages in a single packet, placing them back-to-back // The header length is very important in these scenarios, as it determines which bytes are part of the message @@ -39,7 +47,7 @@ public unsafe void HandlePacket(ReadOnlySpan data) // Command header if (!CommandHeader.TryParse(data, out var header, out int headerLength)) { - return; + return XboxResult.InvalidMessage; } int messageLength = headerLength + header.DataLength; @@ -48,7 +56,7 @@ public unsafe void HandlePacket(ReadOnlySpan data) { if (!ParsingUtils.DecodeLEB128(data.Slice(headerLength), out int _, out int indexLength)) { - return; + return XboxResult.InvalidMessage; } messageLength += indexLength; @@ -57,7 +65,7 @@ public unsafe void HandlePacket(ReadOnlySpan data) // Verify bounds if (data.Length < messageLength) { - return; + return XboxResult.InvalidMessage; } var messageData = data.Slice(0, messageLength); @@ -68,11 +76,23 @@ public unsafe void HandlePacket(ReadOnlySpan data) client = new XboxClient(); clients.Add(header.Client, client); } - client.HandleMessage(header, commandData); + var clientResult = client.HandleMessage(header, commandData); + switch (clientResult) + { + case XboxResult.Success: + case XboxResult.Pending: + break; + default: + if (data.Length < 1) + return clientResult; + break; + } // Progress to next message data = data.Slice(messageData.Length); } + + return XboxResult.Success; } /// From 00565f903f1058ace4533ea18222e3c04db64e3c Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 22 Jun 2023 03:31:25 -0600 Subject: [PATCH 158/437] Implement handling of device disconnections --- Program/PacketParsing/Backends/PcapBackend.cs | 7 +++- .../PacketParsing/Packets/CommandHeader.cs | 1 + Program/PacketParsing/Packets/DeviceStatus.cs | 34 +++++++++++++++++++ Program/PacketParsing/XboxClient.cs | 19 ++++++++++- Program/PacketParsing/XboxDevice.cs | 2 ++ Program/RB4InstrumentMapper.csproj | 1 + 6 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 Program/PacketParsing/Packets/DeviceStatus.cs diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 83eb6d8..6c55db3 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -132,7 +132,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) { device = new XboxDevice(); devices.Add(deviceId, device); - Console.WriteLine($"Encountered new device with ID {deviceId:X12}"); + Console.WriteLine($"Device with ID {deviceId:X12} was connected"); } try @@ -140,6 +140,11 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) var result = device.HandlePacket(packetData); switch (result) { + case XboxResult.Disconnected: + device.Dispose(); + devices.Remove(deviceId); + Console.WriteLine($"Device with ID {deviceId:X12} was disconnected"); + break; case XboxResult.InvalidMessage: if (LogPackets) { diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 6bb10d5..7f80af4 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -10,6 +10,7 @@ internal enum CommandId : byte { Acknowledgement = 0x01, Arrival = 0x02, + Status = 0x03, Descriptor = 0x04, Authentication = 0x06, Keystroke = 0x07, diff --git a/Program/PacketParsing/Packets/DeviceStatus.cs b/Program/PacketParsing/Packets/DeviceStatus.cs new file mode 100644 index 0000000..e41c172 --- /dev/null +++ b/Program/PacketParsing/Packets/DeviceStatus.cs @@ -0,0 +1,34 @@ +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + internal enum BatteryType : byte + { + Wired = 0, + Standard = 1, + ChargeKit = 2, + } + + internal enum BatteryLevel : byte + { + Low = 0, + Medium = 1, + High = 2, + Full = 3, + + Wired = Low, + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct DeviceStatus + { + private byte status; + private byte unk1; + private byte unk2; + private byte unk3; + + public bool Connected => (status & 0b1100_0000) != 0; + public BatteryType BatteryType => (BatteryType)(status & 0b0000_1100); + public BatteryLevel BatteryLevel => (BatteryLevel)(status & 0b0000_0011); + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index e33f40b..ef0a21b 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -78,6 +78,9 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan data) return XboxResult.Success; } + /// + /// Handles the arrival message of the device. + /// + private unsafe XboxResult HandleStatus(ReadOnlySpan data) + { + if (data.Length < sizeof(DeviceStatus) || !MemoryMarshal.TryRead(data, out DeviceStatus status)) + return XboxResult.InvalidMessage; + + if (!status.Connected) + return XboxResult.Disconnected; + + return XboxResult.Success; + } + /// /// Handles the Xbox One descriptor of the device. /// diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 263af01..e6a7cf1 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -82,6 +82,8 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) case XboxResult.Success: case XboxResult.Pending: break; + case XboxResult.Disconnected: + return clientResult; default: if (data.Length < 1) return clientResult; diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index c2480b8..3dee9b3 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -122,6 +122,7 @@ + From f62f5d151f99271998fe7b1118b5c88a8441a110 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 22 Jun 2023 03:41:29 -0600 Subject: [PATCH 159/437] Remove handling of device arrival message --- .../PacketParsing/Packets/DeviceArrival.cs | 14 ----------- Program/PacketParsing/XboxClient.cs | 23 +------------------ Program/RB4InstrumentMapper.csproj | 1 - 3 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 Program/PacketParsing/Packets/DeviceArrival.cs diff --git a/Program/PacketParsing/Packets/DeviceArrival.cs b/Program/PacketParsing/Packets/DeviceArrival.cs deleted file mode 100644 index 8f60c1f..0000000 --- a/Program/PacketParsing/Packets/DeviceArrival.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Runtime.InteropServices; - -namespace RB4InstrumentMapper.Parsing -{ - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct DeviceArrival - { - public ulong SerialNumber; - public ushort VendorId; - public ushort ProductId; - private ulong ignored1; - private ulong ignored2; - } -} \ No newline at end of file diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index ef0a21b..dc0ea5a 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -10,9 +10,6 @@ namespace RB4InstrumentMapper.Parsing /// internal class XboxClient : IDisposable { - public ushort VendorId { get; private set; } - public ushort ProductId { get; private set; } - /// /// The descriptor of the client. /// @@ -71,13 +68,11 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan - /// Handles the arrival message of the device. - /// - private unsafe XboxResult HandleArrival(ReadOnlySpan data) - { - if (VendorId != 0 || ProductId != 0) - return XboxResult.Success; - - if (data.Length < sizeof(DeviceArrival) || MemoryMarshal.TryRead(data, out DeviceArrival arrival)) - return XboxResult.InvalidMessage; - - VendorId = arrival.VendorId; - ProductId = arrival.ProductId; - return XboxResult.Success; - } - /// /// Handles the arrival message of the device. /// diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 3dee9b3..0d7b9d9 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -121,7 +121,6 @@ - From d691cdce161e0c58f90d5ab2b128a64e98a825ec Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 22 Jun 2023 04:38:07 -0600 Subject: [PATCH 160/437] Remove no-longer-used ParsingHelpers class --- Program/MainWindow/ParsingHelpers.cs | 79 ---------------------------- Program/RB4InstrumentMapper.csproj | 1 - 2 files changed, 80 deletions(-) delete mode 100644 Program/MainWindow/ParsingHelpers.cs diff --git a/Program/MainWindow/ParsingHelpers.cs b/Program/MainWindow/ParsingHelpers.cs deleted file mode 100644 index 002224c..0000000 --- a/Program/MainWindow/ParsingHelpers.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace RB4InstrumentMapper -{ - public static class ParsingHelpers - { - /// - /// Converts a hex string representing a 32-bit integer into a byte array. - /// - /// The string to be converted. - /// A byte array converted from the hex string, or null if the conversion failed. - public static byte[] Int32HexStringToByteArray(string hexString) - { - byte[] byteArray = null; - uint number; - if (uint.TryParse(hexString, NumberStyles.AllowHexSpecifier, NumberFormatInfo.CurrentInfo, out number)) - { - byteArray = BitConverter.GetBytes(number); - } - - return byteArray; - } - - /// - /// Converts a byte array into a hex string. - /// - /// The byte array to converted. - /// A hex string representing the byte array, or null if input is null or empty. - public static string ByteArrayToHexString(byte[] byteArray) - { - string hexString = null; - if (byteArray != null && byteArray.Length > 0) - { - hexString = BitConverter.ToString(byteArray).Replace("-", string.Empty); - } - - return hexString; - } - - /// - /// Converts a string representing a 64-bit hexadecimal number into a 64-bit unsigned integer. - /// - /// The string to be converted. - /// The converted number. - /// True if the conversion was successful, or false if it failed. - public static bool HexStringToUInt64(string hexString, out ulong number) - { - if (hexString.StartsWith("0x") || hexString.StartsWith("&h")) - { - hexString = hexString.Remove(0, 2); - } - - return ulong.TryParse(hexString, NumberStyles.HexNumber, NumberFormatInfo.CurrentInfo, out number); - } - - /// - /// Converts a 32-bit unsigned integer into a string representing a 32-bit hexadecimal number. - /// - /// The number to be converted. - /// A flag indicating if this is an instrument ID. - /// A string representing the input number, or String.Empty if the input is 0 and isID is set. - public static string UInt32ToHexString(uint number, bool isID) - { - if (isID) - { - return number == 0 ? String.Empty : Convert.ToString(number, 16); - } - else - { - return Convert.ToString(number, 16); - } - } - } -} diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 0d7b9d9..8a0871c 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -131,7 +131,6 @@ - From 92b9aaef0a92de17987e650f76d0e655f521ab9f Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 22 Jun 2023 04:38:28 -0600 Subject: [PATCH 161/437] Fix device-still-present check when starting capture --- Program/MainWindow/MainWindow.xaml.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 7dac76f..f4739f2 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -311,8 +311,9 @@ private void StartCapture() } // Check if the device is still present + pcapDeviceList.Refresh(); bool deviceStillPresent = false; - foreach (var device in CaptureDeviceList.Instance) + foreach (var device in pcapDeviceList) { if (device.Name == pcapSelectedDevice.Name) { From 5d803c37ea52bc8c926f03495d2d51d62bcd41e8 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sat, 24 Jun 2023 06:00:24 -0600 Subject: [PATCH 162/437] A bunch of general cleanup --- Program/{LogUtils.cs => Logging.cs} | 1 - .../MainWindow/FixedSizeConcurrentQueue.cs | 33 +-- Program/MainWindow/MainWindow.xaml.cs | 188 ++++++------------ Program/MainWindow/PcapDeviceExtensions.cs | 8 +- .../{TextBoxConsole.cs => TextBoxWriter.cs} | 38 ++-- Program/PacketParsing/Backends/PcapBackend.cs | 33 +-- Program/PacketParsing/DeviceGuids.cs | 2 +- .../PacketParsing/Mappers/DrumsVigemMapper.cs | 16 +- .../Mappers/FallbackVigemMapper.cs | 4 +- .../Mappers/GamepadVigemMapper.cs | 2 +- .../PacketParsing/Mappers/IDeviceMapper.cs | 3 + .../PacketParsing/Mappers/MapperFactory.cs | 2 +- Program/PacketParsing/Mappers/VigemMapper.cs | 9 +- Program/PacketParsing/Packets/DeviceStatus.cs | 10 +- Program/PacketParsing/Packets/DrumInput.cs | 14 +- Program/PacketParsing/Packets/GuitarInput.cs | 6 +- Program/PacketParsing/ParsingUtils.cs | 31 --- Program/Properties/AssemblyInfo.cs | 2 - Program/RB4InstrumentMapper.csproj | 6 +- Program/Vjoy/VjoyClient.cs | 42 +++- 20 files changed, 186 insertions(+), 264 deletions(-) rename Program/{LogUtils.cs => Logging.cs} (99%) rename Program/MainWindow/{TextBoxConsole.cs => TextBoxWriter.cs} (71%) diff --git a/Program/LogUtils.cs b/Program/Logging.cs similarity index 99% rename from Program/LogUtils.cs rename to Program/Logging.cs index 20355ab..d040e6a 100644 --- a/Program/LogUtils.cs +++ b/Program/Logging.cs @@ -2,7 +2,6 @@ using System.Diagnostics; using System.Globalization; using System.IO; -using System.Text; namespace RB4InstrumentMapper { diff --git a/Program/MainWindow/FixedSizeConcurrentQueue.cs b/Program/MainWindow/FixedSizeConcurrentQueue.cs index 72d5f5f..df6a250 100644 --- a/Program/MainWindow/FixedSizeConcurrentQueue.cs +++ b/Program/MainWindow/FixedSizeConcurrentQueue.cs @@ -1,50 +1,39 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Concurrent; namespace RB4InstrumentMapper { /// - /// Implementation of a fixed-size concurrent queue. + /// A concurrent queue with a size limit. /// - /// public class FixedSizeConcurrentQueue : ConcurrentQueue { - /// - /// Sync root of the queue for locking. - /// - private readonly object privateLockObject = new object(); + private readonly object queueLock = new object(); /// /// Size of the queue. /// - public int Size { get; private set; } + public int MaxSize { get; private set; } /// - /// Create a new instance of the FixedSizeConcurrentQueue. + /// Creates a new queue with the given max size. /// - /// Size of the queue. + public FixedSizeConcurrentQueue(int size) { - Size = size; + MaxSize = size; } /// - /// Enqueue an object into the queue. Maintains size limit of queue. + /// Enqueues an object into the queue, discarding any items at the end of the queue which exceed the max count. /// - /// Object to enqueue public new void Enqueue(T obj) { base.Enqueue(obj); - lock (privateLockObject) + lock (queueLock) { - while (base.Count > Size) + while (Count > MaxSize) { - T outObj; - base.TryDequeue(out outObj); + TryDequeue(out _); } } } diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index f4739f2..2708d08 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -1,33 +1,20 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; +using System.Reflection; using System.Text; -using System.Threading; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; -using System.Runtime.InteropServices; using System.Windows.Threading; -using SharpPcap; -using SharpPcap.LibPcap; using RB4InstrumentMapper.Parsing; -using RB4InstrumentMapper.Vjoy; +using RB4InstrumentMapper.Properties; using RB4InstrumentMapper.Vigem; +using RB4InstrumentMapper.Vjoy; +using SharpPcap; namespace RB4InstrumentMapper { - /// - /// Interaction logic for MainWindow.xaml - /// public partial class MainWindow : Window { /// @@ -61,10 +48,13 @@ public partial class MainWindow : Window private bool packetDebugLog = false; /// - /// Common name for Pcap combo box items. + /// Prefix for Pcap combo box items. /// - private const string pcapComboBoxItemName = "pcapDeviceComboBoxItem"; + private const string pcapComboBoxPrefix = "pcapDeviceComboBoxItem"; + /// + /// Available controller emulation types. + /// private enum ControllerType { None = -1, @@ -72,9 +62,6 @@ private enum ControllerType VigemBus = 1 } - /// - /// Initializes a new MainWindow. - /// public MainWindow() { // Assign event handler for unhandled exceptions @@ -82,30 +69,28 @@ public MainWindow() InitializeComponent(); - Version version = System.Reflection.Assembly.GetEntryAssembly().GetName().Version; - versionLabel.Content = $"v{version.ToString()}"; + var version = Assembly.GetEntryAssembly().GetName().Version; + versionLabel.Content = $"v{version}"; #if DEBUG versionLabel.Content += " Debug"; #endif // Capture Dispatcher object for use in callback - uiDispatcher = this.Dispatcher; + uiDispatcher = Dispatcher; } /// /// Called when the window loads. /// - /// - /// private void Window_Loaded(object sender, RoutedEventArgs e) { // Connect to console - TextBoxConsole.RedirectConsoleToTextBox(messageConsole, displayLinesWithTimestamp: false); + TextBoxWriter.RedirectConsoleToTextBox(messageConsole, displayLinesWithTimestamp: false); // Load saved settings - packetDebugCheckBox.IsChecked = Properties.Settings.Default.packetDebug; - packetLogCheckBox.IsChecked = Properties.Settings.Default.packetDebugLog; - int deviceType = controllerDeviceTypeCombo.SelectedIndex = Properties.Settings.Default.controllerDeviceType; + packetDebugCheckBox.IsChecked = Settings.Default.packetDebug; + packetLogCheckBox.IsChecked = Settings.Default.packetDebugLog; + int deviceType = controllerDeviceTypeCombo.SelectedIndex = Settings.Default.controllerDeviceType; // Check for vJoy bool vjoyFound = VjoyClient.Enabled; @@ -120,7 +105,7 @@ private void Window_Loaded(object sender, RoutedEventArgs e) Console.WriteLine($"WARNING: vJoy library version (0x{libraryVersion:X8}) does not match driver version (0x{driverVersion:X8})! vJoy mode may cause errors!"); } - if (CountAvailableVjoyDevices() > 0) + if (VjoyClient.GetAvailableDeviceCount() > 0) { (controllerDeviceTypeCombo.Items[0] as ComboBoxItem).IsEnabled = true; } @@ -178,8 +163,6 @@ private void Window_Loaded(object sender, RoutedEventArgs e) /// /// Called when the window has closed. /// - /// - /// private void Window_Closed(object sender, EventArgs e) { // Shutdown @@ -215,16 +198,16 @@ private void PopulatePcapDropdown() } // Load saved device name - string currentPcapSelection = Properties.Settings.Default.pcapDevice; + string currentPcapSelection = Settings.Default.pcapDevice; // Populate combo and print the list for (int i = 0; i < pcapDeviceList.Count; i++) { - ILiveDevice device = pcapDeviceList[i]; + var device = pcapDeviceList[i]; string itemNumber = $"{i + 1}"; string deviceName = $"{itemNumber}. {device.GetDisplayName()}"; - string itemName = pcapComboBoxItemName + itemNumber; + string itemName = pcapComboBoxPrefix + itemNumber; bool isSelected = device.Name.Equals(currentPcapSelection) || device.Name.Equals(pcapSelectedDevice?.Name); if (isSelected || (string.IsNullOrEmpty(currentPcapSelection) && device.IsXboxOneReceiver())) @@ -251,51 +234,11 @@ private void PopulatePcapDropdown() Console.WriteLine($"Discovered {pcapDeviceList.Count} Pcap devices."); } - /// - /// Populates the controller device list as vJoy. - /// - private int CountAvailableVjoyDevices() - { - if (!VjoyClient.Enabled) - { - return 0; - } - - // Loop through vJoy IDs and populate list - int freeDeviceCount = 0; - for (uint id = 1; id <= 16; id++) - { - if (VjoyClient.IsDeviceAvailable(id)) - { - freeDeviceCount++; - } - } - - switch (freeDeviceCount) - { - case 0: - Console.WriteLine($"No vJoy devices available! Please configure some in the Configure vJoy application."); - Console.WriteLine($"Devices must be configured with 16 or more buttons, 1 or more continuous POV hats, and have the X, Y, and Z axes."); - break; - - case 1: - Console.WriteLine($"{freeDeviceCount} vJoy device available."); - break; - - default: - Console.WriteLine($"{freeDeviceCount} vJoy devices available."); - break; - } - - return freeDeviceCount; - } - private void SetStartButtonEnabled() { - startButton.IsEnabled = ( + startButton.IsEnabled = controllerDeviceTypeCombo.SelectedIndex != (int)ControllerType.None && - pcapDeviceCombo.SelectedIndex != -1 - ); + pcapDeviceCombo.SelectedIndex != -1; } /// @@ -388,7 +331,7 @@ private void StopCapture() bool doPacketLogMessage = Logging.PacketLogExists; // Close packet log file Logging.ClosePacketLog(); - + // Disable packet capture active flag packetCaptureActive = false; @@ -421,22 +364,20 @@ private void StopCapture() private void pcapDeviceCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) { // Get selected combo box item - ComboBoxItem selection = pcapDeviceCombo.SelectedItem as ComboBoxItem; - if (selection == null) + if (!(pcapDeviceCombo.SelectedItem is ComboBoxItem selection)) { // Disable start button startButton.IsEnabled = false; // Clear saved device - Properties.Settings.Default.pcapDevice = String.Empty; - Properties.Settings.Default.Save(); + Settings.Default.pcapDevice = String.Empty; + Settings.Default.Save(); return; } string itemName = selection.Name; // Get index of selected Pcap device - int pcapDeviceIndex = -1; - if (int.TryParse(itemName.Substring(pcapComboBoxItemName.Length), out pcapDeviceIndex)) + if (int.TryParse(itemName.Substring(pcapComboBoxPrefix.Length), out int pcapDeviceIndex)) { // Adjust index count (UI->Logical) pcapDeviceIndex -= 1; @@ -449,16 +390,14 @@ private void pcapDeviceCombo_SelectionChanged(object sender, SelectionChangedEve SetStartButtonEnabled(); // Remember selected Pcap device's name - Properties.Settings.Default.pcapDevice = pcapSelectedDevice.Name; - Properties.Settings.Default.Save(); + Settings.Default.pcapDevice = pcapSelectedDevice.Name; + Settings.Default.Save(); } } /// /// Handles the click of the Start button. /// - /// - /// private void startButton_Click(object sender, RoutedEventArgs e) { if (!packetCaptureActive) @@ -483,8 +422,8 @@ private void packetDebugCheckBox_Checked(object sender, RoutedEventArgs e) packetDebugLog = packetLogCheckBox.IsChecked.GetValueOrDefault(); // Remember selected packet debug state - Properties.Settings.Default.packetDebug = true; - Properties.Settings.Default.Save(); + Settings.Default.packetDebug = true; + Settings.Default.Save(); } /// @@ -499,8 +438,8 @@ private void packetDebugCheckBox_Unchecked(object sender, RoutedEventArgs e) packetDebugLog = false; // Remember selected packet debug state - Properties.Settings.Default.packetDebug = false; - Properties.Settings.Default.Save(); + Settings.Default.packetDebug = false; + Settings.Default.Save(); } /// @@ -513,8 +452,8 @@ private void packetLogCheckBox_Checked(object sender, RoutedEventArgs e) packetDebugLog = true; // Remember selected packet debug state - Properties.Settings.Default.packetDebugLog = true; - Properties.Settings.Default.Save(); + Settings.Default.packetDebugLog = true; + Settings.Default.Save(); } /// @@ -527,8 +466,8 @@ private void packetLogCheckBox_Unchecked(object sender, RoutedEventArgs e) packetDebugLog = false; // Remember selected packet debug state - Properties.Settings.Default.packetDebugLog = false; - Properties.Settings.Default.Save(); + Settings.Default.packetDebugLog = false; + Settings.Default.Save(); } /// @@ -549,10 +488,10 @@ private void controllerDeviceTypeCombo_SelectionChanged(object sender, Selection { // vJoy case 0: - if (CountAvailableVjoyDevices() > 0) + if (VjoyClient.GetAvailableDeviceCount() > 0) { XboxDevice.MapperMode = MappingMode.vJoy; - Properties.Settings.Default.controllerDeviceType = (int)ControllerType.vJoy; + Settings.Default.controllerDeviceType = (int)ControllerType.vJoy; } else { @@ -566,18 +505,18 @@ private void controllerDeviceTypeCombo_SelectionChanged(object sender, Selection // ViGEmBus case 1: XboxDevice.MapperMode = MappingMode.ViGEmBus; - Properties.Settings.Default.controllerDeviceType = (int)ControllerType.VigemBus; + Settings.Default.controllerDeviceType = (int)ControllerType.VigemBus; break; default: XboxDevice.MapperMode = 0; - Properties.Settings.Default.controllerDeviceType = (int)ControllerType.None; + Settings.Default.controllerDeviceType = (int)ControllerType.None; break; } // Save setting - Properties.Settings.Default.controllerDeviceType = controllerDeviceTypeCombo.SelectedIndex; - Properties.Settings.Default.Save(); + Settings.Default.controllerDeviceType = controllerDeviceTypeCombo.SelectedIndex; + Settings.Default.Save(); // Enable start button SetStartButtonEnabled(); @@ -614,8 +553,8 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) pcapSelectedDevice = device; // Remember the new device - Properties.Settings.Default.pcapDevice = pcapSelectedDevice.Name; - Properties.Settings.Default.Save(); + Settings.Default.pcapDevice = pcapSelectedDevice.Name; + Settings.Default.Save(); // Refresh the dropdown PopulatePcapDropdown(); @@ -630,7 +569,7 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) if (!foundDevice) { result = MessageBox.Show( - "No Xbox One receivers could be found through checking device properties.\nYou will now be guided through a second auto-detection process. Press Cancel at any time to cancel the process.", + "No Xbox One receivers could be found through checking device \nYou will now be guided through a second auto-detection process. Press Cancel at any time to cancel the process.", "Auto-Detect Receiver", MessageBoxButton.OKCancel ); @@ -652,7 +591,7 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) } // Get the list of devices for when receiver is unplugged - CaptureDeviceList notPlugged = CaptureDeviceList.New(); + var notPlugged = CaptureDeviceList.New(); // Prompt user to plug in their receiver result = MessageBox.Show( @@ -666,22 +605,22 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) } // Get the list of devices for when receiver is plugged in - CaptureDeviceList plugged = CaptureDeviceList.New(); + var plugged = CaptureDeviceList.New(); // Get device names for both not plugged and plugged lists - List notPluggedNames = new List(); - List pluggedNames = new List(); - foreach (ILiveDevice oldDevice in notPlugged) + var notPluggedNames = new List(); + var pluggedNames = new List(); + foreach (var oldDevice in notPlugged) { notPluggedNames.Add(oldDevice.Name); } - foreach (ILiveDevice newDevice in plugged) + foreach (var newDevice in plugged) { pluggedNames.Add(newDevice.Name); } // Compare the lists and find what notPlugged doesn't contain - List newNames = new List(); + var newNames = new List(); foreach (string pluggedName in pluggedNames) { if (!notPluggedNames.Contains(pluggedName)) @@ -691,8 +630,8 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) } // Create a list of new devices based on the list of new device names - List newDevices = new List(); - foreach (ILiveDevice newDevice in plugged) + var newDevices = new List(); + foreach (var newDevice in plugged) { if (newNames.Contains(newDevice.Name)) { @@ -707,8 +646,8 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) pcapSelectedDevice = newDevices.First(); // Remember the new device - Properties.Settings.Default.pcapDevice = pcapSelectedDevice.Name; - Properties.Settings.Default.Save(); + Settings.Default.pcapDevice = pcapSelectedDevice.Name; + Settings.Default.Save(); } else { @@ -729,18 +668,15 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) } /// - /// Event handler for AppDomain.CurrentDomain.UnhandledException. + /// Logs unhandled exceptions to a file and prompts the user with the exception message. /// - /// - /// Logs the exception info to a file and prompts the user with the exception message. - /// public static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) { // The unhandled exception - Exception unhandledException = args.ExceptionObject as Exception; + var unhandledException = args.ExceptionObject as Exception; // MessageBox message - StringBuilder message = new StringBuilder(); + var message = new StringBuilder(); message.AppendLine("An unhandled error has occured:"); message.AppendLine(); message.AppendLine(unhandledException.GetFirstLine()); @@ -761,7 +697,7 @@ public static void OnUnhandledException(object sender, UnhandledExceptionEventAr message.AppendLine("A log of the error has been created, do you want to open it?"); // Display message - MessageBoxResult result = MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.YesNo, MessageBoxImage.Error); + var result = MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.YesNo, MessageBoxImage.Error); // If user requested to, open the log if (result == MessageBoxResult.Yes) { @@ -780,7 +716,7 @@ public static void OnUnhandledException(object sender, UnhandledExceptionEventAr // Close the log files Logging.CloseAll(); // Save settings - Properties.Settings.Default.Save(); + Settings.Default.Save(); // Close program MessageBox.Show("The program will now shut down.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); diff --git a/Program/MainWindow/PcapDeviceExtensions.cs b/Program/MainWindow/PcapDeviceExtensions.cs index 62a2d4c..39953ef 100644 --- a/Program/MainWindow/PcapDeviceExtensions.cs +++ b/Program/MainWindow/PcapDeviceExtensions.cs @@ -5,9 +5,9 @@ namespace RB4InstrumentMapper public static class PcapDeviceExtensions { /// - /// Gets the display name for a capture device. + /// Gets the display name for this capture device. /// - public static string GetDisplayName(this ILiveDevice device) + public static string GetDisplayName(this ICaptureDevice device) { if (!string.IsNullOrWhiteSpace(device.Description)) { @@ -18,9 +18,9 @@ public static string GetDisplayName(this ILiveDevice device) } /// - /// Determines whether or not a capture device is an Xbox One receiver. + /// Determines whether or not this capture device is an Xbox One receiver. /// - public static bool IsXboxOneReceiver(this ILiveDevice device) + public static bool IsXboxOneReceiver(this ICaptureDevice device) { // Depending on the receiver, there are two ways of detection: // - Description of "MT7612US_RL" diff --git a/Program/MainWindow/TextBoxConsole.cs b/Program/MainWindow/TextBoxWriter.cs similarity index 71% rename from Program/MainWindow/TextBoxConsole.cs rename to Program/MainWindow/TextBoxWriter.cs index a7f0bcf..5c95272 100644 --- a/Program/MainWindow/TextBoxConsole.cs +++ b/Program/MainWindow/TextBoxWriter.cs @@ -1,22 +1,18 @@ using System; using System.IO; +using System.Linq; using System.Text; using System.Windows.Controls; -using System.Linq; - -// Adds a Debug/Output Console to WPF application -// - XAML: -// - MainWindow(): TextBoxOutputter.RedirectConsoleToTextBox(messageConsole); namespace RB4InstrumentMapper { /// - /// Adds a Debug/Output Console to WPF application + /// A text writer which redirects the standard output stream to a WPF textbox. /// /// /// https://social.technet.microsoft.com/wiki/contents/articles/12347.wpfhowto-add-a-debugoutput-console-to-your-application.aspx /// - public class TextBoxConsole : TextWriter + public class TextBoxWriter : TextWriter { /// /// Default maximum number of lines to keep in console. @@ -26,7 +22,7 @@ public class TextBoxConsole : TextWriter /// /// Line split characters. /// - private static char[] newlineChars = Environment.NewLine.ToCharArray(); + private static readonly char[] newlineChars = Environment.NewLine.ToCharArray(); /// /// Cache for current line. @@ -41,25 +37,21 @@ public class TextBoxConsole : TextWriter /// /// Text box handle that displays text. /// - private TextBox textBox = null; + private readonly TextBox textBox = null; /// /// Display lines in reverse order (newest first) if set. Defaults to false. /// - private bool displayLinesInReverseOrder = false; + private readonly bool displayLinesInReverseOrder = false; /// /// Display timestamp for each line. Defaults to true. /// - private bool displayLinesWithTimestamp = true; + private readonly bool displayLinesWithTimestamp = true; /// /// Connects console output to a given text box. /// - /// Text box to receive console output. - /// Maximum number of lines to display in the text box. Defaults to 100. - /// Display lines in reverse order (newest first). Defaults to true. - /// Display timestamp for each line. Defaults to true. public static void RedirectConsoleToTextBox( TextBox textBox, int maxNumberOfLines = DefaultMaxNumberLines, @@ -67,18 +59,16 @@ public static void RedirectConsoleToTextBox( bool displayLinesWithTimestamp = true ) { - TextBoxConsole textBoxOutputter = new TextBoxConsole(textBox, displayLinesInReverseOrder, displayLinesWithTimestamp); - Console.SetOut(textBoxOutputter); + var textboxConsole = new TextBoxWriter(textBox, displayLinesInReverseOrder, displayLinesWithTimestamp); + Console.SetOut(textboxConsole); currentLineCache = new StringBuilder(); visibleTextCache = new FixedSizeConcurrentQueue(maxNumberOfLines); } /// - /// Creates a TextBoxOutputter instance. + /// Creates a new TextBoxConsole. /// - /// Target text box - /// Reverse text. - public TextBoxConsole(TextBox output, bool reverse = false, bool timestamp = true) + public TextBoxWriter(TextBox output, bool reverse = false, bool timestamp = true) { textBox = output; displayLinesInReverseOrder = reverse; @@ -88,7 +78,6 @@ public TextBoxConsole(TextBox output, bool reverse = false, bool timestamp = tru /// /// Write text to outputter. /// - /// public override void Write(char value) { base.Write(value); @@ -132,9 +121,6 @@ public override void Write(char value) /// /// Gets the encoding of the outputter. /// - public override Encoding Encoding - { - get { return System.Text.Encoding.UTF8; } - } + public override Encoding Encoding => Encoding.UTF8; } } diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 6c55db3..0db953b 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -6,21 +6,19 @@ namespace RB4InstrumentMapper.Parsing { - public delegate void PacketReceivedHandler(DateTime timestamp, ReadOnlySpan data); - /// /// A standard IEEE 802.11 QoS header. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] internal unsafe struct QoSHeader { - ushort frameControl; - ushort durationId; - fixed byte receiverAddress[6]; - fixed byte transmitterAddress[6]; - fixed byte destinationAddress[6]; - ushort sequenceControl; - ushort qosControl; + private readonly ushort frameControl; + private readonly ushort durationId; + private fixed byte receiverAddress[6]; + private fixed byte transmitterAddress[6]; + private fixed byte destinationAddress[6]; + private readonly ushort sequenceControl; + private readonly ushort qosControl; public byte FrameType => (byte)((frameControl & 0xC) >> 2); public byte FrameSubtype => (byte)((frameControl & 0xF0) >> 4); @@ -41,15 +39,24 @@ public ulong DeviceId } } + /// + /// Backend for handling controllers via Pcap. + /// public static class PcapBackend { - public static bool LogPackets = false; + /// + /// Whether or not packets should be logged to the console. + /// + public static bool LogPackets { get; set; } = false; + + /// + /// Event fired when packet capture stops automatically. + /// + public static event Action OnCaptureStop; private static ILiveDevice captureDevice = null; private static readonly Dictionary devices = new Dictionary(); - public static event Action OnCaptureStop; - /// /// Starts capturing packets from the given device. /// @@ -65,7 +72,7 @@ public static void StartCapture(ILiveDevice device) // Configure packet receive event handler device.OnPacketArrival += OnPacketArrival; - + // Start capture device.StartCapture(); captureDevice = device; diff --git a/Program/PacketParsing/DeviceGuids.cs b/Program/PacketParsing/DeviceGuids.cs index 7a97683..4a2c372 100644 --- a/Program/PacketParsing/DeviceGuids.cs +++ b/Program/PacketParsing/DeviceGuids.cs @@ -9,7 +9,7 @@ internal static class DeviceGuids { public static readonly Guid MadCatzGuitar = Guid.Parse("0D2AE438-7F7D-4933-8693-30FC55018E77"); public static readonly Guid MadCatzDrumkit = Guid.Parse("06182893-CCE0-4B85-9271-0A10DBAB7E07"); - public static readonly Guid PdpGuitar = Guid.Parse("1A266AF6-3A46-45E3-B9B6-0F2C0B2C1EBE"); + public static readonly Guid PdpGuitar = Guid.Parse("1A266AF6-3A46-45E3-B9B6-0F2C0B2C1EBE"); public static readonly Guid PdpDrumkit = Guid.Parse("A503F9B0-955E-47C4-A2ED-B1336FA7703E"); public static readonly Guid XboxInputDevice = Guid.Parse("9776FF56-9BFD-4581-AD45-B645BBA526D6"); diff --git a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs index 65344bc..0bcd3d6 100644 --- a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs +++ b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs @@ -29,10 +29,14 @@ protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan /// Parses an input report. @@ -54,10 +58,6 @@ private unsafe XboxResult ParseInput(ReadOnlySpan data) /// internal static void HandleReport(IXbox360Controller device, in DrumInput report, ref int previousDpadCymbals, ref int dpadMask) { - // Constants for the d-pad masks - const int yellowBit = 0x01; - const int blueBit = 0x02; - // Menu and Options var buttons = (GamepadButton)report.Buttons; device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); @@ -130,9 +130,9 @@ internal static void HandleReport(IXbox360Controller device, in DrumInput report (yellowCym | blueCym | greenCym) != 0); // Pedals - device.SetButtonState(Xbox360Button.LeftShoulder, + device.SetButtonState(Xbox360Button.LeftShoulder, (report.Buttons & (ushort)DrumButton.KickOne) != 0); - device.SetButtonState(Xbox360Button.LeftThumb, + device.SetButtonState(Xbox360Button.LeftThumb, (report.Buttons & (ushort)DrumButton.KickTwo) != 0); // Velocities diff --git a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs index e561833..98a85f1 100644 --- a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs @@ -28,9 +28,9 @@ protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan /// Parses an input report. diff --git a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs b/Program/PacketParsing/Mappers/GamepadVigemMapper.cs index d37610b..f1a4eb1 100644 --- a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs +++ b/Program/PacketParsing/Mappers/GamepadVigemMapper.cs @@ -72,7 +72,7 @@ internal static void HandleReport(IXbox360Controller device, in GamepadInput rep // Menu and Options device.SetButtonState(Xbox360Button.Start, report.Menu); device.SetButtonState(Xbox360Button.Back, report.Options); - + // Sticks device.SetAxisValue(Xbox360Axis.LeftThumbX, report.LeftStickX); device.SetAxisValue(Xbox360Axis.LeftThumbY, report.LeftStickY); diff --git a/Program/PacketParsing/Mappers/IDeviceMapper.cs b/Program/PacketParsing/Mappers/IDeviceMapper.cs index 401da9e..7e17e6b 100644 --- a/Program/PacketParsing/Mappers/IDeviceMapper.cs +++ b/Program/PacketParsing/Mappers/IDeviceMapper.cs @@ -7,6 +7,9 @@ namespace RB4InstrumentMapper.Parsing /// internal interface IDeviceMapper : IDisposable { + /// + /// Whether or not the guide button should be mapped. + /// bool MapGuideButton { get; set; } /// diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 3ccfe65..9489114 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -11,7 +11,7 @@ namespace RB4InstrumentMapper.Parsing internal static class MapperFactory { // Device interface GUIDs to check when getting the device mapper - private static Dictionary> guidToMapper = new Dictionary>() + private static readonly Dictionary> guidToMapper = new Dictionary>() { { DeviceGuids.MadCatzGuitar, GetGuitarMapper }, { DeviceGuids.PdpGuitar, GetGuitarMapper }, diff --git a/Program/PacketParsing/Mappers/VigemMapper.cs b/Program/PacketParsing/Mappers/VigemMapper.cs index 46c0a3c..c69a84a 100644 --- a/Program/PacketParsing/Mappers/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/VigemMapper.cs @@ -113,8 +113,13 @@ private void Dispose(bool disposing) if (disposing) { // Reset report - try { device?.ResetReport(); } catch {} - try { device?.SubmitReport(); } catch {} + try + { + device?.ResetReport(); + device?.SubmitReport(); + } + catch + { } // Disconnect device try { device?.Disconnect(); } catch {} diff --git a/Program/PacketParsing/Packets/DeviceStatus.cs b/Program/PacketParsing/Packets/DeviceStatus.cs index e41c172..e2cbdc2 100644 --- a/Program/PacketParsing/Packets/DeviceStatus.cs +++ b/Program/PacketParsing/Packets/DeviceStatus.cs @@ -20,12 +20,12 @@ internal enum BatteryLevel : byte } [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct DeviceStatus + internal readonly struct DeviceStatus { - private byte status; - private byte unk1; - private byte unk2; - private byte unk3; + private readonly byte status; + private readonly byte unk1; + private readonly byte unk2; + private readonly byte unk3; public bool Connected => (status & 0b1100_0000) != 0; public BatteryType BatteryType => (BatteryType)(status & 0b0000_1100); diff --git a/Program/PacketParsing/Packets/DrumInput.cs b/Program/PacketParsing/Packets/DrumInput.cs index 31be443..beb5257 100644 --- a/Program/PacketParsing/Packets/DrumInput.cs +++ b/Program/PacketParsing/Packets/DrumInput.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace RB4InstrumentMapper.Parsing @@ -45,8 +44,8 @@ enum DrumCymbal : ushort } public ushort Buttons; - ushort pads; - ushort cymbals; + private readonly ushort pads; + private readonly ushort cymbals; public byte RedPad => (byte)((pads & (ushort)DrumPad.Red) >> 4); public byte YellowPad => (byte)(pads & (ushort)DrumPad.Yellow); @@ -56,14 +55,5 @@ enum DrumCymbal : ushort public byte YellowCymbal => (byte)((cymbals & (ushort)DrumCymbal.Yellow) >> 4); public byte BlueCymbal => (byte)(cymbals & (ushort)DrumCymbal.Blue); public byte GreenCymbal => (byte)((cymbals & (ushort)DrumCymbal.Green) >> 12); - - /// - /// Gets a byte value from a ushort field of multiple values. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - byte getByteValue(ushort field, ushort mask, ushort offset) - { - return (byte)((field & mask) >> offset); - } } } \ No newline at end of file diff --git a/Program/PacketParsing/Packets/GuitarInput.cs b/Program/PacketParsing/Packets/GuitarInput.cs index a550e99..7c02c67 100644 --- a/Program/PacketParsing/Packets/GuitarInput.cs +++ b/Program/PacketParsing/Packets/GuitarInput.cs @@ -44,9 +44,9 @@ internal struct GuitarInput public byte PickupSwitch; public byte UpperFrets; public byte LowerFrets; - byte unk1; - byte unk2; - byte unk3; + private readonly byte unk1; + private readonly byte unk2; + private readonly byte unk3; public bool Green => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Green) != 0; public bool Red => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Red) != 0; diff --git a/Program/PacketParsing/ParsingUtils.cs b/Program/PacketParsing/ParsingUtils.cs index 496f0c0..297113e 100644 --- a/Program/PacketParsing/ParsingUtils.cs +++ b/Program/PacketParsing/ParsingUtils.cs @@ -42,37 +42,6 @@ public static bool DecodeLEB128(ReadOnlySpan data, out int result, out int return true; } - /// - /// Scales this byte to an int, starting from the negative end. - /// - public static int ScaleToInt32(this byte input) - { - // Duplicate the input value to the higher 8-bit regions by multiplying by a number with the - // first bit of each region set to 1, then XOR with the negative bit to make the range start from the negative end - return (int)((input * 0x01010101) ^ 0x80000000); - } - - /// - /// Scales this short to an int. - /// - public static int ScaleToInt32(this short input) - { - // Duplicate the input value to the higher 16-bit regions by multiplying by a number with the - // first bit of each region set to 1 - // Shorts already - return input * 0x00010001; - } - - /// - /// Scales this byte to a uint. - /// - public static uint ScaleToUInt32(this byte input) - { - // Duplicate the input value to the higher 8-bit regions by multiplying by a number with the - // first bit of each region set to 1 - return (uint)(input * 0x01010101); - } - /// /// Scales a byte to a short, starting from the negative end. /// diff --git a/Program/Properties/AssemblyInfo.cs b/Program/Properties/AssemblyInfo.cs index 8f82295..1ab8552 100644 --- a/Program/Properties/AssemblyInfo.cs +++ b/Program/Properties/AssemblyInfo.cs @@ -1,6 +1,4 @@ using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Windows; diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 8a0871c..fe01bc8 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -106,7 +106,8 @@ MSBuild:Compile Designer - + + @@ -131,13 +132,12 @@ - - + MSBuild:Compile Designer diff --git a/Program/Vjoy/VjoyClient.cs b/Program/Vjoy/VjoyClient.cs index 50362a0..62c4103 100644 --- a/Program/Vjoy/VjoyClient.cs +++ b/Program/Vjoy/VjoyClient.cs @@ -1,3 +1,4 @@ +using System; using vJoyInterfaceWrap; namespace RB4InstrumentMapper.Vjoy @@ -50,6 +51,45 @@ public static bool IsDeviceCompatible(uint deviceId) return false; } + /// + /// Counts the number of available vJoy devices. + /// + public static int GetAvailableDeviceCount() + { + if (!Enabled) + { + return 0; + } + + // Loop through vJoy IDs and populate list + int freeDeviceCount = 0; + for (uint id = 1; id <= 16; id++) + { + if (IsDeviceAvailable(id)) + { + freeDeviceCount++; + } + } + + switch (freeDeviceCount) + { + case 0: + Console.WriteLine($"No vJoy devices available! Please configure some in the Configure vJoy application."); + Console.WriteLine($"Devices must be configured with 16 or more buttons, 1 or more continuous POV hats, and have the X, Y, and Z axes."); + break; + + case 1: + Console.WriteLine($"{freeDeviceCount} vJoy device available."); + break; + + default: + Console.WriteLine($"{freeDeviceCount} vJoy devices available."); + break; + } + + return freeDeviceCount; + } + /// /// Gets the next available device ID. /// @@ -59,7 +99,7 @@ public static uint GetNextAvailableID() for (uint deviceId = 1; deviceId <= 16; deviceId++) { // Ensure device is available - if (client.GetVJDStatus(deviceId) == VjdStat.VJD_STAT_FREE && IsDeviceCompatible(deviceId)) + if (IsDeviceAvailable(deviceId)) { return deviceId; } From 3f33fc0d1c66ceb2ce37752de0d255bb1e4eac9a Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 25 Jun 2023 07:50:07 -0600 Subject: [PATCH 163/437] Move chunk index parsing to header parsing --- .../PacketParsing/Packets/CommandHeader.cs | 30 ++++++++++++++----- Program/PacketParsing/XboxClient.cs | 28 +++++------------ Program/PacketParsing/XboxDevice.cs | 13 +------- 3 files changed, 32 insertions(+), 39 deletions(-) diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 7f80af4..0fea292 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -42,6 +42,7 @@ internal struct CommandHeader public int Client; public byte SequenceCount; public int DataLength; + public int ChunkIndex; public static bool TryParse(ReadOnlySpan data, out CommandHeader header, out int bytesRead) { @@ -52,20 +53,35 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o return false; } - if (!ParsingUtils.DecodeLEB128(data.Slice(3), out int dataLength, out int byteLength)) - { - return false; - } - + // Command info header = new CommandHeader() { CommandId = (CommandId)data[0], Flags = (CommandFlags)(data[1] & 0xF0), Client = data[1] & 0x0F, SequenceCount = data[2], - DataLength = dataLength }; - bytesRead = 3 + byteLength; + bytesRead += 3; + + // Message length + if (!ParsingUtils.DecodeLEB128(data.Slice(bytesRead), out int dataLength, out int byteLength)) + { + return false; + } + header.DataLength = dataLength; + bytesRead += byteLength; + + // Chunk index/length + if ((header.Flags & CommandFlags.ChunkPacket) != 0) + { + if (!ParsingUtils.DecodeLEB128(data.Slice(bytesRead), out int chunkIndex, out byteLength)) + { + return false; + } + + header.ChunkIndex = chunkIndex; + bytesRead += byteLength; + } return true; } diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index dc0ea5a..1d05acc 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -30,6 +30,13 @@ internal class XboxClient : IDisposable /// internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan commandData) { + // Verify packet length + if (header.DataLength != commandData.Length) + { + Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {commandData.Length}"); + return XboxResult.InvalidMessage; + } + // Chunked packets if ((header.Flags & CommandFlags.ChunkPacket) != 0) { @@ -47,13 +54,6 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan chunkData) { - // Get sequence length/index - if (!ParsingUtils.DecodeLEB128(chunkData, out int bufferIndex, out int bytesRead)) - { - return XboxResult.InvalidMessage; - } - chunkData = chunkData.Slice(bytesRead); - - // Verify packet length - if (header.DataLength != chunkData.Length) - { - Debug.Fail($"Command header length does not match buffer length! Header: {header.DataLength} Buffer: {chunkData.Length}"); - return XboxResult.InvalidMessage; - } + int bufferIndex = header.ChunkIndex; // Do nothing with chunks of length 0 if (bufferIndex <= 0) diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index e6a7cf1..cd3c7a5 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -51,17 +51,6 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) } int messageLength = headerLength + header.DataLength; - // Chunked messages - if ((header.Flags & CommandFlags.ChunkPacket) != 0) - { - if (!ParsingUtils.DecodeLEB128(data.Slice(headerLength), out int _, out int indexLength)) - { - return XboxResult.InvalidMessage; - } - - messageLength += indexLength; - } - // Verify bounds if (data.Length < messageLength) { @@ -69,7 +58,7 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) } var messageData = data.Slice(0, messageLength); - var commandData = messageData.Slice(headerLength); // Chunk index is not removed here, as message handling needs it + var commandData = messageData.Slice(headerLength); if (!clients.TryGetValue(header.Client, out var client)) { From 8045f9838937f0202c8b99d8db882971a62dd63c Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 25 Jun 2023 08:00:32 -0600 Subject: [PATCH 164/437] Fix package references using the wrong directory --- Program/RB4InstrumentMapper.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index fe01bc8..30d9b3b 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -58,19 +58,19 @@ - ..\packages\Nefarius.ViGEm.Client.1.21.256\lib\netstandard2.0\Nefarius.ViGEm.Client.dll + packages\Nefarius.ViGEm.Client.1.21.256\lib\netstandard2.0\Nefarius.ViGEm.Client.dll - ..\packages\PacketDotNet.1.4.7\lib\net47\PacketDotNet.dll + packages\PacketDotNet.1.4.7\lib\net47\PacketDotNet.dll - ..\packages\SharpPcap.6.2.5\lib\netstandard2.0\SharpPcap.dll + packages\SharpPcap.6.2.5\lib\netstandard2.0\SharpPcap.dll packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + packages\System.Memory.4.5.5\lib\net461\System.Memory.dll @@ -80,7 +80,7 @@ packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - ..\packages\System.Text.Encoding.CodePages.7.0.0\lib\net462\System.Text.Encoding.CodePages.dll + packages\System.Text.Encoding.CodePages.7.0.0\lib\net462\System.Text.Encoding.CodePages.dll False From 0b553237017dde83245452109fac55936687230b Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 25 Jun 2023 08:51:42 -0600 Subject: [PATCH 165/437] Re-add message for when Pcap isn't installed --- Program/MainWindow/MainWindow.xaml.cs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 2708d08..47adc91 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -22,11 +22,6 @@ public partial class MainWindow : Window /// private static Dispatcher uiDispatcher = null; - /// - /// List of available Pcap devices. - /// - private readonly CaptureDeviceList pcapDeviceList = CaptureDeviceList.Instance; - /// /// The selected Pcap device. /// @@ -157,7 +152,20 @@ private void Window_Loaded(object sender, RoutedEventArgs e) } // Initialize Pcap dropdown - PopulatePcapDropdown(); + try + { + PopulatePcapDropdown(); + } + catch (DllNotFoundException ex) + { + MessageBox.Show("Could not load Pcap interface! Please ensure WinPcap is installed on your system.", "Couldn't Find WinPcap", MessageBoxButton.OK, MessageBoxImage.Error); + Application.Current.Shutdown(); + + // Log exception + Logging.Main_WriteException(ex); + + return; + } } /// @@ -190,6 +198,7 @@ private void PopulatePcapDropdown() pcapDeviceCombo.Items.Clear(); // Refresh the device list + var pcapDeviceList = CaptureDeviceList.Instance; pcapDeviceList.Refresh(); if (pcapDeviceList.Count == 0) { @@ -254,6 +263,7 @@ private void StartCapture() } // Check if the device is still present + var pcapDeviceList = CaptureDeviceList.Instance; pcapDeviceList.Refresh(); bool deviceStillPresent = false; foreach (var device in pcapDeviceList) @@ -383,7 +393,7 @@ private void pcapDeviceCombo_SelectionChanged(object sender, SelectionChangedEve pcapDeviceIndex -= 1; // Assign device - pcapSelectedDevice = pcapDeviceList[pcapDeviceIndex]; + pcapSelectedDevice = CaptureDeviceList.Instance[pcapDeviceIndex]; Console.WriteLine($"Selected Pcap device {pcapSelectedDevice.GetDisplayName()}"); // Enable start button @@ -532,6 +542,7 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) MessageBoxResult result; // Refresh and check for Xbox One receivers + var pcapDeviceList = CaptureDeviceList.Instance; pcapDeviceList.Refresh(); bool foundDevice = false; foreach (var device in pcapDeviceList) From 04547be5b3d2f6b5f74e32b7189e1199a113b0a1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 25 Jun 2023 09:02:17 -0600 Subject: [PATCH 166/437] Fix ViGEmBus mode not creating devices --- Program/Vigem/VigemClient.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Program/Vigem/VigemClient.cs b/Program/Vigem/VigemClient.cs index 66996d6..2ef7b8f 100644 --- a/Program/Vigem/VigemClient.cs +++ b/Program/Vigem/VigemClient.cs @@ -23,7 +23,7 @@ public static class VigemClient /// Whether or not new devices can be created. /// public static bool AreDevicesAvailable => Initialized && canCreateDevices; - private static bool canCreateDevices; + private static bool canCreateDevices = false; public static bool TryInitialize() { @@ -33,11 +33,13 @@ public static bool TryInitialize() try { client = new ViGEmClient(); + canCreateDevices = true; return true; } catch { client = null; + canCreateDevices = false; return false; } } From 3d2528c397cfe528a070730442238de7876597d8 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 25 Jun 2023 09:49:14 -0600 Subject: [PATCH 167/437] Add message for device client connection --- Program/PacketParsing/Backends/PcapBackend.cs | 2 +- Program/PacketParsing/XboxDevice.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 0db953b..d5279bd 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -137,7 +137,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) ulong deviceId = header.DeviceId; if (!devices.TryGetValue(deviceId, out var device)) { - device = new XboxDevice(); + device = new XboxDevice(deviceId); devices.Add(deviceId, device); Console.WriteLine($"Device with ID {deviceId:X12} was connected"); } diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index cd3c7a5..5669022 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -24,11 +24,18 @@ public class XboxDevice : IDisposable { public static MappingMode MapperMode; + public ulong DeviceId { get; } + /// /// The clients currently on the device. /// private readonly Dictionary clients = new Dictionary(); + public XboxDevice(ulong deviceId) + { + DeviceId = deviceId; + } + ~XboxDevice() { Dispose(false); @@ -64,6 +71,7 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) { client = new XboxClient(); clients.Add(header.Client, client); + Console.WriteLine($"Client {header.Client} connected on device with ID {DeviceId:X12}"); } var clientResult = client.HandleMessage(header, commandData); switch (clientResult) From f4871b6859b26341e6ddd89c720fdd6c6a59fddb Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 25 Jun 2023 10:12:11 -0600 Subject: [PATCH 168/437] Bump version to v3.0 --- Installer/Product.wxs | 2 +- Program/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index a0bda12..5412969 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -4,7 +4,7 @@ Id="*" Name="RB4InstrumentMapper" Language="1033" - Version="2.1.1.0" + Version="3.0.0.0" Manufacturer="Andreas Schiffler" UpgradeCode="94bef546-701f-4571-9828-d4fa39b2ea84"> diff --git a/Program/Properties/AssemblyInfo.cs b/Program/Properties/AssemblyInfo.cs index 1ab8552..20c9983 100644 --- a/Program/Properties/AssemblyInfo.cs +++ b/Program/Properties/AssemblyInfo.cs @@ -49,5 +49,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.1.1.0")] -[assembly: AssemblyFileVersion("2.1.1.0")] +[assembly: AssemblyVersion("3.0.0.0")] +[assembly: AssemblyFileVersion("3.0.0.0")] From 0916b4cfabff1df2b47cae1e044701397d7212eb Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 25 Jun 2023 10:14:39 -0600 Subject: [PATCH 169/437] Update readme - Remove USBPcap, as it's not actually needed - Update receiver auto-detection info, and remove the troubleshooting info --- README.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f7bc155..5a58c46 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ Both guitars and drums are supported through an Xbox One wireless receiver. - Windows 10 64-bit - [WinPCap](https://www.winpcap.org/install/bin/WinPcap_4_1_3.exe) -- [USBPCap](https://desowin.org/usbpcap/) - [ViGEmBus](https://github.com/ViGEm/ViGEmBus/releases/latest) or [vJoy](https://github.com/jshafer817/vJoy/releases/latest) ## Hardware Notes @@ -31,8 +30,7 @@ Some guitars/drumkits might not sync properly when using just the sync button. T ## Installation 1. Install [WinPCap](https://www.winpcap.org/install/bin/WinPcap_4_1_3.exe). -2. Install [USBPCap](https://desowin.org/usbpcap/). -3. Install [ViGEmBus](https://github.com/ViGEm/ViGEmBus/releases/latest) (recommended) or [vJoy](https://github.com/jshafer817/vJoy/releases/latest). +2. Install [ViGEmBus](https://github.com/ViGEm/ViGEmBus/releases/latest) (recommended) or [vJoy](https://github.com/jshafer817/vJoy/releases/latest). - If you installed vJoy, configure it: 1. Open your Start menu, find the `vJoy` folder, and open the `Configure vJoy` program inside it. 2. Configure one device for each one of your guitars/drumkits, using these settings: @@ -44,17 +42,13 @@ Some guitars/drumkits might not sync properly when using just the sync button. T 3. Click Apply. - If you installed ViGEmBus, there's no configuration required. Outputs for guitars and drums will match that of their Xbox 360 counterparts. -4. Restart your PC. +3. Restart your PC. ## Usage 1. Select your Xbox One receiver from the dropdown menu. - - Xbox receivers should be detected automatically by device name. + - Xbox receivers should be detected automatically. - If they are not, click the `Auto-Detect Pcap` button and follow its instructions. - - You can also check the device list yourself for a device called `MT7612US_RL`. - - If that doesn't work: - - Make sure you are not using a USB 3.0 port, as those may cause the receiver to not show up for some reason. - - If you installed WinPcap, try installing Npcap instead, or vice versa. 2. Select either vJoy or ViGEmBus in the Controller Type dropdown. 3. Connect your instruments if you haven't yet. 4. Click the Start button. Devices will be detected automatically. From 599fd941a2221c8b55a68bd1208786d2eeea12d3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 25 Jun 2023 11:08:53 -0600 Subject: [PATCH 170/437] Re-order device GUID definitions --- Program/PacketParsing/DeviceGuids.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Program/PacketParsing/DeviceGuids.cs b/Program/PacketParsing/DeviceGuids.cs index 4a2c372..0348472 100644 --- a/Program/PacketParsing/DeviceGuids.cs +++ b/Program/PacketParsing/DeviceGuids.cs @@ -7,13 +7,13 @@ namespace RB4InstrumentMapper.Parsing /// internal static class DeviceGuids { + public static readonly Guid XboxInputDevice = Guid.Parse("9776FF56-9BFD-4581-AD45-B645BBA526D6"); + public static readonly Guid XboxNavigationController = Guid.Parse("B8F31FE7-7386-40E9-A9F8-2F21263ACFB7"); + public static readonly Guid XboxGamepad = Guid.Parse("082E402C-07DF-45E1-A5AB-A3127AF197B5"); + public static readonly Guid MadCatzGuitar = Guid.Parse("0D2AE438-7F7D-4933-8693-30FC55018E77"); public static readonly Guid MadCatzDrumkit = Guid.Parse("06182893-CCE0-4B85-9271-0A10DBAB7E07"); public static readonly Guid PdpGuitar = Guid.Parse("1A266AF6-3A46-45E3-B9B6-0F2C0B2C1EBE"); public static readonly Guid PdpDrumkit = Guid.Parse("A503F9B0-955E-47C4-A2ED-B1336FA7703E"); - - public static readonly Guid XboxInputDevice = Guid.Parse("9776FF56-9BFD-4581-AD45-B645BBA526D6"); - public static readonly Guid XboxNavigationController = Guid.Parse("B8F31FE7-7386-40E9-A9F8-2F21263ACFB7"); - public static readonly Guid XboxGamepad = Guid.Parse("082E402C-07DF-45E1-A5AB-A3127AF197B5"); } } From b397749b66c2ff6e4175961c90322657f1137119 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 25 Jun 2023 11:37:38 -0600 Subject: [PATCH 171/437] Fix receiver not being auto-selected on first startup --- Program/MainWindow/MainWindow.xaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 47adc91..69733e4 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -222,6 +222,7 @@ private void PopulatePcapDropdown() if (isSelected || (string.IsNullOrEmpty(currentPcapSelection) && device.IsXboxOneReceiver())) { pcapSelectedDevice = device; + isSelected = true; } pcapDeviceCombo.Items.Add(new ComboBoxItem() From 0519d7fa7708f95d4e8cfc24695c734f751f22bc Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 26 Jun 2023 21:27:31 -0600 Subject: [PATCH 172/437] Disable vJoy version check Probably more confusing than it is helpful, seems to work fine regardless --- Program/MainWindow/MainWindow.xaml.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 69733e4..9326cd8 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -94,12 +94,6 @@ private void Window_Loaded(object sender, RoutedEventArgs e) // Log vJoy driver attributes (Vendor Name, Product Name, Version Number) Console.WriteLine($"vJoy found! - Vendor: {VjoyClient.Manufacturer}, Product: {VjoyClient.Product}, Version Number: {VjoyClient.SerialNumber}"); - // Check if versions match - if (!VjoyClient.DriverMatch(out uint libraryVersion, out uint driverVersion)) - { - Console.WriteLine($"WARNING: vJoy library version (0x{libraryVersion:X8}) does not match driver version (0x{driverVersion:X8})! vJoy mode may cause errors!"); - } - if (VjoyClient.GetAvailableDeviceCount() > 0) { (controllerDeviceTypeCombo.Items[0] as ComboBoxItem).IsEnabled = true; From 1d8a3d229959d1afbe03879851708efdd039be85 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 27 Jun 2023 01:11:33 -0600 Subject: [PATCH 173/437] Update program screenshot --- Docs/Images/ProgramScreenshot.png | Bin 54953 -> 48170 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Docs/Images/ProgramScreenshot.png b/Docs/Images/ProgramScreenshot.png index b1ef3b431c2bff6799a3f044e5843e7e7071cd64..d8e49b8aa870b73ecf9bc50019339dc5558e665f 100644 GIT binary patch literal 48170 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

ykZI34B{B+YE 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 aZSZtruUoL3I2N8RQ$|8TyiC;4|Nj68eMY4K literal 54953 zcmYIu19W6f7j4HK+qN^Y*)b<}Cdo`}+qNgh#5O0kZF^$ddHMde-g~RNZ{0f8-S^(Y zslCrW;fe~9h;Vpt0000{T1reA0DwRP0Km?G&|gQE{&GS90LR=zRntk?(3Ql_!Pdmw z%9zB--OiZA*v;Go0B~EaOxH*z;HeY)tc}nDZ_q^A=cx~De)}6TOqWd7=6O(?UU4=I z1Y#eU#4x{pet3^}JtaEUQxz#Yj;-8KH!XV|iZ*tAoGo3NoX;E%>p!+0Y+gDX%wgN+ z5C{=hJvQ44Xn=S1-F98xLLp)ZA|!43w&hf{-Tm`$%h~kRR+_kEdyQSvahdRWQ}KNl zFkR@&j=qQyFgZ)q&+u5=eeaKk=nHz)JSe{6y8gSKGx**3a<0WJCClZCP0w)?%2U^A z%-MClv#U6Rf#u~KzN;fgaNUvvW9topvjs&yjI?!6*32*%&nwj@v|H`76U+6`+O_qV z4*lCu)+S5pUij;;O~JQnAt5BE$)xbis1;(JTBgf?(A4zM?_+x0{aMZ2GE3dILi(TE zn$Y3vlx!ND%ja$(I3w>7l?GM$5L{F~Lx($cKOhXq4ip#$q4*8z;YW;3BOeEN&NQ#T zoBk6_J%VAfqkSHr^F|=u%RJ%Wn!<3sydM!tktQXG2O&W%HB4WtMbVm|%U1L&ekUDB zQdXc`m{*=69m;f=q@>68gr!VYb)YRwR`)>USX6anY+O`tGw+0Q;AmWzDX(nYOlx3s zI6VHrhaEy}*dH%JWIR73krOl(#Wuk|R#87;ldNwz@LTmxSN4-7Uo7-KpbnpDDKN<{$la?2H}}$fL~)MY8T$_T;IPD zgHU_^v(3tL=VPQ!Z^At*eJRt8`)RhwnQ^>s=!f}~(HO${GOAP&i$l3r|H*ZO54+j7 zq0D1Rp5)lEmys^h-+ikD^dkbW}RI^!is!*e4!B z9WV&7ZG#s*mpn1>Y?6tm$C^MkigxX=??rtewov9NrdlOiQ1+QwcIWI>zuOu@$AL*? ze)b4-`V4QAY#>xfefij%XA>5JAgsjePv`SYGZ<_*uS&7;>9|kZoPKy__&I6 z=&0`ms!;l32>3yH^)k{(oom*TN$w-aWL%V+n2aBz7=?Gn zmnHw18`$=;L51E%k0IPE7(4@^ppz)T_$X&5=j!YBhRiv;cO8Yal-Aa4+#*onEw7xz zQ$l(^C7S7){|-lROTmxp?A>vnyJYemV2~z`P$DO{AhU_U3~Yf zn!0I}2PEs$Esye^yEzOh-46 zx|W8^{XgyC!FQh&qCwvckoQz9f?In?#>LBOsH-C|wT6QrXmCN){2ol!`wM%T-izsn z7G;ts(RDG!!WUMgvI}`KFPXXQw&9FxYpMLvwm(zBY`}fZ{+_LDbMZ80qcN2Aru|Wv z6P0-MOFmQBCwG^qHL@&fcMcyxgV^~SGk`Er+X~$kY7p<#Q{~n`hkQFc`2@`u#IzHl ze27EJ>LVC9lbO5%;$|aV(w>T>IU@fDH5Y^NE`9{M!=k!@Zrd3LM8i$<_hYo_B66B| z7J}GyXoj<4O)%|`?$4VMt)pvLHJir9O)#aTgLVyoAZElc(_f(_N-`Yl#)x4~O>QxO ztsn_%mr*37-d~cT2*&Ez<3A>|2I;)CNkIh2i+Vk+#@KCCJ zg)#qJ{M-R{!=z~gt2PQ`Poi$(G;lh`vi=mB9x{}RC&n@%T@E4rk3)@#jC@OLQ2M#mn3YRTt#JE*(JI45DO@@{GBx<_2QRQnb7qQL3F)Gx(W+&=5 zaFf6iK9 z!TJL=8>2Z6vtD?!^~|q5Jq~=dAwn*GzQ3WsO+VTeQS*dmV}8}Fwp*qEX+Xqnl8glH z3>TGP5vNK8S?TQs**r6;Q{J1bVjlsANj{7#LN5pviUh;Rwoup8N)6NiPb$>*$G>-$ zl%hLvlwam!8>Ie?X6CjCM{GSIwq_9!~oy8c}pdRUxk*UcPX+%*daG{?8H;N`Z zkj8q4GW+6I<)=w=##u0jHw$v}x@luWQkvpTtbrVfc&xnKW#azlA8x~Sd${BHk7TvA zBY&g799XC19;5$PL;deRlGj9597=psfF ztlJv!NDb7xYyehjgEx{K;NNKDKL?JVDECJMho4$~N7WrQBXm<0yT0OS%q>EZ7%zR@ znT?6zm9|ikO7Er5t$QUkVa8x9+g8>KTPrD0(TR#uCUm(HF0q%@j8twY#9Zy~WN4yE z3^m^fXX!OgkSQ)AH@A_hYm_lVu`7_l6vge=AB1Es&NZWEUR$Pzuqa}e4w2l2rRGx9 zR!#3&Z~p_9ZC;en;&g-j98Vdkn$`?0%qlSgdmHuW&W#Z^9x@x(>LVuRA#4FJ+oQgr zH@8+2W$uHBC#@zF3?D}yO29YTY~;8VPvA##<-y5D!wHJpu8YCAeMmlNUVX{!mwB z8{&_>OlrHL34m%bGP|rkk{XH6~?3WB7I74@lv7d#vkl{IoH1 zxA>{dNF8j|7-}(h4MGuiS|#aV8>pzsXEYlcoGiyHW*gj0hnyoVA8tc;haCKVAAuB5 z4ls+Q*q=(Ps3;RL!bjYkMqB##oYm5j@FR>s^8p4^;>eQJmD{XRZBQ$t5aG#+a8d^F zNy3%^ziR}i0|q=;2pe5|3Hf>*M(l?=cOMdKZpxd=W)I2d;~+WK1!xK4n)IDPS#Oaocs*U9b5rm zCAlQ|(qxywV%aWd_+2~?ay14y1_7-Ej)`crw+7LOEDb?R3#D0Ujf`E2SgfOoEfF6R=LDZXGr=zrfr*g`3vKItX6|WB=3K-V2j8Y454YVy&)(0hz&6 zIXD=3!Ci)2oHcBZgj6^q!feXCv~Ao?QSgGqascVKEU%FK?EPcj^Yw|ozE};4_esQ= z-m6Uy|Dz&%h|atJ^o^4phW|2*NhcW^VJG%Y&n^@>RimXsHV^&iX~e>x+71N?thW59 zmGG>%Ki;m=Z(coIXO25o8_6EGvfJ$sQ0cWDP7W|{kN$PssMtp<19PQFMB%|<1X&*9 z+nYXU%U~VBvTzl9EyvN)Ig>Q_D3+?{fIMk;P6yLa4#sQ6r>D-V&j4U|intSu3)bPl zHjoa;hh|EHNc6`$-rAf;s%OR@upAY&%eS>KWM@Ux1bu$Pn*#a;`ltA-x$mR2dXM(y zk21nC)3C~qcH!25^n`qCem(-P)sLR9OsR62kHZk;+vksisucLdJR^@xEeTK&^5RM^|c>;BUV7DMUeNd zeWCRAs=T?mq7||Oau}F%N1wkki)b0K z;NO5rId!VET;MKFne#W_tfyPG2GDoqqQCfV1qu_S>duK}WV9*hH}~1<_f{jQ-;Kfg zy7b#ry)vkOimDo%@Nr+(1aMFG|PW=y*R zZz7VUFWTI-rmfJWqlF<+=%0hil2tOykGc2gwWN(c)hs z&Z)TTOyFvuW8tH7$tRVvKI{naF5v?j5_ouuvg5ul7*@bZB?S||i>ou~$vm#ZNrz9; zQn2@`F})*~K~)*X6S!(pqV5#9DxF&a97-(@$V+}1)Wg>7jn`O-pY2!dqZYRk zK&_e!kDW7-fIR`_`_TsChpPX9Dpc?hZ~i8Fv|9qLBoYMN+?V15_2Y|IMjqfT5I_II zM5dH+H*nk+YW>UL55^bcE{a@_qel%x?<`g^A(#iI=I_SRSDrG8oVal^V6_jyQ9{Y_v!=S$)qO2#&S@u(>`z?iEGh^5 z*ghj%b?)gNtA!?l(YB`0guUq>R>{5)r>c|7m|;w-XmjwLStnhx2Fozq7FbAfgYp0w z5$`y-6V4^iIEWEe8t~rp$87SgxRyAGWfAQR<&b(-vN+{wfNv6ff`{-jDz3^zp-a%U znk4EdCI}x=i@14P@GKhAFq1RWBPJ!SBm)oUnGcUDE3Z=;N1X(R;#7)at;dQqvM zj6Ev+yN&luP_Dat-_p83<}a{-yew9Xy4r>s2Q2$Qwy<-U_4b5`^QHpM#-=ug|J{1_PgIT6|%=vd2ADclzPSD(QduBuTvPpjrX!@-~ z>LkW8Th5VmeaN98wS=C|bcr3D={? zs$7NZxlQD(CA5-+AUGBwl7=R$R14F^QL~U5V;N%I_XtzOLgNpwO|AN0EGo`~XjMK_ zE&9Ui5|tch?g1tj*@^sDE{5-tzc5NeylRks+bRj9!D;hCk&zxgQ8zXw=~g%(h#@!h zViQKG7yalCJ6P|{fU-7$l6%kYsRI_7W=*oG&ObxBiX9e_{Af-}7-@ z&VPC>csHvX7+L$sefZ?w6nnxM=Q6F#oZ#t{H#t}7nJjyqQ=tl}8cnxoV!Z6P1g-@} zrQ$@;15F7=^%pGj8txEqVFg7b6^&0T>z?#4T;rj`0c_t+qpBpo#pBO38HRzD=&rkT zIg~-Q;UX|`%FaVe=^Ixg<9qP6Vp?ca!#rAQGgqaFrh^M6Y&{$V$a7?7)1QRh85AcOHSwL39H-LHPTwTf(lbZ3r*^e!sr;_*8cmpI$5S zm+5G?tZ;0NFQFku#D`xu!`l6ze~|uBz!DB$fiDP}5MX^3^J99>(}Q znWb%?6?9MwSq2@WBFidR!>${{Qle#Z%Fq2HvcMM-?7+3u_x7FfS*W11wNO8yOByOY zbm=qc#bR`Q|8q6cP4Who+p@&li{Y09Uh^v>a* zEuKpi%#Bh^tV343lA;8xZ8Z1#BPzG9cc+&=OKUhVu&9j(EWZ*+@^A7h(bNtQIG#wi z3*SO^y+mzpg&$4#LxAr^k#a?4s4637z<-L;F&^}FWg6w=AjRmbB&n>^h0;7b!HM9o z(71trT6}OCW90p(Pj1!jUC-;xc`bwZF~Jm2pJG?{oiYs7zes5|I(72>7MD;%er&Rc zW+9#QI!-E8e6JP_TI^fHBY`0{ogvsc9i7la963${KhnR@0{g`}F?ZW`8y3Sl*St`$ zR5jsl{Zh4Ky*9&|BN)Rf%({sXvbCg@sbbru36sXPlN0B1Ck0zWrg)>T=1_cRna*Pug9QRR-7e zPk=y82-Q2M>BE=y5NIwcswgcg`hP`-FR3BZJ6=GlPZ1;dyNawh9g+)*{jg%zSsqzT zVT5>%;w)SxZ)*D;aymN&EmKT@WlvAeKv`g9eHBtw0Lc@??#XV!5gFxX3Wj$7)Ao(v zr01dCmbc$@8Oy|pEFj$`Lz#~D0KWX^6DFxxNw42H19_I-c#o_126^{m8~4;r*W)3c zY9tl}IGKF_ZjpSu2!ES7e&%C%eg#>1|6zY36^GQ@u=Btk*Q9X@R5mV$<1D{@k|x7r z&1_%APm6xGeipSkTK!?q3Wv?ziUquiIci_Sf|6>ZzX}{77|C%G&}10wisFczyA7uW znsG<`$9u%i@3;=M`(**~zrdW$;`D@`h~Kg+)zY(X63C=U?smC}_k`WB&A12;UzhDy zsfDj_TV~rnJm6DbDLPgbeoA&vbK2Q&EF=kRJHX($vl^+m6q5W_qyxJ&y_1JBc*4Tb zyggXt89IiD%hzJu0>FfJi>1{qd2eoP@iqHiIvqShKwLR^_{2eIe~CkorXunp06=Xl z{F@=vR~`DNtfUy=^S{@hj^e~G3#^@#mLmWFkN)2W43M6Q^JRo_l9m^TIfO!h#lZ(5 zteF7-BmikK5mmRaZcUBQP>o>=a^@hX#9e$9D}bOj15TZr%+kkV?%0 zi|3(M@wuPYsHmasmd;Ph{i~KXInOhbhB|H+11^_^p6kyQ`fn`7^;t}(-b6mK&+X;7 zSSK5ohldB?#~3NG{ek_hcg=QcsLZ^Srl$kvX@zWm-2>O|a%~xDn+{Mqr@pg!THX6r z{9n!4@FG0K965TXq=? zF30w2HkPA((;helJuTS%Hc}r3RQJ`#`>Uz5LhC~}qD0q5uX~SUK?2AvjLsx?w>G_- z9Ji>ekR`O=ehwd5cr{1}6pg2H+y5*#b?9A8Qe+>tLy0gk$a+CAdWI5~Kv`H=nD7v; zhS?HbXg55+fu9;(thsnMsDGbGV}n7bR_OnCeEf88iEn@TyrJX{$=%iF?EAT@)%}mn zK6z+rNp@)WE+;Fto+$?wfWvGcJjX_k%IA7pQrnU_J=c`ej zqY+H0Vc=1>*w+%?FOv)+SX4n(e*x*ud=FJOI~(cDjqv#Rn5Nx})umk!EOAS4o)t_X z#&^VkV?l-&lwr1AKSU!C zKfXVQ$KzJcRC=*X#1d^x*6&*FzY}yf{C+r|n7ENItnO$kYC2zy%z39+i2u^1^(G)e zXhIvUfu#$hnO?KfIBpl;cTFWI$N}AWzKB6*|M1^E*zQa2oS(;;4cL1eogAC*?aGD^ z?z`zCpmEagyRsa%yTJ&!?f#rYwd|%%EUU7?TfTi^`HQc5joLk^*5eUMe-QjE)eA8$h$jYhRXG@Q+_oP%T zr>~eLz=XeDB7x^3bF>K*8CGoMLp*r z4?>yVEWP7V2?iY@eM#ggD*E+kg|_qUE5r{XDv+&f z>XW)%FFzi+H{Xf*Mdovno^sLju@Cc#+sM0(Q64WY{v*<(=I?BpE>}SC#p@u_>}0UD?PA9B%mNqXayQt*DoVQVmZ+ z>d0d(7FH)qFzlft!D$Z^FV7%G-s6RV1Yr*g6Tq8 zX$6_DT@IN3*13A z8P$(u1|PfKNc~#Tnz2jm!n#pr5HsLHNs7(jIWdv^Q3L>*$xry4m>vu_kHWED0@suA zh#9)Bd!G9`LmLhN_7)$RLHIw0=$x3@!)h`)qyq0A1;xdEmi?d8F-vP*M23|6v~hJv zARs#yCTZ(VrwaAXbKiI8Q8W?U8Jjk4AX`8sl|!$wSh-RE&A%}gucs?c@8{q28ZLnP z30#>kUqK62f7M#zVlCy;Uv`UVg&Mm>LsMjoY$WEYysi;Rp|lD zp1;1g6UCz%Q8my{q%)JJq6Xp4n3q&n7ZdfMiKL>A1AC*6!otRR(&|p^FjWHL+#2T! z-Zcz*VynB8Ep0`QgFj&a%pEDlL|eHgyG~o~bGT5{1wZkYsjd45I=tMiZA5z z#WK-0ZQ0=yc`{;}n{fENeB;aYseR$@7^MNwr{)JuP=i*`j0HW5#D9&!Cgrrhw84&+7IUA+7xXS<%*zTMi(J|Qpgj2*RGGTt~}dJ zWS7|lP{|(qcQBPdwBWoIgl$of#C0g(w)4C>g*j%Q!g_KzlYi+TIl+db=(aaMwdX^< z8%9YkKczEiA;k{R<|`u~@`W+M^#PinuX6l#oRe&A<<^>EZCH-f@(x|`_{BnzSG$sa zNy8RF7J!up{wwK3qCSq|irxo}L(U(yCX*vc`T}>DD$STm!QBwbtDA~Q?qKeaP*G_+)pY(G`armf(PXA{VI% zBrIFds`9RNAmm%PCeu-R&hxq;< zypq0tZ1BH(sYzcL|8CHHSK?*sgzu1p{+HR|b(L>sXJLuFriS48az4NFgOGxn`oG%! z2;O_p~gRubV`t&+hb{gVgu;CDlSlZJ*NK0tvQS5%>4^nqRl@ z@*+?objNan>>VXaJT)|Q`%Ur-xTv;plvKm!cXkpzcGW`MZ!tMvcGULS-&WptQAHIt zQ|v98c6C?iL{XT2W$Di{@PvB?)onH+%FI_rO^IwW&Ki$0Q;9oWn*k- zOqqG*sikSOd1CL}fnHsk>!pd>$^g&nSv_A;f!XZ=y6Xt(B(&xAgTZA2uej-^eGGf- z0}1VF$zyojq&y!D1|kja76j#e|;!oF13iZ}IU;uw|sW zW(0{IjR+8hEx_EVgVdz?ZD-I>Wd{Xt)sdf|f^q)_ZAGUVr_xY3=A9IrQVap$B)X>G z`9gvF_G~Kx54smP5uBs|-lV(-iKMP%a|Mf z&JiZE5&4xEZ0wXKv6z;HKjvI0EzHci+Yk|3Ome?Ga0t>fxn9+C5@x;E94*8q_}+LZ zc!fBpa!4+&U!KH))WF(DM{X{dC_bK|!{gM~mFNIb3{?QY*B;Tfgj|`k<&*whdGKO( zx8kpzdTKBj3C&(BdO-PaOLM3y%S;Wb48HaAOI@?4mk=8|- zq|23y>&v2^Q1aiguoc;%vOjZYj=lDB_IN}Bqn6n!Krjes3{sLz?vmcSp#+yFgi%jtYv8LFVK!6W>xjFausb=7!= zIZ?B^18O<997}J;;q))^OcW7TZs2w|j)spJJmWt&L{VDqe;vlmWTX4%n@veoLc_JE^qm(lL-AXzWEq}dE7TyM*0_j<> z#3pWjd@cM&*7b3e0RUwV8F^^?0UfwWAoDpJp~VGqaO? zIvOe%pC%~}ZQSss7!!7$-Cs{hnuH-KirH&@C`l1;zf~^D&Eegkuv5Wt6=RGtyJ?op zfqYqXZ0P#U@YI)k)JR@pnl*Tj!x$auwUXFw&Uuz-$3gYK1!b5^5)>SO5^CG^p8QpQ z6M8@Xo^gJNL4|)@k?DSr)_JSN)u5=Hdd5UKPn&0BXAoVZhynt5UZ5xXqv(uO@#+q! zlJwJ}O!lz>o$Ta=YpKB* zsP6+#F*QxHjA9F4pV=i6v3FpjWaGHR!tyAsbGn_)P(lLUE;^k6p?0IOnqLw%37Iwt=MczFoRdb<{d2o#7Cr8q%jTRp8QYU z)dN)eY(j}m1gJSrFvrr-I2S)15+Er-BuW1_Qi_yjR1mBP5FV100VSwL7^N3nL<%1Z zXo;S5Apk?b5$85ILU5a^PgrKDEr&2+%8L$&a15T3v-e~UpbF~Nt0r>VlkBt7lLi?D z+ZEi<=G=E4kQDa+VM~bNad>2RY_pO_t+YAh+l2Y-l{Y0)d6QaGw*K_Jc(-_5?Y#QQ ze>3eU=$e=}nel^Uu@uF0GfIGUfAL`w2JJnUA^{B@WUSurLHLn(Sv2sKY>9Kk3o4@M_1lF zyKi1W^{l2H6(AyB0|Rt)RogUReHShjQfsZA3m1tH)#;L zPHmM(tn1ueiG9VPkiUOAQjFxUi#g7N?@Vk{2#4IkeVN+?b*TI)rrvK_f{pIELeFpK z(0r_lSBMn~gt@n5lNr7%4rhLri$2@%7gYQPctMf*y7~Tn$`^>n3R~67kp|pk^wmx+ zzYLC3VWu8E%E|_;?>!&+&#f~nKKpr;h1#7+;L~o})ZyXvv*E_&0?>=d_X)qI zlDt|Otf4UC=7P-AQ+YXf9NK4t%+YIyUk*5JQ{r(Qv_Jee1c`uu9nZmH@YEgbkRD0ob`E;drffbqm{D4M({n1JM)s8>)UaDdnY@R;c(gpdLa@Lzt1bf@f zo67fTLHO#!SGz5X?*A%VaSL&3kiPhwLc}F6-JJZvTZ?4a;v0-I4W`*q4XwJ-Sp=u~ zuJ{3NUl~O(HSr@HRs2J2%q6D&u$NsPBUK%X6iw#*ElS)~o_^Yq1*yQ=^cyZrvHzl@8Go|dB*sgr*mKJSf#=UHoV^jvCFsrCBe46V2xkB6f#&n{OZe;ulGv5JUmEVUOpdc zw^B5xZHP}lXS=Q0Wf5oVdG_3=%koh&W@ub{tqdv;ccTj058<&Zmv$H!l1mHTQ*+lc z8*lh1uBM?uoT9AP4mHg8(ko~M7B{>S*g@3aUgoY>!cvdly<+{5^tk0q{NIF1X4LUo znwy(b()v5dgi?Rij1BW^w%yU+@dNVKml^brX! zWU&p30~wr{Cr;}jU%U$=>0qSIgYQtb(*Lk*Jfj}`Z-TpF{eQKt^AIuH=gBJj+Axs+ z;aC)qLD*iCPRyY%p+iK|c4aSVcJY#7oqAZpHMoodYEySp@q7*JYQQy(szdr4k-8T? ziN}814jAgI+=fF0^AF(s@2F4|0g@o$5jTHE#EdU6ZMLi$_NM03roiJu2As(s^NRZQ z(SYi^>IV506-j00CanIB17QF|)#VNRGPHQzthv3$ZFzfm@XpT7@k#n~=T{&J+-U5$ z)B3lf4Zln&t$GSpxlL?3)%qY3`F?t+%D}Q)1tf)jwSHY(Qg6k(<>Xn$?G>`hqZ6&G zGKppte7Kbr;2ifkugEG0Yy61V%|$NMOV`G1Mb}P1F6a*UHA&OwvDTG@Mu`rce-WH# z(lu7p%P{{j)suR4A`{?t)AnjFk%l>SNROPaETjAB#R#40Mf7pEN|))iX~gfWMp#e* z1K>||{*CE(JE<2YB+v}^1%RJ!b%FtYOVqlNy{N_cb#-U}=F^#G?+bc*ebBakQPb+} zn)5f6+=Rf9ojOj~wkjij2Uq?ewY@p-V2dw$fT4GXn%#PWgFbH=5aPsLn7&aJ1l0@C zrYGI@jMLVxEdSrmOIRtKxhfD&E6U4JWEFyo2cjVblAalp4Z*my5kbHp5gcKp`5vU1 z)%w@(k8iu3Me~Hq{^Jw$RQ?4PQTqzWThsUVQIb|nMM_tc4Hch24wI}2TVT%$+I9G zd?e_+UZ7Ga2`p_DU{Nq0v0EFtu5*hWFa~x94rqTLAlhed7Hen*3Bmss`DYfYhutW=>%(6(Ah+3_;LTt!L zh|0j~VIH}PV6m!^XyQrGpsa+?pJ;8sk@!XHxH5}NhyJmx#>m{C-wYDUTdytiZ#Azn2bZ4J4V}1<P0uC^61*4ZlexZ`-9MkrDb0X%Nt6+4aALz#tj0%AeWl4i(lO33H`f_b=un=sQQR*d1~8McX` zjo3{yEohsd5?2Eg4J-;9o`+U*&2euyM4!zAN39W(x@;%1&G-QX)4PunFoJ{RzYT5A zF}>3zE%$QYkPtlK^N#Qaq{&`142lu80EU5NI57=R41s1d_EZIWCn&r^N?)UW&vBe2 zPZcAIf)z%?jU-Yc+Q32(N}{J48iouHaa_YICad!jfo3kjBZ?bg(qhOr!IEEFjDzPG z-GEHkt9|%qHMCa`{_F(3wJ|!`c6)MqJ@#o58^oex9vp}Z^iYp9IkK4FRq9)6CoAYX z{!-CM2*wRg&`wYkD!)r%&yuEpw~YPvbV!H0h^E$tuesE5ZSA%B`-@Z(DtBy*;31ON z3bTJ`oj&yQZ@l|CsXbXCYTc#>HCk6sCC3%A!jqCIcBrEov=AE!&wzgz$=$AE5*(hLRM}bEE|+=33VN z0$9|KcfjepO)sQ6qekTAa>#KI%FgLy(sco`!iq?Zf&K>5OGU0$-(X1%+_Z0FveZpG z0&QZ6zoe=E(RV4_07I**Q|czx^2+HwpT!t&m5KEI{S#Kf*UAg$E+J7B5)8)NAA1;} zQ#~`IR9;AD;8*waLXr-tlBBO!EUh3>>-`ai%oB8zSMqX9so+4ckYjN;;5b$qb+*#U zjzg*t<@RJVgYHhlE2uw0Q7waohx}~;`tk|?L$3k<;n4qAiThSm# z%fabC0bR?vC4yE2NE%d01p<=FQ(s_oopY^218COlfR=)*VSExMq#ey15!7IBXVG1( z|A3?gzyG#84zMfEj~#(K;!6Y>RM!*8*!c&1 z{&le~dyDvv*Bg)bskQr_{GT_RZbycQ-)2Ge$9LV!Oh z&Wyb4yDJ<`8A_>}V;)S*z&TGXdqR$;LBxDF7aT(_!T7^|b))7lju`N=)J}DoB@xg zS9+WUdWY8MUKlXIXdNbtLZT_fFGmFyzX_@A_$ZMM&RaqR?Syy!`%ore&)ukzUD z)!hDUTEmb~HQC9W?b(IbEhv|Mr@8)`NF|Weefm?*CQlGH=yQy;jh~<4!qVF zdw2y(K~h|f5Ex*&#ft-AaQQ<0i|%^Cb7K_~qV@Gs{Pu=$gEUCEsfl(cU-Tc^%@vmv zIN+}5+pa1<2xyjZ0sy4{%o1w5N*{jjL~hy9+Yg_EVq0Qo62&qRrO*qA&aI%LR{J$o zy;ktNTVV#xF23EY?m#xzX<`N!gwnF-5A&71Q3he5iuV#!sV73;^%$~+-MYFYaGBjP zeb473?5fZI@v}I~@x$-X=K%!a2Sy65nY~mR4fn+#BK=pf?S{r+HtqnElFXxed2f!J zD!2W5PE!xC&es{H-agKU8Eb8u#5RwSZyy(Ke+Iy_;ttSC=N}Dce|-a^h4abSKo^Gd z*n8DPriEh?6!6jfiVTExG!kBBJ9DuDGClX4lD@f}Et92xrEzfPD8r7CRJ^ub7FI4+ zGEPYYSkHHmb(cieqB}?nAZLGq#_Klju3MsC_y0HxsWN>flQ>LHQ;cnmBmZ|gY#tV& zlbobHs+jEj!y;70tDUpRS8cAYEh?S|94G!!aji$%^ zVTuqJJKEM9^oU;L>|ybGbn<<>L1|P*)234c`q(3p!s_oq`_lrEyKf1m@up8xW0kiR zig^f*IbR**!t47H^!h{LZS4F`KFN3H^4PPP`x!HQZ?few*r4ufQGP9#y zS5VNt$x%chleA}??5ZTM)6-=WI5jJ#Gw-Cgi41L_u7l{l_E!j}D(-1}huxQ#I6p3KEv3I8Yp>d#w3dzP8#*j3Z6SVF z_wIS;XxDmTb~J3b+g6OuisXAhAxuT@+#$^e4Fym-Y&bKKw#(y){=}z1FCPvJQTT7t za2QueQYmpo50(_xBSO zK$i{mb{?_BjnQ$-EzY6z1YVG!VSrtDx;bpn4T@M%gn>GipAwJ^8w6N9la6fJ`C9en z8tj(ay#MnBv$Q+i9A9}VaIjyVW8yF=hT!@A^^pw*_IS7b^`Z?&z=IuN@Fl?imuZvo zJ@HH8-aanQDT=#73i|Rj=H9C%Z6kj5n1Lu38RDDl@>{n5=95w_F~$GVj7~UTewR

s2s*fX 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 From d5f866cb0eb040658cf84244838c6b04a5442e0d Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 27 Jun 2023 23:47:24 -0600 Subject: [PATCH 174/437] Remove device ID from XboxDevice and re-handle arrival message --- Program/PacketParsing/Backends/PcapBackend.cs | 2 +- Program/PacketParsing/Packets/DeviceArrival.cs | 14 ++++++++++++++ Program/PacketParsing/XboxClient.cs | 16 +++++++++++++++- Program/PacketParsing/XboxDevice.cs | 8 -------- Program/RB4InstrumentMapper.csproj | 1 + 5 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 Program/PacketParsing/Packets/DeviceArrival.cs diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index d5279bd..0db953b 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -137,7 +137,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) ulong deviceId = header.DeviceId; if (!devices.TryGetValue(deviceId, out var device)) { - device = new XboxDevice(deviceId); + device = new XboxDevice(); devices.Add(deviceId, device); Console.WriteLine($"Device with ID {deviceId:X12} was connected"); } diff --git a/Program/PacketParsing/Packets/DeviceArrival.cs b/Program/PacketParsing/Packets/DeviceArrival.cs new file mode 100644 index 0000000..067edc4 --- /dev/null +++ b/Program/PacketParsing/Packets/DeviceArrival.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal readonly struct DeviceArrival + { + public readonly ulong SerialNumber; + public readonly ushort VendorId; + public readonly ushort ProductId; + private readonly ulong ignored1; + private readonly ulong ignored2; + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 1d05acc..12ea3f6 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -68,11 +68,13 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan + /// Handles the arrival message of the device. + ///

+ private unsafe XboxResult HandleArrival(ReadOnlySpan data) + { + if (data.Length < sizeof(DeviceArrival) || MemoryMarshal.TryRead(data, out DeviceArrival arrival)) + return XboxResult.InvalidMessage; + + Console.WriteLine($"New client connected with ID {arrival.SerialNumber:X12}"); + return XboxResult.Success; + } + /// /// Handles the arrival message of the device. /// diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 5669022..cd3c7a5 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -24,18 +24,11 @@ public class XboxDevice : IDisposable { public static MappingMode MapperMode; - public ulong DeviceId { get; } - /// /// The clients currently on the device. /// private readonly Dictionary clients = new Dictionary(); - public XboxDevice(ulong deviceId) - { - DeviceId = deviceId; - } - ~XboxDevice() { Dispose(false); @@ -71,7 +64,6 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) { client = new XboxClient(); clients.Add(header.Client, client); - Console.WriteLine($"Client {header.Client} connected on device with ID {DeviceId:X12}"); } var clientResult = client.HandleMessage(header, commandData); switch (clientResult) diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 30d9b3b..852feb9 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -122,6 +122,7 @@ + From e4945d8220aff8a3f420f5ad6190c9e8b1ca3ebe Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 27 Jun 2023 23:48:58 -0600 Subject: [PATCH 175/437] Add span-based byte buffer ToString helper --- Program/PacketParsing/Backends/PcapBackend.cs | 2 +- Program/PacketParsing/ParsingUtils.cs | 24 ++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 0db953b..b3462d4 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -128,7 +128,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) if (LogPackets) { string packetLogString = $"{packet.Header.Timeval.Date:yyyy-MM-dd hh:mm:ss.fff} [{packet.Data.Length}] " + - $"{BitConverter.ToString(headerData.ToArray())} | {BitConverter.ToString(packetData.ToArray())}"; + $"{ParsingUtils.ToString(headerData)} | {ParsingUtils.ToString(packetData)}"; Console.WriteLine(packetLogString); Logging.Packet_WriteLine(packetLogString); } diff --git a/Program/PacketParsing/ParsingUtils.cs b/Program/PacketParsing/ParsingUtils.cs index 297113e..48795b8 100644 --- a/Program/PacketParsing/ParsingUtils.cs +++ b/Program/PacketParsing/ParsingUtils.cs @@ -33,7 +33,7 @@ public static bool DecodeLEB128(ReadOnlySpan data, out int result, out int // Detect length sequences longer than 4 bytes if ((value & 0x80) != 0) { - Debug.WriteLine($"Variable-length value is greater than 4 bytes! Buffer: {BitConverter.ToString(data.ToArray())}"); + Debug.WriteLine($"Variable-length value is greater than 4 bytes! Buffer: {ToString(data)}"); byteLength = 0; result = 0; return false; @@ -42,6 +42,28 @@ public static bool DecodeLEB128(ReadOnlySpan data, out int result, out int return true; } + public static string ToString(ReadOnlySpan buffer) + { + const string characters = "0123456789ABCDEF"; + + if (buffer.IsEmpty) + return ""; + + Span stringBuffer = stackalloc char[buffer.Length * 3]; + for (int i = 0; i < buffer.Length; i++) + { + byte value = buffer[i]; + int stringIndex = i * 3; + stringBuffer[stringIndex] = characters[(value & 0xF0) >> 4]; + stringBuffer[stringIndex + 1] = characters[value & 0x0F]; + stringBuffer[stringIndex + 2] = '-'; + } + // Exclude last '-' + stringBuffer = stringBuffer.Slice(0, stringBuffer.Length - 1); + + return stringBuffer.ToString(); + } + /// /// Scales a byte to a short, starting from the negative end. /// From a250fec1bc119d30a060f98944ad7bb22170b452 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 27 Jun 2023 20:47:13 -0600 Subject: [PATCH 176/437] Add Nefarius.Drivers.WinUSB and Nefarius.Utilities.DeviceManagement packages --- Installer/Product.wxs | 6 ++++++ Program/RB4InstrumentMapper.csproj | 8 +++++++- Program/packages.config | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 5412969..219ab00 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -45,6 +45,12 @@ + + + + + + diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 852feb9..dd58915 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -1,4 +1,4 @@ - + @@ -57,6 +57,12 @@ true + + packages\Nefarius.Drivers.WinUSB.4.3.83\lib\net472\Nefarius.Drivers.WinUSB.dll + + + packages\Nefarius.Utilities.DeviceManagement.3.14.305\lib\netstandard2.0\Nefarius.Utilities.DeviceManagement.dll + packages\Nefarius.ViGEm.Client.1.21.256\lib\netstandard2.0\Nefarius.ViGEm.Client.dll diff --git a/Program/packages.config b/Program/packages.config index 6be5412..febb7df 100644 --- a/Program/packages.config +++ b/Program/packages.config @@ -1,5 +1,7 @@  + + From f471f8f2e27105e526712924c27a2a764ed9efc9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 27 Jun 2023 23:51:49 -0600 Subject: [PATCH 177/437] Implement basic WinUSB backend; not fully functional yet --- .../PacketParsing/Backends/WinUsbBackend.cs | 116 ++++++++++++++++++ .../Backends/XboxWinUsbDevice.cs | 108 ++++++++++++++++ Program/PacketParsing/XboxDevice.cs | 13 +- Program/RB4InstrumentMapper.csproj | 4 +- 4 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 Program/PacketParsing/Backends/WinUsbBackend.cs create mode 100644 Program/PacketParsing/Backends/XboxWinUsbDevice.cs diff --git a/Program/PacketParsing/Backends/WinUsbBackend.cs b/Program/PacketParsing/Backends/WinUsbBackend.cs new file mode 100644 index 0000000..a0b8119 --- /dev/null +++ b/Program/PacketParsing/Backends/WinUsbBackend.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using Nefarius.Drivers.WinUSB; +using Nefarius.Utilities.DeviceManagement.PnP; + +// TODO: Doesn't actually work yet, and will block if a device hasn't sent any inputs yet + +namespace RB4InstrumentMapper.Parsing +{ + public static class WinUsbBackend + { + private static readonly Thread readThread = new Thread(ReadThread) { IsBackground = true }; + private static volatile bool stopReading = false; + + private static readonly DeviceNotificationListener watcher = new DeviceNotificationListener(); + private static readonly ConcurrentDictionary devices + = new ConcurrentDictionary(); + + public static void Start() + { + foreach (var deviceInfo in USBDevice.GetDevices(DeviceInterfaceIds.UsbDevice)) + { + AddDevice(deviceInfo.DevicePath); + } + + watcher.DeviceArrived += DeviceArrived; + watcher.DeviceRemoved += DeviceRemoved; + watcher.StartListen(DeviceInterfaceIds.UsbDevice); + + readThread.Start(); + } + + public static void Stop() + { + stopReading = true; + readThread.Join(); + + watcher.StopListen(); + watcher.DeviceArrived -= DeviceArrived; + watcher.DeviceRemoved -= DeviceRemoved; + + foreach (var device in devices.Values) + { + device.Dispose(); + } + devices.Clear(); + } + + private static void DeviceArrived(DeviceEventArgs args) + { + AddDevice(args.SymLink); + } + + private static void DeviceRemoved(DeviceEventArgs args) + { + RemoveDevice(args.SymLink); + } + + private static void AddDevice(string devicePath) + { + var device = XboxWinUsbDevice.TryCreate(devicePath); + if (device == null) + return; + + devices.TryAdd(devicePath, device); + } + + private static void RemoveDevice(string devicePath) + { + if (devices.TryRemove(devicePath, out var device)) + { + device.Dispose(); + } + } + + private static void ReadThread() + { + while (!stopReading) + { + foreach (var device in devices.Values) + { + if (stopReading) + break; + + // Read continuously while a chunk sequence is going on + XboxResult result; + do + { + result = ReadPacket(device); + } + while (result == XboxResult.Pending); + } + } + } + + private static XboxResult ReadPacket(XboxWinUsbDevice device) + { + Span readBuffer = stackalloc byte[device.InputSize]; + int bytesRead = device.ReadPacket(readBuffer); + if (bytesRead < 0) + return XboxResult.InvalidMessage; + + Debug.WriteLine(ParsingUtils.ToString(readBuffer)); + var result = device.HandlePacket(readBuffer.Slice(0, bytesRead)); + switch (result) + { + case XboxResult.InvalidMessage: + Debug.WriteLine($"Invalid packet received!"); + break; + } + return result; + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs new file mode 100644 index 0000000..0ea8ebd --- /dev/null +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -0,0 +1,108 @@ +using System; +using System.Diagnostics; +using Nefarius.Drivers.WinUSB; +using Nefarius.Utilities.DeviceManagement.PnP; + +namespace RB4InstrumentMapper.Parsing +{ + internal class XboxWinUsbDevice : XboxDevice + { + private static readonly Guid WinUsbClassGuid = Guid.Parse("88BAE032-5A81-49F0-BC3D-A4FF138216D6"); + private const string XGIP_COMPATIBLE_ID = @"USB\MS_COMP_XGIP10"; + private const byte XBOX_INTERFACE_CLASS = 0xFF; // Vendor-specific + private const byte XBOX_INTERFACE_SUB_CLASS = 0x47; + private const byte XBOX_INTERFACE_PROTOCOL = 0xD0; + + private USBDevice usbDevice; + private USBInterface mainInterface; + + public int InputSize => mainInterface.InPipe.MaximumPacketSize; + + private XboxWinUsbDevice(USBDevice usb, USBInterface @interface) + { + usbDevice = usb; + mainInterface = @interface; + } + + public static XboxWinUsbDevice TryCreate(string devicePath) + { + // Only accept WinUSB devices, at least for now + var pnpDevice = PnPDevice.GetDeviceByInterfaceId(devicePath); + var classGuid = pnpDevice.GetProperty(DevicePropertyKey.Device_ClassGuid); + if (classGuid != WinUsbClassGuid) + return null; + + // Check for the Xbox One compatible ID + if (!HasCompatibleId(pnpDevice, XGIP_COMPATIBLE_ID)) + return null; + + // Open device + var usbDevice = USBDevice.GetSingleDeviceByPath(devicePath); + + // Get input data pipe + var mainInterface = FindMainInterface(usbDevice); + if (mainInterface == null) + { + usbDevice.Dispose(); + return null; + } + + return new XboxWinUsbDevice(usbDevice, mainInterface); + } + + public int ReadPacket(Span readBuffer) + { + try + { + return mainInterface.InPipe.Read(readBuffer); + } + catch (Exception ex) + { + Debug.WriteLine($"Error while reading packet: {ex}"); + return -1; + } + } + + private static bool HasCompatibleId(PnPDevice pnpDevice, string compatibleId) + { + var compatibleIds = pnpDevice.GetProperty(DevicePropertyKey.Device_CompatibleIds); + foreach (string id in compatibleIds) + { + if (id == compatibleId) + return true; + } + + return false; + } + + private static USBInterface FindMainInterface(USBDevice device) + { + foreach (var iface in device.Interfaces) + { + // Ignore non-XGIP interfaces + if (iface.ClassValue != XBOX_INTERFACE_CLASS || + iface.SubClass != XBOX_INTERFACE_SUB_CLASS || + iface.Protocol != XBOX_INTERFACE_PROTOCOL) + continue; + + // The main interface uses interrupt transfers + if (iface.InPipe?.TransferType != USBTransferType.Interrupt || + iface.OutPipe?.TransferType != USBTransferType.Interrupt) + continue; + + return iface; + } + + return null; + } + + protected override void ReleaseManagedResources() + { + base.ReleaseManagedResources(); + + usbDevice?.Dispose(); + usbDevice = null; + mainInterface = null; + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index cd3c7a5..bf0968f 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -99,10 +99,15 @@ private void Dispose(bool disposing) { if (disposing) { - foreach (var client in clients.Values) - { - client.Dispose(); - } + ReleaseManagedResources(); + } + } + + protected virtual void ReleaseManagedResources() + { + foreach (var client in clients.Values) + { + client.Dispose(); } } } diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index dd58915..4bfd6d0 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -1,4 +1,4 @@ - + @@ -115,6 +115,8 @@ + + From c3e7138d63be314ed168bb6ce812e8502771afe0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 28 Jun 2023 00:55:02 -0600 Subject: [PATCH 178/437] Move USB read thread to device class Each device gets its own thread, easiest way of doing things sensibly lol --- .../PacketParsing/Backends/WinUsbBackend.cs | 66 +++---------------- .../Backends/XboxWinUsbDevice.cs | 60 ++++++++++++++++- 2 files changed, 67 insertions(+), 59 deletions(-) diff --git a/Program/PacketParsing/Backends/WinUsbBackend.cs b/Program/PacketParsing/Backends/WinUsbBackend.cs index a0b8119..de72556 100644 --- a/Program/PacketParsing/Backends/WinUsbBackend.cs +++ b/Program/PacketParsing/Backends/WinUsbBackend.cs @@ -1,22 +1,16 @@ -using System; -using System.Collections.Concurrent; -using System.Diagnostics; -using System.Threading; +using System.Collections.Generic; using Nefarius.Drivers.WinUSB; using Nefarius.Utilities.DeviceManagement.PnP; -// TODO: Doesn't actually work yet, and will block if a device hasn't sent any inputs yet +// TODO: Doesn't actually work yet, need to send data back to the device namespace RB4InstrumentMapper.Parsing { public static class WinUsbBackend { - private static readonly Thread readThread = new Thread(ReadThread) { IsBackground = true }; - private static volatile bool stopReading = false; - private static readonly DeviceNotificationListener watcher = new DeviceNotificationListener(); - private static readonly ConcurrentDictionary devices - = new ConcurrentDictionary(); + private static readonly Dictionary devices + = new Dictionary(); public static void Start() { @@ -28,15 +22,10 @@ public static void Start() watcher.DeviceArrived += DeviceArrived; watcher.DeviceRemoved += DeviceRemoved; watcher.StartListen(DeviceInterfaceIds.UsbDevice); - - readThread.Start(); } public static void Stop() { - stopReading = true; - readThread.Join(); - watcher.StopListen(); watcher.DeviceArrived -= DeviceArrived; watcher.DeviceRemoved -= DeviceRemoved; @@ -64,53 +53,16 @@ private static void AddDevice(string devicePath) if (device == null) return; - devices.TryAdd(devicePath, device); + devices.Add(devicePath, device); } private static void RemoveDevice(string devicePath) { - if (devices.TryRemove(devicePath, out var device)) - { - device.Dispose(); - } - } - - private static void ReadThread() - { - while (!stopReading) - { - foreach (var device in devices.Values) - { - if (stopReading) - break; - - // Read continuously while a chunk sequence is going on - XboxResult result; - do - { - result = ReadPacket(device); - } - while (result == XboxResult.Pending); - } - } - } - - private static XboxResult ReadPacket(XboxWinUsbDevice device) - { - Span readBuffer = stackalloc byte[device.InputSize]; - int bytesRead = device.ReadPacket(readBuffer); - if (bytesRead < 0) - return XboxResult.InvalidMessage; + if (!devices.TryGetValue(devicePath, out var device)) + return; - Debug.WriteLine(ParsingUtils.ToString(readBuffer)); - var result = device.HandlePacket(readBuffer.Slice(0, bytesRead)); - switch (result) - { - case XboxResult.InvalidMessage: - Debug.WriteLine($"Invalid packet received!"); - break; - } - return result; + devices.Remove(devicePath); + device.Dispose(); } } } \ No newline at end of file diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index 0ea8ebd..d8d1924 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Threading; using Nefarius.Drivers.WinUSB; using Nefarius.Utilities.DeviceManagement.PnP; @@ -16,7 +17,8 @@ internal class XboxWinUsbDevice : XboxDevice private USBDevice usbDevice; private USBInterface mainInterface; - public int InputSize => mainInterface.InPipe.MaximumPacketSize; + private Thread readThread; + private volatile bool readPackets = false; private XboxWinUsbDevice(USBDevice usb, USBInterface @interface) { @@ -50,7 +52,60 @@ public static XboxWinUsbDevice TryCreate(string devicePath) return new XboxWinUsbDevice(usbDevice, mainInterface); } - public int ReadPacket(Span readBuffer) + public void StartReading() + { + if (readPackets) + return; + + readPackets = true; + readThread = new Thread(ReadThread); + readThread.Start(); + } + + public void StopReading() + { + if (!readPackets) + return; + + readPackets = false; + mainInterface.InPipe.Abort(); + readThread.Join(); + readThread = null; + } + + private void ReadThread() + { + Span readBuffer = stackalloc byte[mainInterface.InPipe.MaximumPacketSize]; + + // Number of errors after which reading will stop + const int errorThreshold = 3; + int errorCount = 0; + while (readPackets) + { + // Read packet data + int bytesRead = ReadPacket(readBuffer); + if (bytesRead < 0) + { + if (errorCount > errorThreshold) + break; + + errorCount++; + continue; + } + + // Process packet data + Debug.WriteLine(ParsingUtils.ToString(readBuffer)); + var result = HandlePacket(readBuffer.Slice(0, bytesRead)); + switch (result) + { + case XboxResult.InvalidMessage: + Debug.WriteLine($"Invalid packet received!"); + break; + } + } + } + + private int ReadPacket(Span readBuffer) { try { @@ -100,6 +155,7 @@ protected override void ReleaseManagedResources() { base.ReleaseManagedResources(); + StopReading(); usbDevice?.Dispose(); usbDevice = null; mainInterface = null; From 04383f56767052e25b01812ce45297bdeb1d217e Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 28 Jun 2023 01:42:32 -0600 Subject: [PATCH 179/437] Move LEB128 decoding to CommandHeader --- .../PacketParsing/Packets/CommandHeader.cs | 39 ++++++++++++++++++- Program/PacketParsing/ParsingUtils.cs | 34 ---------------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 0fea292..7cd866e 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Runtime.InteropServices; namespace RB4InstrumentMapper.Parsing @@ -64,7 +65,7 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o bytesRead += 3; // Message length - if (!ParsingUtils.DecodeLEB128(data.Slice(bytesRead), out int dataLength, out int byteLength)) + if (!DecodeLEB128(data.Slice(bytesRead), out int dataLength, out int byteLength)) { return false; } @@ -74,7 +75,7 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o // Chunk index/length if ((header.Flags & CommandFlags.ChunkPacket) != 0) { - if (!ParsingUtils.DecodeLEB128(data.Slice(bytesRead), out int chunkIndex, out byteLength)) + if (!DecodeLEB128(data.Slice(bytesRead), out int chunkIndex, out byteLength)) { return false; } @@ -85,5 +86,39 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o return true; } + + // https://en.wikipedia.org/wiki/LEB128 + private static bool DecodeLEB128(ReadOnlySpan data, out int result, out int byteLength) + { + byteLength = 0; + result = 0; + + if (data == null || data.Length < 1) + { + return false; + } + + // Decode variable-length length value + // Sequence length is limited to 4 bytes + byte value; + do + { + value = data[byteLength]; + result |= (value & 0x7F) << (byteLength * 7); + byteLength++; + } + while ((value & 0x80) != 0 && byteLength < sizeof(int)); + + // Detect length sequences longer than 4 bytes + if ((value & 0x80) != 0) + { + Debug.WriteLine($"Variable-length value is greater than 4 bytes! Buffer: {ParsingUtils.ToString(data)}"); + byteLength = 0; + result = 0; + return false; + } + + return true; + } } } \ No newline at end of file diff --git a/Program/PacketParsing/ParsingUtils.cs b/Program/PacketParsing/ParsingUtils.cs index 48795b8..0e9728d 100644 --- a/Program/PacketParsing/ParsingUtils.cs +++ b/Program/PacketParsing/ParsingUtils.cs @@ -8,40 +8,6 @@ namespace RB4InstrumentMapper.Parsing ///
internal static class ParsingUtils { - // https://en.wikipedia.org/wiki/LEB128 - public static bool DecodeLEB128(ReadOnlySpan data, out int result, out int byteLength) - { - byteLength = 0; - result = 0; - - if (data == null || data.Length < 1) - { - return false; - } - - // Decode variable-length length value - // Sequence length is limited to 4 bytes - byte value; - do - { - value = data[byteLength]; - result |= (value & 0x7F) << (byteLength * 7); - byteLength++; - } - while ((value & 0x80) != 0 && byteLength < sizeof(int)); - - // Detect length sequences longer than 4 bytes - if ((value & 0x80) != 0) - { - Debug.WriteLine($"Variable-length value is greater than 4 bytes! Buffer: {ToString(data)}"); - byteLength = 0; - result = 0; - return false; - } - - return true; - } - public static string ToString(ReadOnlySpan buffer) { const string characters = "0123456789ABCDEF"; From c0519a81a0114ef9f7ce93bf6ef1305b893706f5 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 28 Jun 2023 01:52:17 -0600 Subject: [PATCH 180/437] Don't compare spans to null It's just redundant lol --- Program/PacketParsing/Packets/CommandHeader.cs | 4 ++-- Program/PacketParsing/Packets/XboxDescriptor.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 7cd866e..4ba6238 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -49,7 +49,7 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o { header = default; bytesRead = 0; - if (data == null || data.Length < 4) + if (data.Length < 4) { return false; } @@ -93,7 +93,7 @@ private static bool DecodeLEB128(ReadOnlySpan data, out int result, out in byteLength = 0; result = 0; - if (data == null || data.Length < 1) + if (data.IsEmpty) { return false; } diff --git a/Program/PacketParsing/Packets/XboxDescriptor.cs b/Program/PacketParsing/Packets/XboxDescriptor.cs index cdd9282..ba6e7fb 100644 --- a/Program/PacketParsing/Packets/XboxDescriptor.cs +++ b/Program/PacketParsing/Packets/XboxDescriptor.cs @@ -47,7 +47,7 @@ public static bool Parse(ReadOnlySpan data, out XboxDescriptor descriptor) private unsafe bool Parse(ReadOnlySpan data) { - if (data == null) + if (data.IsEmpty) throw new ArgumentNullException(nameof(data)); // Descriptor header size From 58788f107bfe1f99f00e03bfbf430b8ebe81c28f Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 28 Jun 2023 02:12:26 -0600 Subject: [PATCH 181/437] Store client ID as a byte instead of an int --- Program/PacketParsing/Packets/CommandHeader.cs | 4 ++-- Program/PacketParsing/XboxDevice.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 4ba6238..3d2a38a 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -40,7 +40,7 @@ internal struct CommandHeader { public CommandId CommandId; public CommandFlags Flags; - public int Client; + public byte Client; public byte SequenceCount; public int DataLength; public int ChunkIndex; @@ -59,7 +59,7 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o { CommandId = (CommandId)data[0], Flags = (CommandFlags)(data[1] & 0xF0), - Client = data[1] & 0x0F, + Client = (byte)(data[1] & 0x0F), SequenceCount = data[2], }; bytesRead += 3; diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index bf0968f..f77b9c7 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -27,7 +27,7 @@ public class XboxDevice : IDisposable /// /// The clients currently on the device. /// - private readonly Dictionary clients = new Dictionary(); + private readonly Dictionary clients = new Dictionary(); ~XboxDevice() { From 1b179a17f8fc3d729fdc986ae1e017e04c3c4ece Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 28 Jun 2023 02:18:59 -0600 Subject: [PATCH 182/437] Implement command header encoding --- .../PacketParsing/Packets/CommandHeader.cs | 88 ++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 3d2a38a..4bedfc0 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -38,6 +38,8 @@ internal enum CommandFlags : byte [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct CommandHeader { + public const int MinimumByteLength = 4; + public CommandId CommandId; public CommandFlags Flags; public byte Client; @@ -49,7 +51,7 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o { header = default; bytesRead = 0; - if (data.Length < 4) + if (data.Length < MinimumByteLength) { return false; } @@ -62,7 +64,7 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o Client = (byte)(data[1] & 0x0F), SequenceCount = data[2], }; - bytesRead += 3; + bytesRead += MinimumByteLength - 1; // Message length if (!DecodeLEB128(data.Slice(bytesRead), out int dataLength, out int byteLength)) @@ -87,6 +89,55 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o return true; } + public bool TryWriteToBuffer(Span buffer, out int bytesWritten) + { + bytesWritten = 0; + if (buffer.Length < GetByteLength()) + return false; + + // Command info + buffer[0] = (byte)CommandId; + buffer[1] = (byte)Flags; + buffer[1] |= Client; + buffer[2] = SequenceCount; + bytesWritten += MinimumByteLength - 1; + + // Message length + if (!EncodeLEB128(buffer.Slice(bytesWritten), DataLength, out int byteLength)) + return false; + + bytesWritten += byteLength; + + // Chunk index/length + if ((Flags & CommandFlags.ChunkPacket) != 0) + { + if (!EncodeLEB128(buffer.Slice(bytesWritten), ChunkIndex, out byteLength)) + return false; + + bytesWritten += byteLength; + } + + return true; + } + + public int GetByteLength() + { + int size = MinimumByteLength - 1; + + // Data length + Span encodeBuffer = stackalloc byte[sizeof(int)]; + bool success = EncodeLEB128(encodeBuffer, DataLength, out int length); + Debug.Assert(success, "Failed to get byte length for data length!"); + size += length; + + // Chunk index + success = EncodeLEB128(encodeBuffer, ChunkIndex, out length); + Debug.Assert(success, "Failed to get byte length for chunk index!"); + size += length; + + return size; + } + // https://en.wikipedia.org/wiki/LEB128 private static bool DecodeLEB128(ReadOnlySpan data, out int result, out int byteLength) { @@ -120,5 +171,38 @@ private static bool DecodeLEB128(ReadOnlySpan data, out int result, out in return true; } + + private static bool EncodeLEB128(Span buffer, int value, out int byteLength) + { + byteLength = 0; + if (buffer.IsEmpty) + return false; + + // Encode the given value + // Sequence length is limited to 4 bytes + byte result; + do + { + result = (byte)(value & 0x7F); + if (value > 0x7F) + { + result |= 0x80; + value >>= 7; + } + + buffer[byteLength] = result; + byteLength++; + } + while (value > 0x7F && byteLength < sizeof(int)); + + // Detect values too large to encode + if (value > 0x7F) + { + Debug.WriteLine($"Value to encode ({value}) is greater than allowed!"); + return false; + } + + return true; + } } } \ No newline at end of file From a72ca05c58d703ba1d9bb612fe4a4c7de2f74f4f Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Aug 2023 01:57:17 -0600 Subject: [PATCH 183/437] Store client ID and parent device in clients --- Program/PacketParsing/XboxClient.cs | 16 ++++++++++++++++ Program/PacketParsing/XboxDevice.cs | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 12ea3f6..e31f456 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -10,16 +10,32 @@ namespace RB4InstrumentMapper.Parsing ///
internal class XboxClient : IDisposable { + /// + /// The parent device of the client. + /// + public XboxDevice Parent { get; } + /// /// The descriptor of the client. /// public XboxDescriptor Descriptor { get; private set; } + /// + /// The ID of the client. + /// + public byte ClientId { get; } + private IDeviceMapper deviceMapper; private byte[] chunkBuffer; private readonly Dictionary previousSequenceIds = new Dictionary(); + public XboxClient(XboxDevice parent, byte clientId) + { + Parent = parent; + ClientId = clientId; + } + ~XboxClient() { Dispose(false); diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index f77b9c7..360be19 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -62,7 +62,7 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) if (!clients.TryGetValue(header.Client, out var client)) { - client = new XboxClient(); + client = new XboxClient(this, header.Client); clients.Add(header.Client, client); } var clientResult = client.HandleMessage(header, commandData); From b399bbb5ad08afb48aa7c3b324c83ecd43832af9 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Aug 2023 16:43:16 -0600 Subject: [PATCH 184/437] Couple adjustments/notes to chunk handling --- Program/PacketParsing/XboxClient.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index e31f456..840b470 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -56,7 +56,7 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan chunkData) + private unsafe XboxResult ProcessPacketChunk(ref CommandHeader header, ref ReadOnlySpan chunkData) { int bufferIndex = header.ChunkIndex; @@ -137,6 +134,8 @@ private unsafe XboxResult ProcessPacketChunk(CommandHeader header, ref ReadOnlyS // Safety check if ((header.Flags & CommandFlags.ChunkStart) == 0) { + // NOTE: Older Xbox One gamepads trigger this condition during authentication + // Not really an issue since we don't handle that anyways, noting for posterity Debug.Fail("Invalid chunk sequence start! No chunk buffer exists, expected a chunk start packet"); return XboxResult.InvalidMessage; } @@ -165,6 +164,8 @@ private unsafe XboxResult ProcessPacketChunk(CommandHeader header, ref ReadOnlyS // Send off finished chunk buffer chunkData = chunkBuffer; chunkBuffer = null; + header.DataLength = chunkData.Length; + header.Flags &= ~(CommandFlags.ChunkPacket | CommandFlags.ChunkStart); return XboxResult.Success; } From e23aed69ce4ea693fc487da1fdb37eb6356a35d7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Aug 2023 17:08:02 -0600 Subject: [PATCH 185/437] Create dedicated class for chunk buffers and handle them on a command ID basis --- Program/PacketParsing/Packets/ChunkBuffer.cs | 82 ++++++++++++++++++++ Program/PacketParsing/XboxClient.cs | 79 +++---------------- Program/RB4InstrumentMapper.csproj | 1 + 3 files changed, 94 insertions(+), 68 deletions(-) create mode 100644 Program/PacketParsing/Packets/ChunkBuffer.cs diff --git a/Program/PacketParsing/Packets/ChunkBuffer.cs b/Program/PacketParsing/Packets/ChunkBuffer.cs new file mode 100644 index 0000000..1c1293d --- /dev/null +++ b/Program/PacketParsing/Packets/ChunkBuffer.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics; + +namespace RB4InstrumentMapper.Parsing +{ + internal class ChunkBuffer + { + public byte[] Buffer { get; private set; } + public int BytesUsed { get; private set; } + + public XboxResult ProcessChunk(ref CommandHeader header, ref ReadOnlySpan chunkData) + { + int bufferIndex = header.ChunkIndex; + + // Do nothing with chunks of length 0 + if (bufferIndex <= 0) + { + // Chunked packets with a length of 0 are valid and have been observed with Elite controllers + bool emptySequence = bufferIndex == 0; + Debug.Assert(emptySequence, $"Negative buffer index {bufferIndex}!"); + return emptySequence ? XboxResult.Success : XboxResult.InvalidMessage; + } + + // Start of the chunk sequence + if (Buffer == null || (header.Flags & CommandFlags.ChunkStart) != 0) + { + // Safety check + if ((header.Flags & CommandFlags.ChunkStart) == 0) + { + // NOTE: Older Xbox One gamepads trigger this condition during authentication + // Not really an issue since we don't handle that anyways, noting for posterity + Debug.Fail("Invalid chunk sequence start! No chunk buffer exists, expected a chunk start packet"); + return XboxResult.InvalidMessage; + } + + // Buffer index is the total size of the buffer on the starting packet + Buffer = new byte[bufferIndex]; + bufferIndex = 0; + BytesUsed = 0; + } + + // Buffer index equalling buffer length signals the end of the sequence + if (bufferIndex >= Buffer.Length) + { + // Safety checks + if (bufferIndex > Buffer.Length) + { + Debug.Fail("Invalid chunk sequence end! Buffer index is beyond the end of the chunk buffer"); + return XboxResult.InvalidMessage; + } + + if (chunkData.Length != 0) + { + Debug.Fail("Invalid chunk sequence end! Data was provided beyond the end of the buffer"); + return XboxResult.InvalidMessage; + } + + // Send off finished chunk buffer + chunkData = Buffer; + Buffer = null; + BytesUsed = 0; + + // Update header + header.DataLength = chunkData.Length; + header.Flags &= ~(CommandFlags.ChunkPacket | CommandFlags.ChunkStart); + return XboxResult.Success; + } + + // Verify chunk data bounds + if ((bufferIndex + chunkData.Length) > Buffer.Length) + { + Debug.Fail($"Invalid chunk sequence! Data was provided beyond the end of the buffer"); + return XboxResult.InvalidMessage; + } + + // Copy data to buffer + chunkData.CopyTo(Buffer.AsSpan(bufferIndex, chunkData.Length)); + BytesUsed = bufferIndex + chunkData.Length; + return XboxResult.Pending; + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 840b470..dfd6893 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -27,8 +27,11 @@ internal class XboxClient : IDisposable private IDeviceMapper deviceMapper; - private byte[] chunkBuffer; private readonly Dictionary previousSequenceIds = new Dictionary(); + private readonly Dictionary chunkBuffers = new Dictionary() + { + { CommandId.Descriptor, new ChunkBuffer() }, + }; public XboxClient(XboxDevice parent, byte clientId) { @@ -56,7 +59,13 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan chunkData) - { - int bufferIndex = header.ChunkIndex; - - // Do nothing with chunks of length 0 - if (bufferIndex <= 0) - { - // Chunked packets with a length of 0 are valid and have been observed with Elite controllers - bool emptySequence = bufferIndex == 0; - Debug.Assert(emptySequence, $"Negative buffer index {bufferIndex}!"); - return emptySequence ? XboxResult.Success : XboxResult.InvalidMessage; - } - - // Start of the chunk sequence - if (chunkBuffer == null || (header.Flags & CommandFlags.ChunkStart) != 0) - { - // Safety check - if ((header.Flags & CommandFlags.ChunkStart) == 0) - { - // NOTE: Older Xbox One gamepads trigger this condition during authentication - // Not really an issue since we don't handle that anyways, noting for posterity - Debug.Fail("Invalid chunk sequence start! No chunk buffer exists, expected a chunk start packet"); - return XboxResult.InvalidMessage; - } - - // Buffer index is the total size of the buffer on the starting packet - chunkBuffer = new byte[bufferIndex]; - bufferIndex = 0; - } - - // Buffer index equalling buffer length signals the end of the sequence - if (bufferIndex >= chunkBuffer.Length) - { - // Safety checks - if (bufferIndex > chunkBuffer.Length) - { - Debug.Fail("Invalid chunk sequence end! Buffer index is beyond the end of the chunk buffer"); - return XboxResult.InvalidMessage; - } - - if (chunkData.Length != 0) - { - Debug.Fail("Invalid chunk sequence end! Data was provided beyond the end of the buffer"); - return XboxResult.InvalidMessage; - } - - // Send off finished chunk buffer - chunkData = chunkBuffer; - chunkBuffer = null; - header.DataLength = chunkData.Length; - header.Flags &= ~(CommandFlags.ChunkPacket | CommandFlags.ChunkStart); - return XboxResult.Success; - } - - // Verify chunk data bounds - if ((bufferIndex + chunkData.Length) > chunkBuffer.Length) - { - Debug.Fail($"Invalid chunk sequence! Data was provided beyond the end of the buffer"); - return XboxResult.InvalidMessage; - } - - // Copy data to buffer - chunkData.CopyTo(chunkBuffer.AsSpan(bufferIndex, chunkData.Length)); - return XboxResult.Pending; - } - /// /// Handles the arrival message of the device. /// diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 4bfd6d0..116bcc6 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -129,6 +129,7 @@ + From 37df6ca9bf08c1d9c2f58f7084eae8c06f950506 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Aug 2023 18:32:43 -0600 Subject: [PATCH 186/437] Implement packet sending --- .../Backends/XboxWinUsbDevice.cs | 15 +++++ Program/PacketParsing/XboxDevice.cs | 59 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index d8d1924..c9212a7 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -21,6 +21,7 @@ internal class XboxWinUsbDevice : XboxDevice private volatile bool readPackets = false; private XboxWinUsbDevice(USBDevice usb, USBInterface @interface) + : base(@interface.OutPipe.MaximumPacketSize) { usbDevice = usb; mainInterface = @interface; @@ -118,6 +119,20 @@ private int ReadPacket(Span readBuffer) } } + protected override XboxResult SendPacket(Span data) + { + try + { + mainInterface.InPipe.Write(data); + return XboxResult.Success; + } + catch (Exception ex) + { + Debug.WriteLine($"Error while sending packet: {ex}"); + return XboxResult.InvalidMessage; + } + } + private static bool HasCompatibleId(PnPDevice pnpDevice, string compatibleId) { var compatibleIds = pnpDevice.GetProperty(DevicePropertyKey.Device_CompatibleIds); diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 360be19..62a3f7b 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; namespace RB4InstrumentMapper.Parsing { @@ -29,6 +31,17 @@ public class XboxDevice : IDisposable ///
private readonly Dictionary clients = new Dictionary(); + private readonly int maxPacketSize; + + public XboxDevice() : this(maxPacketSize: 0) + { + } + + protected XboxDevice(int maxPacketSize) + { + this.maxPacketSize = maxPacketSize; + } + ~XboxDevice() { Dispose(false); @@ -86,6 +99,52 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) return XboxResult.Success; } + internal unsafe XboxResult SendMessage(CommandHeader header, ref T data) + where T : unmanaged + { + // Create a byte buffer for the given data + var writeBuffer = new Span(Unsafe.AsPointer(ref data), sizeof(T)); + return SendMessage(header, writeBuffer); + } + + // TODO: Span instead of ReadOnlySpan since the WinUSB lib doesn't use ReadOnlySpan for writing atm + internal XboxResult SendMessage(CommandHeader header, Span data) + { + // For devices handled by Pcap and not over USB + if (maxPacketSize < CommandHeader.MinimumByteLength) + return XboxResult.Success; + + // Initialize lengths + header.DataLength = data.Length; + header.ChunkIndex = 0; + int packetLength = header.GetByteLength() + data.Length; + + // Chunked messages + if (packetLength > maxPacketSize) + { + // Sending chunked messages isn't supported currently, as we never need to send one + Debug.Fail($"Message is too long! Max packet length: {maxPacketSize}, message size: {packetLength}"); + return XboxResult.InvalidMessage; + } + + // Create buffer and send it + Span packetBuffer = stackalloc byte[packetLength]; + if (!header.TryWriteToBuffer(packetBuffer, out int bytesWritten) || + !data.TryCopyTo(packetBuffer.Slice(bytesWritten))) + { + Debug.Fail("Failed to create packet buffer!"); + return XboxResult.InvalidMessage; + } + + return SendPacket(packetBuffer); + } + + protected virtual XboxResult SendPacket(Span data) + { + // No-op by default, for Pcap + return XboxResult.Success; + } + /// /// Performs cleanup for the device. /// From 83a9b57f49cbbefae7c4d5025b63e4af8b734c21 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Aug 2023 18:34:58 -0600 Subject: [PATCH 187/437] Fix chunk index always being included in header byte length --- Program/PacketParsing/Packets/CommandHeader.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 4bedfc0..4caedf0 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -2,6 +2,8 @@ using System.Diagnostics; using System.Runtime.InteropServices; +// TODO: Chunk headers need a minimum length of 6 bytes + namespace RB4InstrumentMapper.Parsing { /// @@ -131,9 +133,12 @@ public int GetByteLength() size += length; // Chunk index - success = EncodeLEB128(encodeBuffer, ChunkIndex, out length); - Debug.Assert(success, "Failed to get byte length for chunk index!"); - size += length; + if ((Flags & CommandFlags.ChunkPacket) != 0) + { + success = EncodeLEB128(encodeBuffer, ChunkIndex, out length); + Debug.Assert(success, "Failed to get byte length for chunk index!"); + size += length; + } return size; } From cb0997b9667ba63a2c6c1a4b8c174949ae3ada17 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Aug 2023 19:05:49 -0600 Subject: [PATCH 188/437] Refactor command header client/flags retrieval --- .../PacketParsing/Packets/CommandHeader.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 4caedf0..33d5fd9 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -43,12 +43,23 @@ internal struct CommandHeader public const int MinimumByteLength = 4; public CommandId CommandId; - public CommandFlags Flags; - public byte Client; + public byte Flags_Client; public byte SequenceCount; public int DataLength; public int ChunkIndex; + public CommandFlags Flags + { + get => (CommandFlags)(Flags_Client & 0xF0); + set => Flags_Client = (byte)((byte)value & 0xF0 | Client); + } + + public byte Client + { + get => (byte)(Flags_Client & 0x0F); + set => Flags_Client = (byte)((byte)Flags | value & 0x0F); + } + public static bool TryParse(ReadOnlySpan data, out CommandHeader header, out int bytesRead) { header = default; @@ -62,8 +73,7 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o header = new CommandHeader() { CommandId = (CommandId)data[0], - Flags = (CommandFlags)(data[1] & 0xF0), - Client = (byte)(data[1] & 0x0F), + Flags_Client = data[1], SequenceCount = data[2], }; bytesRead += MinimumByteLength - 1; @@ -99,8 +109,7 @@ public bool TryWriteToBuffer(Span buffer, out int bytesWritten) // Command info buffer[0] = (byte)CommandId; - buffer[1] = (byte)Flags; - buffer[1] |= Client; + buffer[1] = Flags_Client; buffer[2] = SequenceCount; bytesWritten += MinimumByteLength - 1; From 01337b1a38ae7b5b8585d3d5e74994ea49de62ab Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Aug 2023 20:36:08 -0600 Subject: [PATCH 189/437] Handle acknowledge flag --- .../PacketParsing/Packets/Acknowledgement.cs | 64 +++++++++++++++++++ Program/PacketParsing/Packets/ChunkBuffer.cs | 1 + Program/PacketParsing/XboxClient.cs | 43 +++++++++---- Program/RB4InstrumentMapper.csproj | 1 + 4 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 Program/PacketParsing/Packets/Acknowledgement.cs diff --git a/Program/PacketParsing/Packets/Acknowledgement.cs b/Program/PacketParsing/Packets/Acknowledgement.cs new file mode 100644 index 0000000..67140f8 --- /dev/null +++ b/Program/PacketParsing/Packets/Acknowledgement.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct Acknowledgement + { + public byte unk1; + public CommandId InnerCommand; + public byte InnerFlags_Client; + public ushort BytesReceived; + public ushort unk2; + public ushort RemainingBuffer; + + public CommandFlags InnerFlags + { + get => (CommandFlags)(InnerFlags_Client & 0xF0); + set => InnerFlags_Client = (byte)((byte)value & 0xF0 | InnerClient); + } + + public byte InnerClient + { + get => (byte)(InnerFlags_Client & 0x0F); + set => InnerFlags_Client = (byte)((byte)InnerFlags | value & 0x0F); + } + + public static (CommandHeader header, Acknowledgement acknowledge) FromMessage(CommandHeader header, + ReadOnlySpan messageBuffer) + { + // The Xbox One driver seems to always send this for the inner flag + header.Flags = CommandFlags.SystemCommand; + + // Set acknowledgement data + var acknowledge = new Acknowledgement() + { + unk1 = 0, + InnerCommand = header.CommandId, + InnerFlags_Client = header.Flags_Client, + unk2 = 0, + BytesReceived = (ushort)messageBuffer.Length, + }; + + // Set remaining header data (length is set when sending) + header.CommandId = CommandId.Acknowledgement; + + return (header, acknowledge); + } + + public static (CommandHeader header, Acknowledgement acknowledge) FromMessage(CommandHeader header, + ReadOnlySpan messageBuffer, ChunkBuffer chunkBuffer) + { + var pair = FromMessage(header, messageBuffer); + + if (chunkBuffer.Buffer != null) + { + pair.acknowledge.BytesReceived = (ushort)chunkBuffer.BytesUsed; + pair.acknowledge.RemainingBuffer = (ushort)chunkBuffer.BytesRemaining; + } + + return pair; + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/ChunkBuffer.cs b/Program/PacketParsing/Packets/ChunkBuffer.cs index 1c1293d..4c31f16 100644 --- a/Program/PacketParsing/Packets/ChunkBuffer.cs +++ b/Program/PacketParsing/Packets/ChunkBuffer.cs @@ -7,6 +7,7 @@ internal class ChunkBuffer { public byte[] Buffer { get; private set; } public int BytesUsed { get; private set; } + public int BytesRemaining => Buffer != null ? Buffer.Length - BytesUsed : 0; public XboxResult ProcessChunk(ref CommandHeader header, ref ReadOnlySpan chunkData) { diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index dfd6893..187b13f 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -56,23 +56,40 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan + From cc805473047846ddfada6226be2bfbe4dc2337dc Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Aug 2023 21:09:11 -0600 Subject: [PATCH 190/437] Respect system command flag --- Program/PacketParsing/XboxClient.cs | 48 ++++++++++++++++------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 187b13f..539f951 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -103,14 +103,31 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan commandData) + { + switch (commandId) + { case CommandId.Arrival: return HandleArrival(commandData); @@ -120,22 +137,9 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan Date: Mon, 21 Aug 2023 21:15:38 -0600 Subject: [PATCH 191/437] Refactor handling of keystrokes --- .../PacketParsing/Mappers/IDeviceMapper.cs | 2 ++ Program/PacketParsing/Mappers/VigemMapper.cs | 28 ++++--------------- Program/PacketParsing/Mappers/VjoyMapper.cs | 28 ++++--------------- Program/PacketParsing/Packets/Keystroke.cs | 8 +++--- Program/PacketParsing/XboxClient.cs | 17 ++++++++++- 5 files changed, 32 insertions(+), 51 deletions(-) diff --git a/Program/PacketParsing/Mappers/IDeviceMapper.cs b/Program/PacketParsing/Mappers/IDeviceMapper.cs index 7e17e6b..d53ae63 100644 --- a/Program/PacketParsing/Mappers/IDeviceMapper.cs +++ b/Program/PacketParsing/Mappers/IDeviceMapper.cs @@ -16,5 +16,7 @@ internal interface IDeviceMapper : IDisposable /// Handles an incoming packet. /// XboxResult HandlePacket(CommandId command, ReadOnlySpan data); + + XboxResult HandleKeystroke(Keystroke key); } } \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/VigemMapper.cs b/Program/PacketParsing/Mappers/VigemMapper.cs index c69a84a..0e70ea3 100644 --- a/Program/PacketParsing/Mappers/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/VigemMapper.cs @@ -65,35 +65,17 @@ public XboxResult HandlePacket(CommandId command, ReadOnlySpan data) if (!deviceConnected) return XboxResult.Pending; - switch (command) - { - case CommandId.Keystroke: - return HandleKeystroke(data); - - default: - return OnPacketReceived(command, data); - } + return OnPacketReceived(command, data); } protected abstract XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data); - private unsafe XboxResult HandleKeystroke(ReadOnlySpan data) + public XboxResult HandleKeystroke(Keystroke key) { - if (!MapGuideButton) - return XboxResult.Success; - - if (data.Length < sizeof(Keystroke)) - return XboxResult.InvalidMessage; - - // Multiple keystrokes can be sent in a single message - var keys = MemoryMarshal.Cast(data); - foreach (var key in keys) + if (key.Keycode == KeyCode.LeftWindows && MapGuideButton) { - if ((KeyCode)key.Keycode == KeyCode.LeftWindows) - { - device.SetButtonState(Xbox360Button.Guide, key.Pressed); - device.SubmitReport(); - } + device.SetButtonState(Xbox360Button.Guide, key.Pressed); + device.SubmitReport(); } return XboxResult.Success; diff --git a/Program/PacketParsing/Mappers/VjoyMapper.cs b/Program/PacketParsing/Mappers/VjoyMapper.cs index 65afc0a..6fc0c48 100644 --- a/Program/PacketParsing/Mappers/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/VjoyMapper.cs @@ -48,35 +48,17 @@ public XboxResult HandlePacket(CommandId command, ReadOnlySpan data) if (deviceId == 0) throw new ObjectDisposedException("this"); - switch (command) - { - case CommandId.Keystroke: - return HandleKeystroke(data); - - default: - return OnPacketReceived(command, data); - } + return OnPacketReceived(command, data); } protected abstract XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data); - private unsafe XboxResult HandleKeystroke(ReadOnlySpan data) + public XboxResult HandleKeystroke(Keystroke key) { - if (!MapGuideButton) - return XboxResult.Success; - - if (data.Length < sizeof(Keystroke)) - return XboxResult.InvalidMessage; - - // Multiple keystrokes can be sent in a single message - var keys = MemoryMarshal.Cast(data); - foreach (var key in keys) + if (key.Keycode == KeyCode.LeftWindows && MapGuideButton) { - if ((KeyCode)key.Keycode == KeyCode.LeftWindows) - { - state.SetButton(VjoyButton.Fourteen, key.Pressed); - VjoyClient.UpdateDevice(deviceId, ref state); - } + state.SetButton(VjoyButton.Fourteen, key.Pressed); + VjoyClient.UpdateDevice(deviceId, ref state); } return XboxResult.Success; diff --git a/Program/PacketParsing/Packets/Keystroke.cs b/Program/PacketParsing/Packets/Keystroke.cs index e4ac59a..ad3e4ed 100644 --- a/Program/PacketParsing/Packets/Keystroke.cs +++ b/Program/PacketParsing/Packets/Keystroke.cs @@ -2,7 +2,7 @@ namespace RB4InstrumentMapper.Parsing { - internal enum KeystrokeFlags + internal enum KeystrokeFlags : byte { Pressed = 0x01, } @@ -15,9 +15,9 @@ public enum KeyCode : byte [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct Keystroke { - public byte Flags; - public byte Keycode; + public KeystrokeFlags Flags; + public KeyCode Keycode; - public bool Pressed => (Flags & (byte)KeystrokeFlags.Pressed) != 0; + public bool Pressed => (Flags & KeystrokeFlags.Pressed) != 0; } } \ No newline at end of file diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 539f951..5fe7216 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -139,7 +139,7 @@ private XboxResult HandleSystemCommand(CommandId commandId, ReadOnlySpan c // Keystrokes are handled by the mapper case CommandId.Keystroke: - return deviceMapper.HandlePacket(commandId, commandData); + return HandleKeystroke(commandData); } return XboxResult.Success; @@ -187,6 +187,21 @@ private XboxResult HandleDescriptor(ReadOnlySpan data) return XboxResult.Success; } + private unsafe XboxResult HandleKeystroke(ReadOnlySpan data) + { + if (data.Length % sizeof(Keystroke) != 0) + return XboxResult.InvalidMessage; + + // Multiple keystrokes can be sent in a single message + var keys = MemoryMarshal.Cast(data); + foreach (var key in keys) + { + deviceMapper.HandleKeystroke(key); + } + + return XboxResult.Success; + } + public void Dispose() { Dispose(true); From 9db468ec9bbac4959528a3f6798d24b86ea75f6b Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 21 Aug 2023 21:17:20 -0600 Subject: [PATCH 192/437] Fix sequence ID handling --- Program/PacketParsing/XboxClient.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 5fe7216..277157f 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -95,13 +95,11 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan Date: Tue, 22 Aug 2023 01:54:25 -0600 Subject: [PATCH 193/437] Client side of message sending --- Program/PacketParsing/XboxClient.cs | 40 ++++++++++++++++++++++++++--- Program/PacketParsing/XboxDevice.cs | 5 ++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 277157f..b6fca2a 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -27,7 +27,8 @@ internal class XboxClient : IDisposable private IDeviceMapper deviceMapper; - private readonly Dictionary previousSequenceIds = new Dictionary(); + private readonly Dictionary previousReceiveSequence = new Dictionary(); + private readonly Dictionary previousSendSequence = new Dictionary(); private readonly Dictionary chunkBuffers = new Dictionary() { { CommandId.Descriptor, new ChunkBuffer() }, @@ -88,18 +89,18 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan data) return XboxResult.Success; } + internal unsafe XboxResult SendMessage(CommandHeader header) + { + SetUpHeader(ref header); + return Parent.SendMessage(header); + } + + internal unsafe XboxResult SendMessage(CommandHeader header, ref T data) + where T : unmanaged + { + SetUpHeader(ref header); + return Parent.SendMessage(header, ref data); + } + + internal XboxResult SendMessage(CommandHeader header, Span data) + { + SetUpHeader(ref header); + return Parent.SendMessage(header, data); + } + + private void SetUpHeader(ref CommandHeader header) + { + header.Client = ClientId; + + if (!previousSendSequence.TryGetValue(header.CommandId, out byte sequence) || + sequence == 0xFF) // Sequence IDs of 0 are not valid + sequence = 0; + + header.SequenceCount = ++sequence; + previousSendSequence[header.CommandId] = sequence; + } + public void Dispose() { Dispose(true); diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 62a3f7b..a29ddbd 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -99,6 +99,11 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) return XboxResult.Success; } + internal unsafe XboxResult SendMessage(CommandHeader header) + { + return SendMessage(header, Span.Empty); + } + internal unsafe XboxResult SendMessage(CommandHeader header, ref T data) where T : unmanaged { From 79937191a4beec7f767e1ed7a3a2fc82a506940a Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 02:07:58 -0600 Subject: [PATCH 194/437] Add XboxMessage container for constant messages --- Program/PacketParsing/Packets/XboxMessage.cs | 45 ++++++++++++++++++++ Program/PacketParsing/XboxClient.cs | 11 +++++ Program/PacketParsing/XboxDevice.cs | 11 +++++ Program/RB4InstrumentMapper.csproj | 3 +- 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 Program/PacketParsing/Packets/XboxMessage.cs diff --git a/Program/PacketParsing/Packets/XboxMessage.cs b/Program/PacketParsing/Packets/XboxMessage.cs new file mode 100644 index 0000000..e0fa191 --- /dev/null +++ b/Program/PacketParsing/Packets/XboxMessage.cs @@ -0,0 +1,45 @@ +namespace RB4InstrumentMapper.Parsing +{ + internal class XboxMessage + { + private CommandHeader _header; + private byte[] _data; + + public CommandHeader Header + { + get => _header; + set + { + _header = value; + _header.DataLength = _data?.Length ?? 0; + } + } + + public byte[] Data + { + get => _data; + set + { + _data = value; + _header.DataLength = _data?.Length ?? 0; + } + } + } + + internal unsafe class XboxMessage + where TData : unmanaged + { + private CommandHeader _header; + public TData Data; + + public CommandHeader Header + { + get => _header; + set + { + _header = value; + _header.DataLength = sizeof(TData); + } + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index b6fca2a..85ba70b 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -201,6 +201,17 @@ private unsafe XboxResult HandleKeystroke(ReadOnlySpan data) return XboxResult.Success; } + internal unsafe XboxResult SendMessage(XboxMessage message) + { + return SendMessage(message.Header, message.Data); + } + + internal unsafe XboxResult SendMessage(XboxMessage message) + where T : unmanaged + { + return SendMessage(message.Header, ref message.Data); + } + internal unsafe XboxResult SendMessage(CommandHeader header) { SetUpHeader(ref header); diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index a29ddbd..3764e5d 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -99,6 +99,17 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) return XboxResult.Success; } + internal unsafe XboxResult SendMessage(XboxMessage message) + { + return SendMessage(message.Header, message.Data); + } + + internal unsafe XboxResult SendMessage(XboxMessage message) + where T : unmanaged + { + return SendMessage(message.Header, ref message.Data); + } + internal unsafe XboxResult SendMessage(CommandHeader header) { return SendMessage(header, Span.Empty); diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 5c8b155..dfaec95 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -1,4 +1,4 @@ - + @@ -139,6 +139,7 @@ + From b71bb7c3cd800ff0043a3d8fa90d854a9bbb901b Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 02:25:10 -0600 Subject: [PATCH 195/437] Retry a few times on failed packet sends --- Program/PacketParsing/XboxDevice.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 3764e5d..2787d5e 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -152,7 +152,17 @@ internal XboxResult SendMessage(CommandHeader header, Span data) return XboxResult.InvalidMessage; } - return SendPacket(packetBuffer); + // Attempt a few times + const int retryThreshold = 3; + int retryCount = 0; + XboxResult result; + do + { + result = SendPacket(packetBuffer); + } + while (++retryCount < retryThreshold && result != XboxResult.Success); + + return result; } protected virtual XboxResult SendPacket(Span data) From b4e94565068d67ea7d20bbe11af1cbeae243e248 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 03:34:57 -0600 Subject: [PATCH 196/437] Reorganize/refactor packet definitions and add missing doc comments --- Program/PacketParsing/Packets/DrumInput.cs | 59 ------------------- .../PacketParsing/Packets/Drums/DrumInput.cs | 38 ++++++++++++ .../Packets/{ => Gamepad}/GamepadInput.cs | 4 +- .../Packets/{ => Guitar}/GuitarInput.cs | 0 .../Packets/{ => System}/Acknowledgement.cs | 10 +++- .../Packets/{ => System}/DeviceArrival.cs | 3 + .../Packets/{ => System}/DeviceStatus.cs | 9 +++ .../Packets/{ => System}/Keystroke.cs | 12 ++++ .../Packets/{ => System}/XboxDescriptor.cs | 14 +++-- Program/RB4InstrumentMapper.csproj | 16 ++--- 10 files changed, 88 insertions(+), 77 deletions(-) delete mode 100644 Program/PacketParsing/Packets/DrumInput.cs create mode 100644 Program/PacketParsing/Packets/Drums/DrumInput.cs rename Program/PacketParsing/Packets/{ => Gamepad}/GamepadInput.cs (96%) rename Program/PacketParsing/Packets/{ => Guitar}/GuitarInput.cs (100%) rename Program/PacketParsing/Packets/{ => System}/Acknowledgement.cs (88%) rename Program/PacketParsing/Packets/{ => System}/DeviceArrival.cs (76%) rename Program/PacketParsing/Packets/{ => System}/DeviceStatus.cs (70%) rename Program/PacketParsing/Packets/{ => System}/Keystroke.cs (58%) rename Program/PacketParsing/Packets/{ => System}/XboxDescriptor.cs (97%) diff --git a/Program/PacketParsing/Packets/DrumInput.cs b/Program/PacketParsing/Packets/DrumInput.cs deleted file mode 100644 index beb5257..0000000 --- a/Program/PacketParsing/Packets/DrumInput.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace RB4InstrumentMapper.Parsing -{ - /// - /// Re-definitions for button flags that have specific meanings. - /// - [Flags] - internal enum DrumButton : ushort - { - // Not used as these are for menu navigation purposes - // RedPad = GamepadButton.B, - // GreenPad = GamepadButton.A, - KickOne = GamepadButton.LeftBumper, - KickTwo = GamepadButton.RightBumper - } - - /// - /// An input report from a drumkit. - /// - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct DrumInput - { - /// - /// Masks for each pad's value. - /// - enum DrumPad : ushort - { - Red = 0x00F0, - Yellow = 0x000F, - Blue = 0xF000, - Green = 0x0F00 - } - - /// - /// Masks for each cymbal's value. - /// - enum DrumCymbal : ushort - { - Yellow = 0x00F0, - Blue = 0x000F, - Green = 0xF000 - } - - public ushort Buttons; - private readonly ushort pads; - private readonly ushort cymbals; - - public byte RedPad => (byte)((pads & (ushort)DrumPad.Red) >> 4); - public byte YellowPad => (byte)(pads & (ushort)DrumPad.Yellow); - public byte BluePad => (byte)((pads & (ushort)DrumPad.Blue) >> 12); - public byte GreenPad => (byte)((pads & (ushort)DrumPad.Green) >> 8); - - public byte YellowCymbal => (byte)((cymbals & (ushort)DrumCymbal.Yellow) >> 4); - public byte BlueCymbal => (byte)(cymbals & (ushort)DrumCymbal.Blue); - public byte GreenCymbal => (byte)((cymbals & (ushort)DrumCymbal.Green) >> 12); - } -} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/Drums/DrumInput.cs b/Program/PacketParsing/Packets/Drums/DrumInput.cs new file mode 100644 index 0000000..dbf648a --- /dev/null +++ b/Program/PacketParsing/Packets/Drums/DrumInput.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Re-definitions for button flags that have specific meanings. + /// + [Flags] + internal enum DrumButton : ushort + { + // Not used as these are for menu navigation purposes + // RedPad = GamepadButton.B, + // GreenPad = GamepadButton.A, + KickOne = GamepadButton.LeftBumper, + KickTwo = GamepadButton.RightBumper + } + + /// + /// An input report from a drumkit. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct DrumInput + { + public ushort Buttons; + private readonly ushort pads; + private readonly ushort cymbals; + + public byte RedPad => (byte)((pads & 0x00F0) >> 4); + public byte YellowPad => (byte)(pads & 0x000F); + public byte BluePad => (byte)((pads & 0xF000) >> 12); + public byte GreenPad => (byte)((pads & 0x0F00) >> 8); + + public byte YellowCymbal => (byte)((cymbals & 0x00F0) >> 4); + public byte BlueCymbal => (byte)(cymbals & 0x000F); + public byte GreenCymbal => (byte)((cymbals & 0xF000) >> 12); + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/GamepadInput.cs b/Program/PacketParsing/Packets/Gamepad/GamepadInput.cs similarity index 96% rename from Program/PacketParsing/Packets/GamepadInput.cs rename to Program/PacketParsing/Packets/Gamepad/GamepadInput.cs index 88a1b53..095d621 100644 --- a/Program/PacketParsing/Packets/GamepadInput.cs +++ b/Program/PacketParsing/Packets/Gamepad/GamepadInput.cs @@ -10,7 +10,7 @@ namespace RB4InstrumentMapper.Parsing internal enum GamepadButton : ushort { Sync = 0x0001, - Unused = 0x0002, + // Unused = 0x0002, Menu = 0x0004, Options = 0x0008, A = 0x0010, @@ -30,7 +30,7 @@ internal enum GamepadButton : ushort #if DEBUG /// - /// An input report from a drumkit. + /// An input report from a gamepad. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct GamepadInput diff --git a/Program/PacketParsing/Packets/GuitarInput.cs b/Program/PacketParsing/Packets/Guitar/GuitarInput.cs similarity index 100% rename from Program/PacketParsing/Packets/GuitarInput.cs rename to Program/PacketParsing/Packets/Guitar/GuitarInput.cs diff --git a/Program/PacketParsing/Packets/Acknowledgement.cs b/Program/PacketParsing/Packets/System/Acknowledgement.cs similarity index 88% rename from Program/PacketParsing/Packets/Acknowledgement.cs rename to Program/PacketParsing/Packets/System/Acknowledgement.cs index 67140f8..56e45f8 100644 --- a/Program/PacketParsing/Packets/Acknowledgement.cs +++ b/Program/PacketParsing/Packets/System/Acknowledgement.cs @@ -3,14 +3,20 @@ namespace RB4InstrumentMapper.Parsing { + /// + /// Acknowledges a prior command and provides info about current buffer allocations. + /// + /// + /// Used for communication reliability and error detection. + /// [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct Acknowledgement { - public byte unk1; + private byte unk1; public CommandId InnerCommand; public byte InnerFlags_Client; public ushort BytesReceived; - public ushort unk2; + private ushort unk2; public ushort RemainingBuffer; public CommandFlags InnerFlags diff --git a/Program/PacketParsing/Packets/DeviceArrival.cs b/Program/PacketParsing/Packets/System/DeviceArrival.cs similarity index 76% rename from Program/PacketParsing/Packets/DeviceArrival.cs rename to Program/PacketParsing/Packets/System/DeviceArrival.cs index 067edc4..ec7ac36 100644 --- a/Program/PacketParsing/Packets/DeviceArrival.cs +++ b/Program/PacketParsing/Packets/System/DeviceArrival.cs @@ -2,6 +2,9 @@ namespace RB4InstrumentMapper.Parsing { + /// + /// Indicates that a new device has connected and is awaiting initialization. + /// [StructLayout(LayoutKind.Sequential, Pack = 1)] internal readonly struct DeviceArrival { diff --git a/Program/PacketParsing/Packets/DeviceStatus.cs b/Program/PacketParsing/Packets/System/DeviceStatus.cs similarity index 70% rename from Program/PacketParsing/Packets/DeviceStatus.cs rename to Program/PacketParsing/Packets/System/DeviceStatus.cs index e2cbdc2..82fe32f 100644 --- a/Program/PacketParsing/Packets/DeviceStatus.cs +++ b/Program/PacketParsing/Packets/System/DeviceStatus.cs @@ -2,6 +2,9 @@ namespace RB4InstrumentMapper.Parsing { + /// + /// Available types of batteries that can be used on a controller. + /// internal enum BatteryType : byte { Wired = 0, @@ -9,6 +12,9 @@ internal enum BatteryType : byte ChargeKit = 2, } + /// + /// The amount of battery remaining on the controller. + /// internal enum BatteryLevel : byte { Low = 0, @@ -19,6 +25,9 @@ internal enum BatteryLevel : byte Wired = Low, } + /// + /// Provides information about a device's current status, such as battery type and level. + /// [StructLayout(LayoutKind.Sequential, Pack = 1)] internal readonly struct DeviceStatus { diff --git a/Program/PacketParsing/Packets/Keystroke.cs b/Program/PacketParsing/Packets/System/Keystroke.cs similarity index 58% rename from Program/PacketParsing/Packets/Keystroke.cs rename to Program/PacketParsing/Packets/System/Keystroke.cs index ad3e4ed..2411838 100644 --- a/Program/PacketParsing/Packets/Keystroke.cs +++ b/Program/PacketParsing/Packets/System/Keystroke.cs @@ -2,16 +2,28 @@ namespace RB4InstrumentMapper.Parsing { + /// + /// Flags for keystroke events. + /// internal enum KeystrokeFlags : byte { Pressed = 0x01, } + /// + /// Possible key codes. + /// + /// + /// These mirror those in the Win32 API; for brevity, only the ones used are defined here. + /// public enum KeyCode : byte { LeftWindows = 0x5B, // Used for the guide button } + /// + /// A keystroke event from a controller. + /// [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct Keystroke { diff --git a/Program/PacketParsing/Packets/XboxDescriptor.cs b/Program/PacketParsing/Packets/System/XboxDescriptor.cs similarity index 97% rename from Program/PacketParsing/Packets/XboxDescriptor.cs rename to Program/PacketParsing/Packets/System/XboxDescriptor.cs index ba6e7fb..1d95bc9 100644 --- a/Program/PacketParsing/Packets/XboxDescriptor.cs +++ b/Program/PacketParsing/Packets/System/XboxDescriptor.cs @@ -7,16 +7,18 @@ namespace RB4InstrumentMapper.Parsing { /// /// The descriptor data of an Xbox One device. - /// A large amount of the descriptor data is ignored, only data necessary for identifying device types is read. /// + /// + /// A large amount of the descriptor data is ignored, only data necessary for identifying device types is read. + /// public class XboxDescriptor { [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct Header { public ushort HeaderLength; - public int unk1; - public ulong unk2; + private int unk1; + private ulong unk2; public ushort DataLength; } @@ -31,9 +33,9 @@ private struct Offsets public ushort ClassNames; public ushort InterfaceGuids; public ushort HidDescriptor; - public ushort unk1; - public ushort unk2; - public ushort unk3; + private ushort unk1; + private ushort unk2; + private ushort unk3; } public IReadOnlyList ClassNames { get; private set; } diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index dfaec95..5805178 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -129,16 +129,16 @@ - + + + + + + + + - - - - - - - From 66afaa40c185629bfeba14400c38014b6f11054a Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 03:48:30 -0600 Subject: [PATCH 197/437] Refactor command ID definitions --- .../PacketParsing/Mappers/DrumsVigemMapper.cs | 4 ++-- .../PacketParsing/Mappers/DrumsVjoyMapper.cs | 4 ++-- .../Mappers/FallbackVigemMapper.cs | 9 +++++++-- .../Mappers/FallbackVjoyMapper.cs | 9 +++++++-- .../Mappers/GamepadVigemMapper.cs | 4 ++-- .../Mappers/GamepadVjoyMapper.cs | 4 ++-- .../Mappers/GuitarVigemMapper.cs | 4 ++-- .../PacketParsing/Mappers/GuitarVjoyMapper.cs | 4 ++-- .../PacketParsing/Mappers/IDeviceMapper.cs | 2 +- Program/PacketParsing/Mappers/VigemMapper.cs | 4 ++-- Program/PacketParsing/Mappers/VjoyMapper.cs | 4 ++-- .../PacketParsing/Packets/CommandHeader.cs | 6 +++--- .../PacketParsing/Packets/Drums/DrumInput.cs | 2 ++ .../Packets/Gamepad/GamepadInput.cs | 2 ++ .../Packets/Guitar/GuitarInput.cs | 2 ++ .../Packets/System/Acknowledgement.cs | 6 ++++-- .../Packets/System/DeviceArrival.cs | 2 ++ .../Packets/System/DeviceStatus.cs | 2 ++ .../PacketParsing/Packets/System/Keystroke.cs | 2 ++ .../Packets/System/XboxDescriptor.cs | 3 +++ Program/PacketParsing/XboxClient.cs | 19 +++++++++---------- 21 files changed, 62 insertions(+), 36 deletions(-) diff --git a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs index 0bcd3d6..ee030f0 100644 --- a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs +++ b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs @@ -17,11 +17,11 @@ public DrumsVigemMapper() : base() /// /// Handles an incoming packet. /// - protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) { switch (command) { - case CommandId.Input: + case DrumInput.CommandId: return ParseInput(data); default: diff --git a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs b/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs index 506f57e..29a1b7a 100644 --- a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs @@ -17,11 +17,11 @@ public DrumsVjoyMapper() : base() /// /// Handles an incoming packet. /// - protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) { switch (command) { - case CommandId.Input: + case DrumInput.CommandId: return ParseInput(data); default: diff --git a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs index 98a85f1..123424e 100644 --- a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs @@ -15,11 +15,16 @@ public FallbackVigemMapper() : base() /// /// Handles an incoming packet. /// - protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) { switch (command) { - case CommandId.Input: + case GuitarInput.CommandId: + // These have the same value + // case DrumInput.CommandId: + // #if DEBUG + // case GamepadInput.CommandId: + // #endif return ParseInput(data); default: diff --git a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs index 7851a0d..9bdf1fd 100644 --- a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs @@ -16,11 +16,16 @@ public FallbackVjoyMapper() : base() /// /// Handles an incoming packet. /// - protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) { switch (command) { - case CommandId.Input: + case GuitarInput.CommandId: + // These have the same value + // case DrumInput.CommandId: + // #if DEBUG + // case GamepadInput.CommandId: + // #endif return ParseInput(data); default: diff --git a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs b/Program/PacketParsing/Mappers/GamepadVigemMapper.cs index f1a4eb1..6558da6 100644 --- a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs +++ b/Program/PacketParsing/Mappers/GamepadVigemMapper.cs @@ -19,11 +19,11 @@ public GamepadVigemMapper() : base() /// /// Handles an incoming packet. /// - protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) { switch (command) { - case CommandId.Input: + case GamepadInput.CommandId: return ParseInput(data); default: diff --git a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs b/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs index d580372..d64f13a 100644 --- a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs @@ -19,11 +19,11 @@ public GamepadVjoyMapper() : base() /// /// Handles an incoming packet. /// - protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) { switch (command) { - case CommandId.Input: + case GamepadInput.CommandId: return ParseInput(data); default: diff --git a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs b/Program/PacketParsing/Mappers/GuitarVigemMapper.cs index 42df711..ad2562f 100644 --- a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs +++ b/Program/PacketParsing/Mappers/GuitarVigemMapper.cs @@ -17,11 +17,11 @@ public GuitarVigemMapper() : base() /// /// Handles an incoming packet. /// - protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) { switch (command) { - case CommandId.Input: + case GuitarInput.CommandId: return ParseInput(data); default: diff --git a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs index 2dec946..3e65fc7 100644 --- a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs @@ -17,11 +17,11 @@ public GuitarVjoyMapper() : base() /// /// Handles an incoming packet. /// - protected override XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data) + protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) { switch (command) { - case CommandId.Input: + case GuitarInput.CommandId: return ParseInput(data); default: diff --git a/Program/PacketParsing/Mappers/IDeviceMapper.cs b/Program/PacketParsing/Mappers/IDeviceMapper.cs index d53ae63..2a4b1c5 100644 --- a/Program/PacketParsing/Mappers/IDeviceMapper.cs +++ b/Program/PacketParsing/Mappers/IDeviceMapper.cs @@ -15,7 +15,7 @@ internal interface IDeviceMapper : IDisposable /// /// Handles an incoming packet. /// - XboxResult HandlePacket(CommandId command, ReadOnlySpan data); + XboxResult HandlePacket(byte command, ReadOnlySpan data); XboxResult HandleKeystroke(Keystroke key); } diff --git a/Program/PacketParsing/Mappers/VigemMapper.cs b/Program/PacketParsing/Mappers/VigemMapper.cs index 0e70ea3..0b55693 100644 --- a/Program/PacketParsing/Mappers/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/VigemMapper.cs @@ -57,7 +57,7 @@ private void DeviceConnected(object sender, Xbox360FeedbackReceivedEventArgs arg /// /// Handles an incoming packet. /// - public XboxResult HandlePacket(CommandId command, ReadOnlySpan data) + public XboxResult HandlePacket(byte command, ReadOnlySpan data) { if (device == null) throw new ObjectDisposedException(nameof(device)); @@ -68,7 +68,7 @@ public XboxResult HandlePacket(CommandId command, ReadOnlySpan data) return OnPacketReceived(command, data); } - protected abstract XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data); + protected abstract XboxResult OnPacketReceived(byte command, ReadOnlySpan data); public XboxResult HandleKeystroke(Keystroke key) { diff --git a/Program/PacketParsing/Mappers/VjoyMapper.cs b/Program/PacketParsing/Mappers/VjoyMapper.cs index 6fc0c48..0982b49 100644 --- a/Program/PacketParsing/Mappers/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/VjoyMapper.cs @@ -43,7 +43,7 @@ public VjoyMapper() /// /// Handles an incoming packet. /// - public XboxResult HandlePacket(CommandId command, ReadOnlySpan data) + public XboxResult HandlePacket(byte command, ReadOnlySpan data) { if (deviceId == 0) throw new ObjectDisposedException("this"); @@ -51,7 +51,7 @@ public XboxResult HandlePacket(CommandId command, ReadOnlySpan data) return OnPacketReceived(command, data); } - protected abstract XboxResult OnPacketReceived(CommandId command, ReadOnlySpan data); + protected abstract XboxResult OnPacketReceived(byte command, ReadOnlySpan data); public XboxResult HandleKeystroke(Keystroke key) { diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/CommandHeader.cs index 33d5fd9..9e609f7 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/CommandHeader.cs @@ -42,7 +42,7 @@ internal struct CommandHeader { public const int MinimumByteLength = 4; - public CommandId CommandId; + public byte CommandId; public byte Flags_Client; public byte SequenceCount; public int DataLength; @@ -72,7 +72,7 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o // Command info header = new CommandHeader() { - CommandId = (CommandId)data[0], + CommandId = data[0], Flags_Client = data[1], SequenceCount = data[2], }; @@ -108,7 +108,7 @@ public bool TryWriteToBuffer(Span buffer, out int bytesWritten) return false; // Command info - buffer[0] = (byte)CommandId; + buffer[0] = CommandId; buffer[1] = Flags_Client; buffer[2] = SequenceCount; bytesWritten += MinimumByteLength - 1; diff --git a/Program/PacketParsing/Packets/Drums/DrumInput.cs b/Program/PacketParsing/Packets/Drums/DrumInput.cs index dbf648a..1d8564a 100644 --- a/Program/PacketParsing/Packets/Drums/DrumInput.cs +++ b/Program/PacketParsing/Packets/Drums/DrumInput.cs @@ -22,6 +22,8 @@ internal enum DrumButton : ushort [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct DrumInput { + public const byte CommandId = 0x20; + public ushort Buttons; private readonly ushort pads; private readonly ushort cymbals; diff --git a/Program/PacketParsing/Packets/Gamepad/GamepadInput.cs b/Program/PacketParsing/Packets/Gamepad/GamepadInput.cs index 095d621..bee55d7 100644 --- a/Program/PacketParsing/Packets/Gamepad/GamepadInput.cs +++ b/Program/PacketParsing/Packets/Gamepad/GamepadInput.cs @@ -35,6 +35,8 @@ internal enum GamepadButton : ushort [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct GamepadInput { + public const byte CommandId = 0x20; + public const ushort TriggerMax = 0x03FF; public bool A => (Buttons & (ushort)GamepadButton.A) != 0; diff --git a/Program/PacketParsing/Packets/Guitar/GuitarInput.cs b/Program/PacketParsing/Packets/Guitar/GuitarInput.cs index 7c02c67..a735c3f 100644 --- a/Program/PacketParsing/Packets/Guitar/GuitarInput.cs +++ b/Program/PacketParsing/Packets/Guitar/GuitarInput.cs @@ -38,6 +38,8 @@ internal enum GuitarFret : byte [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct GuitarInput { + public const byte CommandId = 0x20; + public ushort Buttons; public byte Tilt; public byte WhammyBar; diff --git a/Program/PacketParsing/Packets/System/Acknowledgement.cs b/Program/PacketParsing/Packets/System/Acknowledgement.cs index 56e45f8..d7fe1ca 100644 --- a/Program/PacketParsing/Packets/System/Acknowledgement.cs +++ b/Program/PacketParsing/Packets/System/Acknowledgement.cs @@ -12,8 +12,10 @@ namespace RB4InstrumentMapper.Parsing [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct Acknowledgement { + public const byte CommandId = 0x01; + private byte unk1; - public CommandId InnerCommand; + public byte InnerCommand; public byte InnerFlags_Client; public ushort BytesReceived; private ushort unk2; @@ -48,7 +50,7 @@ public static (CommandHeader header, Acknowledgement acknowledge) FromMessage(Co }; // Set remaining header data (length is set when sending) - header.CommandId = CommandId.Acknowledgement; + header.CommandId = CommandId; return (header, acknowledge); } diff --git a/Program/PacketParsing/Packets/System/DeviceArrival.cs b/Program/PacketParsing/Packets/System/DeviceArrival.cs index ec7ac36..03329ff 100644 --- a/Program/PacketParsing/Packets/System/DeviceArrival.cs +++ b/Program/PacketParsing/Packets/System/DeviceArrival.cs @@ -8,6 +8,8 @@ namespace RB4InstrumentMapper.Parsing [StructLayout(LayoutKind.Sequential, Pack = 1)] internal readonly struct DeviceArrival { + public const byte CommandId = 0x02; + public readonly ulong SerialNumber; public readonly ushort VendorId; public readonly ushort ProductId; diff --git a/Program/PacketParsing/Packets/System/DeviceStatus.cs b/Program/PacketParsing/Packets/System/DeviceStatus.cs index 82fe32f..ad2e0b4 100644 --- a/Program/PacketParsing/Packets/System/DeviceStatus.cs +++ b/Program/PacketParsing/Packets/System/DeviceStatus.cs @@ -31,6 +31,8 @@ internal enum BatteryLevel : byte [StructLayout(LayoutKind.Sequential, Pack = 1)] internal readonly struct DeviceStatus { + public const byte CommandId = 0x03; + private readonly byte status; private readonly byte unk1; private readonly byte unk2; diff --git a/Program/PacketParsing/Packets/System/Keystroke.cs b/Program/PacketParsing/Packets/System/Keystroke.cs index 2411838..9254274 100644 --- a/Program/PacketParsing/Packets/System/Keystroke.cs +++ b/Program/PacketParsing/Packets/System/Keystroke.cs @@ -27,6 +27,8 @@ public enum KeyCode : byte [StructLayout(LayoutKind.Sequential, Pack = 1)] internal struct Keystroke { + public const byte CommandId = 0x07; + public KeystrokeFlags Flags; public KeyCode Keycode; diff --git a/Program/PacketParsing/Packets/System/XboxDescriptor.cs b/Program/PacketParsing/Packets/System/XboxDescriptor.cs index 1d95bc9..5f1aabb 100644 --- a/Program/PacketParsing/Packets/System/XboxDescriptor.cs +++ b/Program/PacketParsing/Packets/System/XboxDescriptor.cs @@ -38,6 +38,9 @@ private struct Offsets private ushort unk3; } + public const byte CommandId = 0x04; + + public IReadOnlyList ClassNames { get; private set; } public IReadOnlyList InterfaceGuids { get; private set; } diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 85ba70b..33fb189 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -27,11 +27,11 @@ internal class XboxClient : IDisposable private IDeviceMapper deviceMapper; - private readonly Dictionary previousReceiveSequence = new Dictionary(); - private readonly Dictionary previousSendSequence = new Dictionary(); - private readonly Dictionary chunkBuffers = new Dictionary() + private readonly Dictionary previousReceiveSequence = new Dictionary(); + private readonly Dictionary previousSendSequence = new Dictionary(); + private readonly Dictionary chunkBuffers = new Dictionary() { - { CommandId.Descriptor, new ChunkBuffer() }, + { XboxDescriptor.CommandId, new ChunkBuffer() }, }; public XboxClient(XboxDevice parent, byte clientId) @@ -123,21 +123,20 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan commandData) + private XboxResult HandleSystemCommand(byte commandId, ReadOnlySpan commandData) { switch (commandId) { - case CommandId.Arrival: + case DeviceArrival.CommandId: return HandleArrival(commandData); - case CommandId.Status: + case DeviceStatus.CommandId: return HandleStatus(commandData); - case CommandId.Descriptor: + case XboxDescriptor.CommandId: return HandleDescriptor(commandData); - // Keystrokes are handled by the mapper - case CommandId.Keystroke: + case Keystroke.CommandId: return HandleKeystroke(commandData); } From 255bae519cef378769fb5fb662f00410c27ecc99 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 07:28:18 -0600 Subject: [PATCH 198/437] Implement basic initialization sequence --- .../Packets/System/Authentication.cs | 17 ++++++ .../Packets/System/DeviceConfiguration.cs | 39 +++++++++++++ .../Packets/System/LedControl.cs | 26 +++++++++ Program/PacketParsing/XboxClient.cs | 58 ++++++++++++++++++- Program/RB4InstrumentMapper.csproj | 3 + 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 Program/PacketParsing/Packets/System/Authentication.cs create mode 100644 Program/PacketParsing/Packets/System/DeviceConfiguration.cs create mode 100644 Program/PacketParsing/Packets/System/LedControl.cs diff --git a/Program/PacketParsing/Packets/System/Authentication.cs b/Program/PacketParsing/Packets/System/Authentication.cs new file mode 100644 index 0000000..e9926d3 --- /dev/null +++ b/Program/PacketParsing/Packets/System/Authentication.cs @@ -0,0 +1,17 @@ +namespace RB4InstrumentMapper.Parsing +{ + internal static class Authentication + { + public const byte CommandId = 0x06; + + public static readonly XboxMessage SuccessMessage = new XboxMessage() + { + Header = new CommandHeader() + { + CommandId = CommandId, + Flags = CommandFlags.SystemCommand, + }, + Data = new byte[] { 0x01, 0x00 }, + }; + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/System/DeviceConfiguration.cs b/Program/PacketParsing/Packets/System/DeviceConfiguration.cs new file mode 100644 index 0000000..0e8e993 --- /dev/null +++ b/Program/PacketParsing/Packets/System/DeviceConfiguration.cs @@ -0,0 +1,39 @@ +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + internal enum ConfigurationCommand : byte + { + PowerOn = 0x00, + Sleep = 0x01, + PowerOff = 0x04, + WirelessPairing = 0x06, + Reset = 0x07, + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct DeviceConfiguration + { + public const byte CommandId = 0x05; + + public ConfigurationCommand SubCommand; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct DeviceConfiguration + where TSub : unmanaged + { + public ConfigurationCommand SubCommand; + public TSub SubData; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal unsafe struct WirelessPairing + { + public const ConfigurationCommand SubCommand = ConfigurationCommand.WirelessPairing; + + private fixed byte pairingAddress[6]; + public ushort countryCode; + private fixed byte unknown[6]; + } +} diff --git a/Program/PacketParsing/Packets/System/LedControl.cs b/Program/PacketParsing/Packets/System/LedControl.cs new file mode 100644 index 0000000..3102810 --- /dev/null +++ b/Program/PacketParsing/Packets/System/LedControl.cs @@ -0,0 +1,26 @@ +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + internal enum LedMode : byte + { + Off = 0x00, + On = 0x01, + BlinkFast = 0x02, + BlinkNormal = 0x03, + BlinkSlow = 0x04, + FadeSlow = 0x08, + FadeFast = 0x09, + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct LedControl + { + public const byte CommandId = 0x0a; + + private byte unknown; + + public LedMode Mode; + public byte Brightness; + } +} diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 33fb189..9d46579 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -10,6 +10,45 @@ namespace RB4InstrumentMapper.Parsing ///
internal class XboxClient : IDisposable { + #region Message definitions + private static readonly XboxMessage GetDescriptor = new XboxMessage() + { + Header = new CommandHeader() + { + CommandId = XboxDescriptor.CommandId, + Flags = CommandFlags.SystemCommand, + }, + // Header only, no data + }; + + private static readonly XboxMessage PowerOnDevice = new XboxMessage() + { + Header = new CommandHeader() + { + CommandId = DeviceConfiguration.CommandId, + Flags = CommandFlags.SystemCommand, + }, + Data = new DeviceConfiguration() + { + SubCommand = ConfigurationCommand.PowerOn, + } + }; + + private static readonly XboxMessage EnableLed = new XboxMessage() + { + Header = new CommandHeader() + { + CommandId = LedControl.CommandId, + Flags = CommandFlags.SystemCommand, + }, + Data = new LedControl() + { + Mode = LedMode.On, + Brightness = 0x14 + } + }; + #endregion + /// /// The parent device of the client. /// @@ -152,7 +191,9 @@ private unsafe XboxResult HandleArrival(ReadOnlySpan data) return XboxResult.InvalidMessage; Console.WriteLine($"New client connected with ID {arrival.SerialNumber:X12}"); - return XboxResult.Success; + + // Kick off descriptor request + return SendMessage(GetDescriptor); } /// @@ -182,6 +223,21 @@ private XboxResult HandleDescriptor(ReadOnlySpan data) Descriptor = descriptor; deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, XboxDevice.MapperMode); + + // Send final set of initialization messages + var result = SendMessage(PowerOnDevice); + if (result != XboxResult.Success) + return result; + + result = SendMessage(EnableLed); + if (result != XboxResult.Success) + return result; + + // Authentication is not and will not be implemented, we just automatically pass all devices + result = SendMessage(Authentication.SuccessMessage); + if (result != XboxResult.Success) + return result; + return XboxResult.Success; } diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 5805178..c69b192 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -133,9 +133,12 @@ + + + From cf4cbd5328a407062455baef16ed7c8f83384893 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 08:07:52 -0600 Subject: [PATCH 199/437] Rework descriptor parsing to use hash sets --- .../PacketParsing/Mappers/MapperFactory.cs | 2 +- .../Packets/System/XboxDescriptor.cs | 68 ++++++------------- 2 files changed, 20 insertions(+), 50 deletions(-) diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 9489114..54eef35 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -22,7 +22,7 @@ internal static class MapperFactory #endif }; - public static IDeviceMapper GetMapper(IReadOnlyList interfaceGuids, MappingMode mode) + public static IDeviceMapper GetMapper(IEnumerable interfaceGuids, MappingMode mode) { // Get unique interface GUID Guid interfaceGuid = default; diff --git a/Program/PacketParsing/Packets/System/XboxDescriptor.cs b/Program/PacketParsing/Packets/System/XboxDescriptor.cs index 5f1aabb..d7259a1 100644 --- a/Program/PacketParsing/Packets/System/XboxDescriptor.cs +++ b/Program/PacketParsing/Packets/System/XboxDescriptor.cs @@ -28,8 +28,8 @@ private struct Offsets public ushort CustomCommands; public ushort FirmwareVersions; public ushort AudioFormats; - public ushort OutputCommands; public ushort InputCommands; + public ushort OutputCommands; public ushort ClassNames; public ushort InterfaceGuids; public ushort HidDescriptor; @@ -40,9 +40,8 @@ private struct Offsets public const byte CommandId = 0x04; - - public IReadOnlyList ClassNames { get; private set; } - public IReadOnlyList InterfaceGuids { get; private set; } + public HashSet ClassNames { get; private set; } + public HashSet InterfaceGuids { get; private set; } public static bool Parse(ReadOnlySpan data, out XboxDescriptor descriptor) { @@ -98,7 +97,7 @@ private unsafe bool Parse(ReadOnlySpan data) // Data elements ClassNames = ParseStrings(data, offsets.ClassNames, nameof(ClassNames)); - InterfaceGuids = ParseElements(data, offsets.InterfaceGuids, nameof(InterfaceGuids)); + InterfaceGuids = ParseUnique(data, offsets.InterfaceGuids, nameof(InterfaceGuids)); return true; } @@ -149,7 +148,7 @@ private static bool VerifyOffset(ReadOnlySpan buffer, ushort offset, int e return true; } - private static unsafe T[] ParseElements(ReadOnlySpan buffer, ushort offset, string elementName, Func customCheck = null) + private static unsafe HashSet ParseUnique(ReadOnlySpan buffer, ushort offset, string elementName) where T : unmanaged { if (!VerifyOffset(buffer, offset, sizeof(T), out byte count, elementName) || count == 0) @@ -159,54 +158,33 @@ private static unsafe T[] ParseElements(ReadOnlySpan buffer, ushort off // Get data bounds buffer = buffer.Slice(offset + sizeof(byte), count * sizeof(T)); - // Get element data - if (customCheck == null) - { - // No checks, get everything at once - return MemoryMarshal.Cast(buffer).ToArray(); - } - // Checks required, go through elements individually - var elements = new T[count]; - for (byte index = 0; index < count; index++) + // Get element data + var set = new HashSet(count); + var elements = MemoryMarshal.Cast(buffer); + foreach (var element in elements) { - if (!MemoryMarshal.TryRead(buffer, out T element)) - { - Debug.Fail($"Failed to read element from buffer! Buffer size: {buffer.Length}, element size: {sizeof(T)}"); - TruncateArray(ref elements, index); - break; - } - - if (!customCheck(element)) - { - Debug.Fail($"Check for {elementName} failed!"); - TruncateArray(ref elements, index); - break; - } - - elements[index] = element; - buffer = buffer.Slice(sizeof(T)); + set.Add(element); } - return elements; + return set; } - private static unsafe string[] ParseStrings(ReadOnlySpan buffer, ushort offset, string elementName) + private static unsafe HashSet ParseStrings(ReadOnlySpan buffer, ushort offset, string elementName) { if (!VerifyOffset(buffer, offset, 0, out byte count, elementName) || count == 0) { return null; } - var elements = new string[count]; + var set = new HashSet(count); buffer = buffer.Slice(offset + 1); - for (byte index = 0; index < elements.Length; index++) + for (byte index = 0; index < count; index++) { // Get length if (!MemoryMarshal.TryRead(buffer, out ushort length)) { - // Resize array to exclude null elements - TruncateArray(ref elements, index); + set.TrimExcess(); break; } buffer = buffer.Slice(sizeof(ushort)); @@ -215,8 +193,7 @@ private static unsafe string[] ParseStrings(ReadOnlySpan buffer, ushort of if (buffer.Length < length) { Debug.Fail($"Descriptor string length is greater than buffer size! Index: {index}; String length: {length}; Buffer size: {buffer.Length}"); - // Resize array to exclude null elements - TruncateArray(ref elements, index); + set.TrimExcess(); break; } @@ -225,20 +202,13 @@ private static unsafe string[] ParseStrings(ReadOnlySpan buffer, ushort of fixed (byte* ptr = buffer) { sbyte* sPtr = (sbyte*)ptr; - elements[index] = new string(sPtr, 0, length); + var str = new string(sPtr, 0, length); + set.Add(str); } buffer = buffer.Slice(length); } - return elements; - } - - private static void TruncateArray(ref T[] array, int length) - { - if (length == 0) - array = null; - else - array = array.AsSpan().Slice(0, length).ToArray(); + return set; } } } \ No newline at end of file From bdb6189e11ac6dcacd0b86294bfabdbe839190f3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 08:13:10 -0600 Subject: [PATCH 200/437] Respect descriptor's list of output commands --- .../Packets/System/XboxDescriptor.cs | 4 ++++ Program/PacketParsing/XboxClient.cs | 21 ++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Program/PacketParsing/Packets/System/XboxDescriptor.cs b/Program/PacketParsing/Packets/System/XboxDescriptor.cs index d7259a1..ef7455f 100644 --- a/Program/PacketParsing/Packets/System/XboxDescriptor.cs +++ b/Program/PacketParsing/Packets/System/XboxDescriptor.cs @@ -40,6 +40,8 @@ private struct Offsets public const byte CommandId = 0x04; + public HashSet InputCommands { get; private set; } + public HashSet OutputCommands { get; private set; } public HashSet ClassNames { get; private set; } public HashSet InterfaceGuids { get; private set; } @@ -96,6 +98,8 @@ private unsafe bool Parse(ReadOnlySpan data) // No slice, offsets are relative to the start of the offsets block // Data elements + InputCommands = ParseUnique(data, offsets.InputCommands, nameof(InputCommands)); + OutputCommands = ParseUnique(data, offsets.OutputCommands, nameof(OutputCommands)); ClassNames = ParseStrings(data, offsets.ClassNames, nameof(ClassNames)); InterfaceGuids = ParseUnique(data, offsets.InterfaceGuids, nameof(InterfaceGuids)); diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 9d46579..b19d86e 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -225,18 +225,25 @@ private XboxResult HandleDescriptor(ReadOnlySpan data) deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, XboxDevice.MapperMode); // Send final set of initialization messages + Debug.Assert(Descriptor.OutputCommands.Contains(DeviceConfiguration.CommandId)); var result = SendMessage(PowerOnDevice); if (result != XboxResult.Success) return result; - result = SendMessage(EnableLed); - if (result != XboxResult.Success) - return result; + if (Descriptor.OutputCommands.Contains(LedControl.CommandId)) + { + result = SendMessage(EnableLed); + if (result != XboxResult.Success) + return result; + } - // Authentication is not and will not be implemented, we just automatically pass all devices - result = SendMessage(Authentication.SuccessMessage); - if (result != XboxResult.Success) - return result; + if (Descriptor.OutputCommands.Contains(Authentication.CommandId)) + { + // Authentication is not and will not be implemented, we just automatically pass all devices + result = SendMessage(Authentication.SuccessMessage); + if (result != XboxResult.Success) + return result; + } return XboxResult.Success; } From 5906c52a996adbb5d09495fb899fa3e959941519 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 19:36:59 -0600 Subject: [PATCH 201/437] Add Xbox prefix to all packet types to prevent name collisions --- .../PacketParsing/Mappers/DrumsVigemMapper.cs | 36 ++++----- .../PacketParsing/Mappers/DrumsVjoyMapper.cs | 24 +++--- .../Mappers/FallbackVigemMapper.cs | 8 +- .../Mappers/FallbackVjoyMapper.cs | 8 +- .../Mappers/GamepadVigemMapper.cs | 6 +- .../Mappers/GamepadVjoyMapper.cs | 8 +- .../Mappers/GuitarVigemMapper.cs | 20 ++--- .../PacketParsing/Mappers/GuitarVjoyMapper.cs | 12 +-- .../PacketParsing/Mappers/IDeviceMapper.cs | 2 +- .../PacketParsing/Mappers/MapperFactory.cs | 10 +-- Program/PacketParsing/Mappers/VigemMapper.cs | 4 +- Program/PacketParsing/Mappers/VjoyMapper.cs | 22 +++--- .../Drums/{DrumInput.cs => XboxDrumInput.cs} | 8 +- .../Packets/Gamepad/GamepadInput.cs | 70 ----------------- .../Packets/Gamepad/XboxGamepadInput.cs | 70 +++++++++++++++++ .../{GuitarInput.cs => XboxGuitarInput.cs} | 36 ++++----- ...nowledgement.cs => XboxAcknowledgement.cs} | 16 ++-- .../{DeviceArrival.cs => XboxArrival.cs} | 2 +- ...uthentication.cs => XboxAuthentication.cs} | 6 +- ...eConfiguration.cs => XboxConfiguration.cs} | 14 ++-- .../System/{Keystroke.cs => XboxKeystroke.cs} | 12 +-- .../{LedControl.cs => XboxLedControl.cs} | 6 +- .../System/{DeviceStatus.cs => XboxStatus.cs} | 10 +-- .../{ChunkBuffer.cs => XboxChunkBuffer.cs} | 10 +-- ...{CommandHeader.cs => XboxCommandHeader.cs} | 33 +++----- Program/PacketParsing/Packets/XboxMessage.cs | 8 +- Program/PacketParsing/XboxClient.cs | 78 +++++++++---------- Program/PacketParsing/XboxDevice.cs | 10 +-- .../{DeviceGuids.cs => XboxDeviceGuids.cs} | 2 +- Program/RB4InstrumentMapper.csproj | 26 +++---- 30 files changed, 281 insertions(+), 296 deletions(-) rename Program/PacketParsing/Packets/Drums/{DrumInput.cs => XboxDrumInput.cs} (86%) delete mode 100644 Program/PacketParsing/Packets/Gamepad/GamepadInput.cs create mode 100644 Program/PacketParsing/Packets/Gamepad/XboxGamepadInput.cs rename Program/PacketParsing/Packets/Guitar/{GuitarInput.cs => XboxGuitarInput.cs} (52%) rename Program/PacketParsing/Packets/System/{Acknowledgement.cs => XboxAcknowledgement.cs} (76%) rename Program/PacketParsing/Packets/System/{DeviceArrival.cs => XboxArrival.cs} (92%) rename Program/PacketParsing/Packets/System/{Authentication.cs => XboxAuthentication.cs} (68%) rename Program/PacketParsing/Packets/System/{DeviceConfiguration.cs => XboxConfiguration.cs} (62%) rename Program/PacketParsing/Packets/System/{Keystroke.cs => XboxKeystroke.cs} (71%) rename Program/PacketParsing/Packets/System/{LedControl.cs => XboxLedControl.cs} (80%) rename Program/PacketParsing/Packets/System/{DeviceStatus.cs => XboxStatus.cs} (75%) rename Program/PacketParsing/Packets/{ChunkBuffer.cs => XboxChunkBuffer.cs} (88%) rename Program/PacketParsing/Packets/{CommandHeader.cs => XboxCommandHeader.cs} (88%) rename Program/PacketParsing/{DeviceGuids.cs => XboxDeviceGuids.cs} (95%) diff --git a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs index ee030f0..17bd4d5 100644 --- a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs +++ b/Program/PacketParsing/Mappers/DrumsVigemMapper.cs @@ -21,7 +21,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan { switch (command) { - case DrumInput.CommandId: + case XboxDrumInput.CommandId: return ParseInput(data); default: @@ -43,7 +43,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan /// private unsafe XboxResult ParseInput(ReadOnlySpan data) { - if (data.Length != sizeof(DrumInput) || !MemoryMarshal.TryRead(data, out DrumInput drumReport)) + if (data.Length != sizeof(XboxDrumInput) || !MemoryMarshal.TryRead(data, out XboxDrumInput drumReport)) return XboxResult.InvalidMessage; HandleReport(device, drumReport, ref previousDpadCymbals, ref dpadMask); @@ -56,18 +56,18 @@ private unsafe XboxResult ParseInput(ReadOnlySpan data) /// /// Maps drumkit input data to an Xbox 360 controller. /// - internal static void HandleReport(IXbox360Controller device, in DrumInput report, ref int previousDpadCymbals, ref int dpadMask) + internal static void HandleReport(IXbox360Controller device, in XboxDrumInput report, ref int previousDpadCymbals, ref int dpadMask) { // Menu and Options - var buttons = (GamepadButton)report.Buttons; - device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); - device.SetButtonState(Xbox360Button.Back, (buttons & GamepadButton.Options) != 0); + var buttons = (XboxGamepadButton)report.Buttons; + device.SetButtonState(Xbox360Button.Start, (buttons & XboxGamepadButton.Menu) != 0); + device.SetButtonState(Xbox360Button.Back, (buttons & XboxGamepadButton.Options) != 0); // Dpad - device.SetButtonState(Xbox360Button.Up, (buttons & GamepadButton.DpadUp) != 0); - device.SetButtonState(Xbox360Button.Down, (buttons & GamepadButton.DpadDown) != 0); - device.SetButtonState(Xbox360Button.Left, (buttons & GamepadButton.DpadLeft) != 0); - device.SetButtonState(Xbox360Button.Right, (buttons & GamepadButton.DpadRight) != 0); + device.SetButtonState(Xbox360Button.Up, (buttons & XboxGamepadButton.DpadUp) != 0); + device.SetButtonState(Xbox360Button.Down, (buttons & XboxGamepadButton.DpadDown) != 0); + device.SetButtonState(Xbox360Button.Left, (buttons & XboxGamepadButton.DpadLeft) != 0); + device.SetButtonState(Xbox360Button.Right, (buttons & XboxGamepadButton.DpadRight) != 0); // Pads and cymbals byte redPad = report.RedPad; @@ -113,14 +113,14 @@ internal static void HandleReport(IXbox360Controller device, in DrumInput report previousDpadCymbals = cymbalMask; } - device.SetButtonState(Xbox360Button.Up, ((dpadMask & yellowBit) != 0) || ((buttons & GamepadButton.DpadUp) != 0)); - device.SetButtonState(Xbox360Button.Down, ((dpadMask & blueBit) != 0) || ((buttons & GamepadButton.DpadDown) != 0)); + device.SetButtonState(Xbox360Button.Up, ((dpadMask & yellowBit) != 0) || ((buttons & XboxGamepadButton.DpadUp) != 0)); + device.SetButtonState(Xbox360Button.Down, ((dpadMask & blueBit) != 0) || ((buttons & XboxGamepadButton.DpadDown) != 0)); // Color flags - device.SetButtonState(Xbox360Button.B, (redPad != 0) || ((buttons & GamepadButton.B) != 0)); - device.SetButtonState(Xbox360Button.Y, ((yellowPad | yellowCym) != 0) || ((buttons & GamepadButton.Y) != 0)); - device.SetButtonState(Xbox360Button.X, ((bluePad | blueCym) != 0) || ((buttons & GamepadButton.X) != 0)); - device.SetButtonState(Xbox360Button.A, ((greenPad | greenCym) != 0) || ((buttons & GamepadButton.A) != 0)); + device.SetButtonState(Xbox360Button.B, (redPad != 0) || ((buttons & XboxGamepadButton.B) != 0)); + device.SetButtonState(Xbox360Button.Y, ((yellowPad | yellowCym) != 0) || ((buttons & XboxGamepadButton.Y) != 0)); + device.SetButtonState(Xbox360Button.X, ((bluePad | blueCym) != 0) || ((buttons & XboxGamepadButton.X) != 0)); + device.SetButtonState(Xbox360Button.A, ((greenPad | greenCym) != 0) || ((buttons & XboxGamepadButton.A) != 0)); // Pad flag device.SetButtonState(Xbox360Button.RightThumb, @@ -131,9 +131,9 @@ internal static void HandleReport(IXbox360Controller device, in DrumInput report // Pedals device.SetButtonState(Xbox360Button.LeftShoulder, - (report.Buttons & (ushort)DrumButton.KickOne) != 0); + (report.Buttons & (ushort)XboxDrumButton.KickOne) != 0); device.SetButtonState(Xbox360Button.LeftThumb, - (report.Buttons & (ushort)DrumButton.KickTwo) != 0); + (report.Buttons & (ushort)XboxDrumButton.KickTwo) != 0); // Velocities device.SetAxisValue( diff --git a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs b/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs index 29a1b7a..b5bb01c 100644 --- a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs @@ -21,7 +21,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan { switch (command) { - case DrumInput.CommandId: + case XboxDrumInput.CommandId: return ParseInput(data); default: @@ -34,7 +34,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan ///
public unsafe XboxResult ParseInput(ReadOnlySpan data) { - if (data.Length != sizeof(DrumInput) || !MemoryMarshal.TryRead(data, out DrumInput guitarReport)) + if (data.Length != sizeof(XboxDrumInput) || !MemoryMarshal.TryRead(data, out XboxDrumInput guitarReport)) return XboxResult.InvalidMessage; HandleReport(ref state, guitarReport); @@ -47,21 +47,21 @@ public unsafe XboxResult ParseInput(ReadOnlySpan data) /// /// Maps drumkit input data to a vJoy device. /// - internal static void HandleReport(ref vJoy.JoystickState state, DrumInput report) + internal static void HandleReport(ref vJoy.JoystickState state, XboxDrumInput report) { // Menu and Options - var buttons = (GamepadButton)report.Buttons; - state.SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); - state.SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); + var buttons = (XboxGamepadButton)report.Buttons; + state.SetButton(VjoyButton.Fifteen, (buttons & XboxGamepadButton.Menu) != 0); + state.SetButton(VjoyButton.Sixteen, (buttons & XboxGamepadButton.Options) != 0); // D-pad ParseDpad(ref state, buttons); // Face buttons - state.SetButton(VjoyButton.Four, (buttons & GamepadButton.A) != 0); - state.SetButton(VjoyButton.One, (buttons & GamepadButton.B) != 0); - state.SetButton(VjoyButton.Three, (buttons & GamepadButton.X) != 0); - state.SetButton(VjoyButton.Two, (buttons & GamepadButton.Y) != 0); + state.SetButton(VjoyButton.Four, (buttons & XboxGamepadButton.A) != 0); + state.SetButton(VjoyButton.One, (buttons & XboxGamepadButton.B) != 0); + state.SetButton(VjoyButton.Three, (buttons & XboxGamepadButton.X) != 0); + state.SetButton(VjoyButton.Two, (buttons & XboxGamepadButton.Y) != 0); // Pads state.SetButton(VjoyButton.One, report.RedPad != 0); @@ -75,8 +75,8 @@ internal static void HandleReport(ref vJoy.JoystickState state, DrumInput report state.SetButton(VjoyButton.Eight, report.GreenCymbal != 0); // Kick pedals - state.SetButton(VjoyButton.Five, (report.Buttons & (ushort)DrumButton.KickOne) != 0); - state.SetButton(VjoyButton.Nine, (report.Buttons & (ushort)DrumButton.KickTwo) != 0); + state.SetButton(VjoyButton.Five, (report.Buttons & (ushort)XboxDrumButton.KickOne) != 0); + state.SetButton(VjoyButton.Nine, (report.Buttons & (ushort)XboxDrumButton.KickTwo) != 0); } } } diff --git a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs index 123424e..b9d01ba 100644 --- a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVigemMapper.cs @@ -19,7 +19,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan { switch (command) { - case GuitarInput.CommandId: + case XboxGuitarInput.CommandId: // These have the same value // case DrumInput.CommandId: // #if DEBUG @@ -42,16 +42,16 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan ///
private unsafe XboxResult ParseInput(ReadOnlySpan data) { - if (data.Length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) + if (data.Length == sizeof(XboxGuitarInput) && MemoryMarshal.TryRead(data, out XboxGuitarInput guitarReport)) { GuitarVigemMapper.HandleReport(device, guitarReport); } - else if (data.Length == sizeof(DrumInput) && MemoryMarshal.TryRead(data, out DrumInput drumReport)) + else if (data.Length == sizeof(XboxDrumInput) && MemoryMarshal.TryRead(data, out XboxDrumInput drumReport)) { DrumsVigemMapper.HandleReport(device, drumReport, ref previousDpadCymbals, ref dpadMask); } #if DEBUG - else if (data.Length == sizeof(GamepadInput) && MemoryMarshal.TryRead(data, out GamepadInput gamepadReport)) + else if (data.Length == sizeof(XboxGamepadInput) && MemoryMarshal.TryRead(data, out XboxGamepadInput gamepadReport)) { GamepadVigemMapper.HandleReport(device, gamepadReport); } diff --git a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs index 9bdf1fd..25e7682 100644 --- a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs @@ -20,7 +20,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan { switch (command) { - case GuitarInput.CommandId: + case XboxGuitarInput.CommandId: // These have the same value // case DrumInput.CommandId: // #if DEBUG @@ -38,16 +38,16 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan ///
public unsafe XboxResult ParseInput(ReadOnlySpan data) { - if (data.Length == sizeof(GuitarInput) && MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) + if (data.Length == sizeof(XboxGuitarInput) && MemoryMarshal.TryRead(data, out XboxGuitarInput guitarReport)) { GuitarVjoyMapper.HandleReport(ref state, guitarReport); } - else if (data.Length == sizeof(DrumInput) && MemoryMarshal.TryRead(data, out DrumInput drumReport)) + else if (data.Length == sizeof(XboxDrumInput) && MemoryMarshal.TryRead(data, out XboxDrumInput drumReport)) { DrumsVjoyMapper.HandleReport(ref state, drumReport); } #if DEBUG - else if (data.Length == sizeof(GamepadInput) && MemoryMarshal.TryRead(data, out GamepadInput gamepadReport)) + else if (data.Length == sizeof(XboxGamepadInput) && MemoryMarshal.TryRead(data, out XboxGamepadInput gamepadReport)) { GamepadVjoyMapper.HandleReport(ref state, gamepadReport); } diff --git a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs b/Program/PacketParsing/Mappers/GamepadVigemMapper.cs index 6558da6..628de00 100644 --- a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs +++ b/Program/PacketParsing/Mappers/GamepadVigemMapper.cs @@ -23,7 +23,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan { switch (command) { - case GamepadInput.CommandId: + case XboxGamepadInput.CommandId: return ParseInput(data); default: @@ -36,7 +36,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan ///
private unsafe XboxResult ParseInput(ReadOnlySpan data) { - if (data.Length < sizeof(GamepadInput) || !MemoryMarshal.TryRead(data, out GamepadInput gamepadReport)) + if (data.Length < sizeof(XboxGamepadInput) || !MemoryMarshal.TryRead(data, out XboxGamepadInput gamepadReport)) return XboxResult.InvalidMessage; HandleReport(device, gamepadReport); @@ -49,7 +49,7 @@ private unsafe XboxResult ParseInput(ReadOnlySpan data) /// /// Maps gamepad input data to an Xbox 360 controller. /// - internal static void HandleReport(IXbox360Controller device, in GamepadInput report) + internal static void HandleReport(IXbox360Controller device, in XboxGamepadInput report) { // Face buttons device.SetButtonState(Xbox360Button.A, report.A); diff --git a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs b/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs index d64f13a..eeb8ec2 100644 --- a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs @@ -23,7 +23,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan { switch (command) { - case GamepadInput.CommandId: + case XboxGamepadInput.CommandId: return ParseInput(data); default: @@ -36,7 +36,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan ///
public unsafe XboxResult ParseInput(ReadOnlySpan data) { - if (data.Length < sizeof(GamepadInput) || !MemoryMarshal.TryRead(data, out GamepadInput gamepadReport)) + if (data.Length < sizeof(XboxGamepadInput) || !MemoryMarshal.TryRead(data, out XboxGamepadInput gamepadReport)) return XboxResult.InvalidMessage; HandleReport(ref state, gamepadReport); @@ -49,7 +49,7 @@ public unsafe XboxResult ParseInput(ReadOnlySpan data) /// /// Maps gamepad input data to a vJoy device. /// - internal static void HandleReport(ref vJoy.JoystickState state, GamepadInput report) + internal static void HandleReport(ref vJoy.JoystickState state, XboxGamepadInput report) { // Buttons and axes are mapped the same way as they display in joy.cpl when used normally @@ -69,7 +69,7 @@ internal static void HandleReport(ref vJoy.JoystickState state, GamepadInput rep state.SetButton(VjoyButton.Ten, report.RightStickPress); // D-pad - ParseDpad(ref state, (GamepadButton)report.Buttons); + ParseDpad(ref state, (XboxGamepadButton)report.Buttons); // Left stick SetAxis(ref state.AxisX, report.LeftStickX); diff --git a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs b/Program/PacketParsing/Mappers/GuitarVigemMapper.cs index ad2562f..241160d 100644 --- a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs +++ b/Program/PacketParsing/Mappers/GuitarVigemMapper.cs @@ -21,7 +21,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan { switch (command) { - case GuitarInput.CommandId: + case XboxGuitarInput.CommandId: return ParseInput(data); default: @@ -34,7 +34,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan ///
private unsafe XboxResult ParseInput(ReadOnlySpan data) { - if (data.Length != sizeof(GuitarInput) || !MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) + if (data.Length != sizeof(XboxGuitarInput) || !MemoryMarshal.TryRead(data, out XboxGuitarInput guitarReport)) return XboxResult.InvalidMessage; HandleReport(device, guitarReport); @@ -47,18 +47,18 @@ private unsafe XboxResult ParseInput(ReadOnlySpan data) /// /// Maps guitar input data to an Xbox 360 controller. /// - internal static void HandleReport(IXbox360Controller device, in GuitarInput report) + internal static void HandleReport(IXbox360Controller device, in XboxGuitarInput report) { // Menu and Options - var buttons = (GamepadButton)report.Buttons; - device.SetButtonState(Xbox360Button.Start, (buttons & GamepadButton.Menu) != 0); - device.SetButtonState(Xbox360Button.Back, (buttons & GamepadButton.Options) != 0); + var buttons = (XboxGamepadButton)report.Buttons; + device.SetButtonState(Xbox360Button.Start, (buttons & XboxGamepadButton.Menu) != 0); + device.SetButtonState(Xbox360Button.Back, (buttons & XboxGamepadButton.Options) != 0); // Dpad - device.SetButtonState(Xbox360Button.Up, (buttons & GamepadButton.DpadUp) != 0); - device.SetButtonState(Xbox360Button.Down, (buttons & GamepadButton.DpadDown) != 0); - device.SetButtonState(Xbox360Button.Left, (buttons & GamepadButton.DpadLeft) != 0); - device.SetButtonState(Xbox360Button.Right, (buttons & GamepadButton.DpadRight) != 0); + device.SetButtonState(Xbox360Button.Up, (buttons & XboxGamepadButton.DpadUp) != 0); + device.SetButtonState(Xbox360Button.Down, (buttons & XboxGamepadButton.DpadDown) != 0); + device.SetButtonState(Xbox360Button.Left, (buttons & XboxGamepadButton.DpadLeft) != 0); + device.SetButtonState(Xbox360Button.Right, (buttons & XboxGamepadButton.DpadRight) != 0); // Frets device.SetButtonState(Xbox360Button.A, report.Green); diff --git a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs index 3e65fc7..0f3ee7b 100644 --- a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs @@ -21,7 +21,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan { switch (command) { - case GuitarInput.CommandId: + case XboxGuitarInput.CommandId: return ParseInput(data); default: @@ -34,7 +34,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan /// public unsafe XboxResult ParseInput(ReadOnlySpan data) { - if (data.Length != sizeof(GuitarInput) || !MemoryMarshal.TryRead(data, out GuitarInput guitarReport)) + if (data.Length != sizeof(XboxGuitarInput) || !MemoryMarshal.TryRead(data, out XboxGuitarInput guitarReport)) return XboxResult.InvalidMessage; HandleReport(ref state, guitarReport); @@ -47,12 +47,12 @@ public unsafe XboxResult ParseInput(ReadOnlySpan data) /// /// Maps guitar input data to a vJoy device. /// - internal static void HandleReport(ref vJoy.JoystickState state, GuitarInput report) + internal static void HandleReport(ref vJoy.JoystickState state, XboxGuitarInput report) { // Menu and Options - var buttons = (GamepadButton)report.Buttons; - state.SetButton(VjoyButton.Fifteen, (buttons & GamepadButton.Menu) != 0); - state.SetButton(VjoyButton.Sixteen, (buttons & GamepadButton.Options) != 0); + var buttons = (XboxGamepadButton)report.Buttons; + state.SetButton(VjoyButton.Fifteen, (buttons & XboxGamepadButton.Menu) != 0); + state.SetButton(VjoyButton.Sixteen, (buttons & XboxGamepadButton.Options) != 0); // D-pad ParseDpad(ref state, buttons); diff --git a/Program/PacketParsing/Mappers/IDeviceMapper.cs b/Program/PacketParsing/Mappers/IDeviceMapper.cs index 2a4b1c5..9d083ed 100644 --- a/Program/PacketParsing/Mappers/IDeviceMapper.cs +++ b/Program/PacketParsing/Mappers/IDeviceMapper.cs @@ -17,6 +17,6 @@ internal interface IDeviceMapper : IDisposable /// XboxResult HandlePacket(byte command, ReadOnlySpan data); - XboxResult HandleKeystroke(Keystroke key); + XboxResult HandleKeystroke(XboxKeystroke key); } } \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 54eef35..4570a09 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -13,12 +13,12 @@ internal static class MapperFactory // Device interface GUIDs to check when getting the device mapper private static readonly Dictionary> guidToMapper = new Dictionary>() { - { DeviceGuids.MadCatzGuitar, GetGuitarMapper }, - { DeviceGuids.PdpGuitar, GetGuitarMapper }, - { DeviceGuids.MadCatzDrumkit, GetDrumsMapper }, - { DeviceGuids.PdpDrumkit, GetDrumsMapper }, + { XboxDeviceGuids.MadCatzGuitar, GetGuitarMapper }, + { XboxDeviceGuids.PdpGuitar, GetGuitarMapper }, + { XboxDeviceGuids.MadCatzDrumkit, GetDrumsMapper }, + { XboxDeviceGuids.PdpDrumkit, GetDrumsMapper }, #if DEBUG - { DeviceGuids.XboxGamepad, GetGamepadMapper }, + { XboxDeviceGuids.XboxGamepad, GetGamepadMapper }, #endif }; diff --git a/Program/PacketParsing/Mappers/VigemMapper.cs b/Program/PacketParsing/Mappers/VigemMapper.cs index 0b55693..af62e73 100644 --- a/Program/PacketParsing/Mappers/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/VigemMapper.cs @@ -70,9 +70,9 @@ public XboxResult HandlePacket(byte command, ReadOnlySpan data) protected abstract XboxResult OnPacketReceived(byte command, ReadOnlySpan data); - public XboxResult HandleKeystroke(Keystroke key) + public XboxResult HandleKeystroke(XboxKeystroke key) { - if (key.Keycode == KeyCode.LeftWindows && MapGuideButton) + if (key.Keycode == XboxKeyCode.LeftWindows && MapGuideButton) { device.SetButtonState(Xbox360Button.Guide, key.Pressed); device.SubmitReport(); diff --git a/Program/PacketParsing/Mappers/VjoyMapper.cs b/Program/PacketParsing/Mappers/VjoyMapper.cs index 0982b49..20b56d5 100644 --- a/Program/PacketParsing/Mappers/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/VjoyMapper.cs @@ -53,9 +53,9 @@ public XboxResult HandlePacket(byte command, ReadOnlySpan data) protected abstract XboxResult OnPacketReceived(byte command, ReadOnlySpan data); - public XboxResult HandleKeystroke(Keystroke key) + public XboxResult HandleKeystroke(XboxKeystroke key) { - if (key.Keycode == KeyCode.LeftWindows && MapGuideButton) + if (key.Keycode == XboxKeyCode.LeftWindows && MapGuideButton) { state.SetButton(VjoyButton.Fourteen, key.Pressed); VjoyClient.UpdateDevice(deviceId, ref state); @@ -83,16 +83,16 @@ protected static void SetAxisInverted(ref int axisField, short value) /// /// Parses the state of the d-pad. /// - protected static void ParseDpad(ref vJoy.JoystickState state, GamepadButton buttons) + protected static void ParseDpad(ref vJoy.JoystickState state, XboxGamepadButton buttons) { VjoyPoV direction; - if ((buttons & GamepadButton.DpadUp) != 0) + if ((buttons & XboxGamepadButton.DpadUp) != 0) { - if ((buttons & GamepadButton.DpadLeft) != 0) + if ((buttons & XboxGamepadButton.DpadLeft) != 0) { direction = VjoyPoV.UpLeft; } - else if ((buttons & GamepadButton.DpadRight) != 0) + else if ((buttons & XboxGamepadButton.DpadRight) != 0) { direction = VjoyPoV.UpRight; } @@ -101,13 +101,13 @@ protected static void ParseDpad(ref vJoy.JoystickState state, GamepadButton butt direction = VjoyPoV.Up; } } - else if ((buttons & GamepadButton.DpadDown) != 0) + else if ((buttons & XboxGamepadButton.DpadDown) != 0) { - if ((buttons & GamepadButton.DpadLeft) != 0) + if ((buttons & XboxGamepadButton.DpadLeft) != 0) { direction = VjoyPoV.DownLeft; } - else if ((buttons & GamepadButton.DpadRight) != 0) + else if ((buttons & XboxGamepadButton.DpadRight) != 0) { direction = VjoyPoV.DownRight; } @@ -118,11 +118,11 @@ protected static void ParseDpad(ref vJoy.JoystickState state, GamepadButton butt } else { - if ((buttons & GamepadButton.DpadLeft) != 0) + if ((buttons & XboxGamepadButton.DpadLeft) != 0) { direction = VjoyPoV.Left; } - else if ((buttons & GamepadButton.DpadRight) != 0) + else if ((buttons & XboxGamepadButton.DpadRight) != 0) { direction = VjoyPoV.Right; } diff --git a/Program/PacketParsing/Packets/Drums/DrumInput.cs b/Program/PacketParsing/Packets/Drums/XboxDrumInput.cs similarity index 86% rename from Program/PacketParsing/Packets/Drums/DrumInput.cs rename to Program/PacketParsing/Packets/Drums/XboxDrumInput.cs index 1d8564a..6db88da 100644 --- a/Program/PacketParsing/Packets/Drums/DrumInput.cs +++ b/Program/PacketParsing/Packets/Drums/XboxDrumInput.cs @@ -7,20 +7,20 @@ namespace RB4InstrumentMapper.Parsing /// Re-definitions for button flags that have specific meanings. /// [Flags] - internal enum DrumButton : ushort + internal enum XboxDrumButton : ushort { // Not used as these are for menu navigation purposes // RedPad = GamepadButton.B, // GreenPad = GamepadButton.A, - KickOne = GamepadButton.LeftBumper, - KickTwo = GamepadButton.RightBumper + KickOne = XboxGamepadButton.LeftBumper, + KickTwo = XboxGamepadButton.RightBumper } /// /// An input report from a drumkit. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct DrumInput + internal struct XboxDrumInput { public const byte CommandId = 0x20; diff --git a/Program/PacketParsing/Packets/Gamepad/GamepadInput.cs b/Program/PacketParsing/Packets/Gamepad/GamepadInput.cs deleted file mode 100644 index bee55d7..0000000 --- a/Program/PacketParsing/Packets/Gamepad/GamepadInput.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace RB4InstrumentMapper.Parsing -{ - /// - /// Flag definitions for the buttons bytes. - /// - [Flags] - internal enum GamepadButton : ushort - { - Sync = 0x0001, - // Unused = 0x0002, - Menu = 0x0004, - Options = 0x0008, - A = 0x0010, - B = 0x0020, - X = 0x0040, - Y = 0x0080, - DpadUp = 0x0100, - DpadDown = 0x0200, - DpadLeft = 0x0400, - DpadRight = 0x0800, - LeftBumper = 0x1000, - RightBumper = 0x2000, - LeftStickPress = 0x4000, - RightStickPress = 0x8000 - } - -#if DEBUG - - /// - /// An input report from a gamepad. - /// - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct GamepadInput - { - public const byte CommandId = 0x20; - - public const ushort TriggerMax = 0x03FF; - - public bool A => (Buttons & (ushort)GamepadButton.A) != 0; - public bool B => (Buttons & (ushort)GamepadButton.B) != 0; - public bool X => (Buttons & (ushort)GamepadButton.X) != 0; - public bool Y => (Buttons & (ushort)GamepadButton.Y) != 0; - - public bool DpadUp => (Buttons & (ushort)GamepadButton.DpadUp) != 0; - public bool DpadDown => (Buttons & (ushort)GamepadButton.DpadDown) != 0; - public bool DpadLeft => (Buttons & (ushort)GamepadButton.DpadLeft) != 0; - public bool DpadRight => (Buttons & (ushort)GamepadButton.DpadRight) != 0; - - public bool LeftBumper => (Buttons & (ushort)GamepadButton.LeftBumper) != 0; - public bool RightBumper => (Buttons & (ushort)GamepadButton.RightBumper) != 0; - public bool LeftStickPress => (Buttons & (ushort)GamepadButton.LeftStickPress) != 0; - public bool RightStickPress => (Buttons & (ushort)GamepadButton.RightStickPress) != 0; - - public bool Menu => (Buttons & (ushort)GamepadButton.Menu) != 0; - public bool Options => (Buttons & (ushort)GamepadButton.Options) != 0; - - public ushort Buttons; - public ushort LeftTrigger; - public ushort RightTrigger; - public short LeftStickX; - public short LeftStickY; - public short RightStickX; - public short RightStickY; - } - -#endif -} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/Gamepad/XboxGamepadInput.cs b/Program/PacketParsing/Packets/Gamepad/XboxGamepadInput.cs new file mode 100644 index 0000000..eb13892 --- /dev/null +++ b/Program/PacketParsing/Packets/Gamepad/XboxGamepadInput.cs @@ -0,0 +1,70 @@ +using System; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Flag definitions for the buttons bytes. + /// + [Flags] + internal enum XboxGamepadButton : ushort + { + Sync = 0x0001, + // Unused = 0x0002, + Menu = 0x0004, + Options = 0x0008, + A = 0x0010, + B = 0x0020, + X = 0x0040, + Y = 0x0080, + DpadUp = 0x0100, + DpadDown = 0x0200, + DpadLeft = 0x0400, + DpadRight = 0x0800, + LeftBumper = 0x1000, + RightBumper = 0x2000, + LeftStickPress = 0x4000, + RightStickPress = 0x8000 + } + +#if DEBUG + + /// + /// An input report from a gamepad. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct XboxGamepadInput + { + public const byte CommandId = 0x20; + + public const ushort TriggerMax = 0x03FF; + + public bool A => (Buttons & (ushort)XboxGamepadButton.A) != 0; + public bool B => (Buttons & (ushort)XboxGamepadButton.B) != 0; + public bool X => (Buttons & (ushort)XboxGamepadButton.X) != 0; + public bool Y => (Buttons & (ushort)XboxGamepadButton.Y) != 0; + + public bool DpadUp => (Buttons & (ushort)XboxGamepadButton.DpadUp) != 0; + public bool DpadDown => (Buttons & (ushort)XboxGamepadButton.DpadDown) != 0; + public bool DpadLeft => (Buttons & (ushort)XboxGamepadButton.DpadLeft) != 0; + public bool DpadRight => (Buttons & (ushort)XboxGamepadButton.DpadRight) != 0; + + public bool LeftBumper => (Buttons & (ushort)XboxGamepadButton.LeftBumper) != 0; + public bool RightBumper => (Buttons & (ushort)XboxGamepadButton.RightBumper) != 0; + public bool LeftStickPress => (Buttons & (ushort)XboxGamepadButton.LeftStickPress) != 0; + public bool RightStickPress => (Buttons & (ushort)XboxGamepadButton.RightStickPress) != 0; + + public bool Menu => (Buttons & (ushort)XboxGamepadButton.Menu) != 0; + public bool Options => (Buttons & (ushort)XboxGamepadButton.Options) != 0; + + public ushort Buttons; + public ushort LeftTrigger; + public ushort RightTrigger; + public short LeftStickX; + public short LeftStickY; + public short RightStickX; + public short RightStickY; + } + +#endif +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/Guitar/GuitarInput.cs b/Program/PacketParsing/Packets/Guitar/XboxGuitarInput.cs similarity index 52% rename from Program/PacketParsing/Packets/Guitar/GuitarInput.cs rename to Program/PacketParsing/Packets/Guitar/XboxGuitarInput.cs index a735c3f..f728434 100644 --- a/Program/PacketParsing/Packets/Guitar/GuitarInput.cs +++ b/Program/PacketParsing/Packets/Guitar/XboxGuitarInput.cs @@ -7,23 +7,23 @@ namespace RB4InstrumentMapper.Parsing /// Re-definitions for button flags that have specific meanings. /// [Flags] - internal enum GuitarButton : ushort + internal enum XboxGuitarButton : ushort { - StrumUp = GamepadButton.DpadUp, - StrumDown = GamepadButton.DpadDown, - GreenFret = GamepadButton.A, - RedFret = GamepadButton.B, - YellowFret = GamepadButton.Y, - BlueFret = GamepadButton.X, - OrangeFret = GamepadButton.LeftBumper, - LowerFretFlag = GamepadButton.LeftStickPress + StrumUp = XboxGamepadButton.DpadUp, + StrumDown = XboxGamepadButton.DpadDown, + GreenFret = XboxGamepadButton.A, + RedFret = XboxGamepadButton.B, + YellowFret = XboxGamepadButton.Y, + BlueFret = XboxGamepadButton.X, + OrangeFret = XboxGamepadButton.LeftBumper, + LowerFretFlag = XboxGamepadButton.LeftStickPress } /// - /// Flags used in and + /// Flags used in and /// [Flags] - internal enum GuitarFret : byte + internal enum XboxGuitarFret : byte { Green = 0x01, Red = 0x02, @@ -36,7 +36,7 @@ internal enum GuitarFret : byte /// An input report from a guitar. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct GuitarInput + internal struct XboxGuitarInput { public const byte CommandId = 0x20; @@ -50,12 +50,12 @@ internal struct GuitarInput private readonly byte unk2; private readonly byte unk3; - public bool Green => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Green) != 0; - public bool Red => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Red) != 0; - public bool Yellow => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Yellow) != 0; - public bool Blue => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Blue) != 0; - public bool Orange => ((UpperFrets | LowerFrets) & (byte)GuitarFret.Orange) != 0; + public bool Green => ((UpperFrets | LowerFrets) & (byte)XboxGuitarFret.Green) != 0; + public bool Red => ((UpperFrets | LowerFrets) & (byte)XboxGuitarFret.Red) != 0; + public bool Yellow => ((UpperFrets | LowerFrets) & (byte)XboxGuitarFret.Yellow) != 0; + public bool Blue => ((UpperFrets | LowerFrets) & (byte)XboxGuitarFret.Blue) != 0; + public bool Orange => ((UpperFrets | LowerFrets) & (byte)XboxGuitarFret.Orange) != 0; - public bool LowerFretFlag => (Buttons & (ushort)GuitarButton.LowerFretFlag) != 0; + public bool LowerFretFlag => (Buttons & (ushort)XboxGuitarButton.LowerFretFlag) != 0; } } \ No newline at end of file diff --git a/Program/PacketParsing/Packets/System/Acknowledgement.cs b/Program/PacketParsing/Packets/System/XboxAcknowledgement.cs similarity index 76% rename from Program/PacketParsing/Packets/System/Acknowledgement.cs rename to Program/PacketParsing/Packets/System/XboxAcknowledgement.cs index d7fe1ca..5084364 100644 --- a/Program/PacketParsing/Packets/System/Acknowledgement.cs +++ b/Program/PacketParsing/Packets/System/XboxAcknowledgement.cs @@ -10,7 +10,7 @@ namespace RB4InstrumentMapper.Parsing /// Used for communication reliability and error detection. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct Acknowledgement + internal struct XboxAcknowledgement { public const byte CommandId = 0x01; @@ -21,9 +21,9 @@ internal struct Acknowledgement private ushort unk2; public ushort RemainingBuffer; - public CommandFlags InnerFlags + public XboxCommandFlags InnerFlags { - get => (CommandFlags)(InnerFlags_Client & 0xF0); + get => (XboxCommandFlags)(InnerFlags_Client & 0xF0); set => InnerFlags_Client = (byte)((byte)value & 0xF0 | InnerClient); } @@ -33,14 +33,14 @@ public byte InnerClient set => InnerFlags_Client = (byte)((byte)InnerFlags | value & 0x0F); } - public static (CommandHeader header, Acknowledgement acknowledge) FromMessage(CommandHeader header, + public static (XboxCommandHeader header, XboxAcknowledgement acknowledge) FromMessage(XboxCommandHeader header, ReadOnlySpan messageBuffer) { // The Xbox One driver seems to always send this for the inner flag - header.Flags = CommandFlags.SystemCommand; + header.Flags = XboxCommandFlags.SystemCommand; // Set acknowledgement data - var acknowledge = new Acknowledgement() + var acknowledge = new XboxAcknowledgement() { unk1 = 0, InnerCommand = header.CommandId, @@ -55,8 +55,8 @@ public static (CommandHeader header, Acknowledgement acknowledge) FromMessage(Co return (header, acknowledge); } - public static (CommandHeader header, Acknowledgement acknowledge) FromMessage(CommandHeader header, - ReadOnlySpan messageBuffer, ChunkBuffer chunkBuffer) + public static (XboxCommandHeader header, XboxAcknowledgement acknowledge) FromMessage(XboxCommandHeader header, + ReadOnlySpan messageBuffer, XboxChunkBuffer chunkBuffer) { var pair = FromMessage(header, messageBuffer); diff --git a/Program/PacketParsing/Packets/System/DeviceArrival.cs b/Program/PacketParsing/Packets/System/XboxArrival.cs similarity index 92% rename from Program/PacketParsing/Packets/System/DeviceArrival.cs rename to Program/PacketParsing/Packets/System/XboxArrival.cs index 03329ff..39d928d 100644 --- a/Program/PacketParsing/Packets/System/DeviceArrival.cs +++ b/Program/PacketParsing/Packets/System/XboxArrival.cs @@ -6,7 +6,7 @@ namespace RB4InstrumentMapper.Parsing /// Indicates that a new device has connected and is awaiting initialization. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal readonly struct DeviceArrival + internal readonly struct XboxArrival { public const byte CommandId = 0x02; diff --git a/Program/PacketParsing/Packets/System/Authentication.cs b/Program/PacketParsing/Packets/System/XboxAuthentication.cs similarity index 68% rename from Program/PacketParsing/Packets/System/Authentication.cs rename to Program/PacketParsing/Packets/System/XboxAuthentication.cs index e9926d3..5f44814 100644 --- a/Program/PacketParsing/Packets/System/Authentication.cs +++ b/Program/PacketParsing/Packets/System/XboxAuthentication.cs @@ -1,15 +1,15 @@ namespace RB4InstrumentMapper.Parsing { - internal static class Authentication + internal static class XboxAuthentication { public const byte CommandId = 0x06; public static readonly XboxMessage SuccessMessage = new XboxMessage() { - Header = new CommandHeader() + Header = new XboxCommandHeader() { CommandId = CommandId, - Flags = CommandFlags.SystemCommand, + Flags = XboxCommandFlags.SystemCommand, }, Data = new byte[] { 0x01, 0x00 }, }; diff --git a/Program/PacketParsing/Packets/System/DeviceConfiguration.cs b/Program/PacketParsing/Packets/System/XboxConfiguration.cs similarity index 62% rename from Program/PacketParsing/Packets/System/DeviceConfiguration.cs rename to Program/PacketParsing/Packets/System/XboxConfiguration.cs index 0e8e993..861beb2 100644 --- a/Program/PacketParsing/Packets/System/DeviceConfiguration.cs +++ b/Program/PacketParsing/Packets/System/XboxConfiguration.cs @@ -2,7 +2,7 @@ namespace RB4InstrumentMapper.Parsing { - internal enum ConfigurationCommand : byte + internal enum XboxConfigurationCommand : byte { PowerOn = 0x00, Sleep = 0x01, @@ -12,25 +12,25 @@ internal enum ConfigurationCommand : byte } [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct DeviceConfiguration + internal struct XboxConfiguration { public const byte CommandId = 0x05; - public ConfigurationCommand SubCommand; + public XboxConfigurationCommand SubCommand; } [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct DeviceConfiguration + internal struct XboxConfiguration where TSub : unmanaged { - public ConfigurationCommand SubCommand; + public XboxConfigurationCommand SubCommand; public TSub SubData; } [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal unsafe struct WirelessPairing + internal unsafe struct XboxWirelessPairing { - public const ConfigurationCommand SubCommand = ConfigurationCommand.WirelessPairing; + public const XboxConfigurationCommand SubCommand = XboxConfigurationCommand.WirelessPairing; private fixed byte pairingAddress[6]; public ushort countryCode; diff --git a/Program/PacketParsing/Packets/System/Keystroke.cs b/Program/PacketParsing/Packets/System/XboxKeystroke.cs similarity index 71% rename from Program/PacketParsing/Packets/System/Keystroke.cs rename to Program/PacketParsing/Packets/System/XboxKeystroke.cs index 9254274..38bfb0c 100644 --- a/Program/PacketParsing/Packets/System/Keystroke.cs +++ b/Program/PacketParsing/Packets/System/XboxKeystroke.cs @@ -5,7 +5,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Flags for keystroke events. /// - internal enum KeystrokeFlags : byte + internal enum XboxKeystrokeFlags : byte { Pressed = 0x01, } @@ -16,7 +16,7 @@ internal enum KeystrokeFlags : byte /// /// These mirror those in the Win32 API; for brevity, only the ones used are defined here. /// - public enum KeyCode : byte + public enum XboxKeyCode : byte { LeftWindows = 0x5B, // Used for the guide button } @@ -25,13 +25,13 @@ public enum KeyCode : byte /// A keystroke event from a controller. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct Keystroke + internal struct XboxKeystroke { public const byte CommandId = 0x07; - public KeystrokeFlags Flags; - public KeyCode Keycode; + public XboxKeystrokeFlags Flags; + public XboxKeyCode Keycode; - public bool Pressed => (Flags & KeystrokeFlags.Pressed) != 0; + public bool Pressed => (Flags & XboxKeystrokeFlags.Pressed) != 0; } } \ No newline at end of file diff --git a/Program/PacketParsing/Packets/System/LedControl.cs b/Program/PacketParsing/Packets/System/XboxLedControl.cs similarity index 80% rename from Program/PacketParsing/Packets/System/LedControl.cs rename to Program/PacketParsing/Packets/System/XboxLedControl.cs index 3102810..23134e4 100644 --- a/Program/PacketParsing/Packets/System/LedControl.cs +++ b/Program/PacketParsing/Packets/System/XboxLedControl.cs @@ -2,7 +2,7 @@ namespace RB4InstrumentMapper.Parsing { - internal enum LedMode : byte + internal enum XboxLedMode : byte { Off = 0x00, On = 0x01, @@ -14,13 +14,13 @@ internal enum LedMode : byte } [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct LedControl + internal struct XboxLedControl { public const byte CommandId = 0x0a; private byte unknown; - public LedMode Mode; + public XboxLedMode Mode; public byte Brightness; } } diff --git a/Program/PacketParsing/Packets/System/DeviceStatus.cs b/Program/PacketParsing/Packets/System/XboxStatus.cs similarity index 75% rename from Program/PacketParsing/Packets/System/DeviceStatus.cs rename to Program/PacketParsing/Packets/System/XboxStatus.cs index ad2e0b4..9a425e9 100644 --- a/Program/PacketParsing/Packets/System/DeviceStatus.cs +++ b/Program/PacketParsing/Packets/System/XboxStatus.cs @@ -5,7 +5,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Available types of batteries that can be used on a controller. /// - internal enum BatteryType : byte + internal enum XboxBatteryType : byte { Wired = 0, Standard = 1, @@ -15,7 +15,7 @@ internal enum BatteryType : byte /// /// The amount of battery remaining on the controller. /// - internal enum BatteryLevel : byte + internal enum XboxBatteryLevel : byte { Low = 0, Medium = 1, @@ -29,7 +29,7 @@ internal enum BatteryLevel : byte /// Provides information about a device's current status, such as battery type and level. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal readonly struct DeviceStatus + internal readonly struct XboxStatus { public const byte CommandId = 0x03; @@ -39,7 +39,7 @@ internal readonly struct DeviceStatus private readonly byte unk3; public bool Connected => (status & 0b1100_0000) != 0; - public BatteryType BatteryType => (BatteryType)(status & 0b0000_1100); - public BatteryLevel BatteryLevel => (BatteryLevel)(status & 0b0000_0011); + public XboxBatteryType BatteryType => (XboxBatteryType)(status & 0b0000_1100); + public XboxBatteryLevel BatteryLevel => (XboxBatteryLevel)(status & 0b0000_0011); } } \ No newline at end of file diff --git a/Program/PacketParsing/Packets/ChunkBuffer.cs b/Program/PacketParsing/Packets/XboxChunkBuffer.cs similarity index 88% rename from Program/PacketParsing/Packets/ChunkBuffer.cs rename to Program/PacketParsing/Packets/XboxChunkBuffer.cs index 4c31f16..bda2f16 100644 --- a/Program/PacketParsing/Packets/ChunkBuffer.cs +++ b/Program/PacketParsing/Packets/XboxChunkBuffer.cs @@ -3,13 +3,13 @@ namespace RB4InstrumentMapper.Parsing { - internal class ChunkBuffer + internal class XboxChunkBuffer { public byte[] Buffer { get; private set; } public int BytesUsed { get; private set; } public int BytesRemaining => Buffer != null ? Buffer.Length - BytesUsed : 0; - public XboxResult ProcessChunk(ref CommandHeader header, ref ReadOnlySpan chunkData) + public XboxResult ProcessChunk(ref XboxCommandHeader header, ref ReadOnlySpan chunkData) { int bufferIndex = header.ChunkIndex; @@ -23,10 +23,10 @@ public XboxResult ProcessChunk(ref CommandHeader header, ref ReadOnlySpan } // Start of the chunk sequence - if (Buffer == null || (header.Flags & CommandFlags.ChunkStart) != 0) + if (Buffer == null || (header.Flags & XboxCommandFlags.ChunkStart) != 0) { // Safety check - if ((header.Flags & CommandFlags.ChunkStart) == 0) + if ((header.Flags & XboxCommandFlags.ChunkStart) == 0) { // NOTE: Older Xbox One gamepads trigger this condition during authentication // Not really an issue since we don't handle that anyways, noting for posterity @@ -63,7 +63,7 @@ public XboxResult ProcessChunk(ref CommandHeader header, ref ReadOnlySpan // Update header header.DataLength = chunkData.Length; - header.Flags &= ~(CommandFlags.ChunkPacket | CommandFlags.ChunkStart); + header.Flags &= ~(XboxCommandFlags.ChunkPacket | XboxCommandFlags.ChunkStart); return XboxResult.Success; } diff --git a/Program/PacketParsing/Packets/CommandHeader.cs b/Program/PacketParsing/Packets/XboxCommandHeader.cs similarity index 88% rename from Program/PacketParsing/Packets/CommandHeader.cs rename to Program/PacketParsing/Packets/XboxCommandHeader.cs index 9e609f7..e9c559d 100644 --- a/Program/PacketParsing/Packets/CommandHeader.cs +++ b/Program/PacketParsing/Packets/XboxCommandHeader.cs @@ -6,26 +6,11 @@ namespace RB4InstrumentMapper.Parsing { - /// - /// Command ID definitions. - /// - internal enum CommandId : byte - { - Acknowledgement = 0x01, - Arrival = 0x02, - Status = 0x03, - Descriptor = 0x04, - Authentication = 0x06, - Keystroke = 0x07, - SerialNumber = 0x1E, - Input = 0x20, - } - /// /// Command flag definitions. /// [Flags] - internal enum CommandFlags : byte + internal enum XboxCommandFlags : byte { None = 0, NeedsAcknowledgement = 0x10, @@ -38,7 +23,7 @@ internal enum CommandFlags : byte /// Header data for a message. /// [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct CommandHeader + internal struct XboxCommandHeader { public const int MinimumByteLength = 4; @@ -48,9 +33,9 @@ internal struct CommandHeader public int DataLength; public int ChunkIndex; - public CommandFlags Flags + public XboxCommandFlags Flags { - get => (CommandFlags)(Flags_Client & 0xF0); + get => (XboxCommandFlags)(Flags_Client & 0xF0); set => Flags_Client = (byte)((byte)value & 0xF0 | Client); } @@ -60,7 +45,7 @@ public byte Client set => Flags_Client = (byte)((byte)Flags | value & 0x0F); } - public static bool TryParse(ReadOnlySpan data, out CommandHeader header, out int bytesRead) + public static bool TryParse(ReadOnlySpan data, out XboxCommandHeader header, out int bytesRead) { header = default; bytesRead = 0; @@ -70,7 +55,7 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o } // Command info - header = new CommandHeader() + header = new XboxCommandHeader() { CommandId = data[0], Flags_Client = data[1], @@ -87,7 +72,7 @@ public static bool TryParse(ReadOnlySpan data, out CommandHeader header, o bytesRead += byteLength; // Chunk index/length - if ((header.Flags & CommandFlags.ChunkPacket) != 0) + if ((header.Flags & XboxCommandFlags.ChunkPacket) != 0) { if (!DecodeLEB128(data.Slice(bytesRead), out int chunkIndex, out byteLength)) { @@ -120,7 +105,7 @@ public bool TryWriteToBuffer(Span buffer, out int bytesWritten) bytesWritten += byteLength; // Chunk index/length - if ((Flags & CommandFlags.ChunkPacket) != 0) + if ((Flags & XboxCommandFlags.ChunkPacket) != 0) { if (!EncodeLEB128(buffer.Slice(bytesWritten), ChunkIndex, out byteLength)) return false; @@ -142,7 +127,7 @@ public int GetByteLength() size += length; // Chunk index - if ((Flags & CommandFlags.ChunkPacket) != 0) + if ((Flags & XboxCommandFlags.ChunkPacket) != 0) { success = EncodeLEB128(encodeBuffer, ChunkIndex, out length); Debug.Assert(success, "Failed to get byte length for chunk index!"); diff --git a/Program/PacketParsing/Packets/XboxMessage.cs b/Program/PacketParsing/Packets/XboxMessage.cs index e0fa191..c867c6e 100644 --- a/Program/PacketParsing/Packets/XboxMessage.cs +++ b/Program/PacketParsing/Packets/XboxMessage.cs @@ -2,10 +2,10 @@ namespace RB4InstrumentMapper.Parsing { internal class XboxMessage { - private CommandHeader _header; + private XboxCommandHeader _header; private byte[] _data; - public CommandHeader Header + public XboxCommandHeader Header { get => _header; set @@ -29,10 +29,10 @@ public byte[] Data internal unsafe class XboxMessage where TData : unmanaged { - private CommandHeader _header; + private XboxCommandHeader _header; public TData Data; - public CommandHeader Header + public XboxCommandHeader Header { get => _header; set diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index b19d86e..b3133e5 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -13,37 +13,37 @@ internal class XboxClient : IDisposable #region Message definitions private static readonly XboxMessage GetDescriptor = new XboxMessage() { - Header = new CommandHeader() + Header = new XboxCommandHeader() { CommandId = XboxDescriptor.CommandId, - Flags = CommandFlags.SystemCommand, + Flags = XboxCommandFlags.SystemCommand, }, // Header only, no data }; - private static readonly XboxMessage PowerOnDevice = new XboxMessage() + private static readonly XboxMessage PowerOnDevice = new XboxMessage() { - Header = new CommandHeader() + Header = new XboxCommandHeader() { - CommandId = DeviceConfiguration.CommandId, - Flags = CommandFlags.SystemCommand, + CommandId = XboxConfiguration.CommandId, + Flags = XboxCommandFlags.SystemCommand, }, - Data = new DeviceConfiguration() + Data = new XboxConfiguration() { - SubCommand = ConfigurationCommand.PowerOn, + SubCommand = XboxConfigurationCommand.PowerOn, } }; - private static readonly XboxMessage EnableLed = new XboxMessage() + private static readonly XboxMessage EnableLed = new XboxMessage() { - Header = new CommandHeader() + Header = new XboxCommandHeader() { - CommandId = LedControl.CommandId, - Flags = CommandFlags.SystemCommand, + CommandId = XboxLedControl.CommandId, + Flags = XboxCommandFlags.SystemCommand, }, - Data = new LedControl() + Data = new XboxLedControl() { - Mode = LedMode.On, + Mode = XboxLedMode.On, Brightness = 0x14 } }; @@ -68,9 +68,9 @@ internal class XboxClient : IDisposable private readonly Dictionary previousReceiveSequence = new Dictionary(); private readonly Dictionary previousSendSequence = new Dictionary(); - private readonly Dictionary chunkBuffers = new Dictionary() + private readonly Dictionary chunkBuffers = new Dictionary() { - { XboxDescriptor.CommandId, new ChunkBuffer() }, + { XboxDescriptor.CommandId, new XboxChunkBuffer() }, }; public XboxClient(XboxDevice parent, byte clientId) @@ -87,7 +87,7 @@ public XboxClient(XboxDevice parent, byte clientId) /// /// Parses command data from a packet. /// - internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan commandData) + internal unsafe XboxResult HandleMessage(XboxCommandHeader header, ReadOnlySpan commandData) { // Verify packet length if (header.DataLength != commandData.Length) @@ -100,11 +100,11 @@ internal unsafe XboxResult HandleMessage(CommandHeader header, ReadOnlySpan comman { switch (commandId) { - case DeviceArrival.CommandId: + case XboxArrival.CommandId: return HandleArrival(commandData); - case DeviceStatus.CommandId: + case XboxStatus.CommandId: return HandleStatus(commandData); case XboxDescriptor.CommandId: return HandleDescriptor(commandData); - case Keystroke.CommandId: + case XboxKeystroke.CommandId: return HandleKeystroke(commandData); } @@ -187,7 +187,7 @@ private XboxResult HandleSystemCommand(byte commandId, ReadOnlySpan comman /// private unsafe XboxResult HandleArrival(ReadOnlySpan data) { - if (data.Length < sizeof(DeviceArrival) || MemoryMarshal.TryRead(data, out DeviceArrival arrival)) + if (data.Length < sizeof(XboxArrival) || MemoryMarshal.TryRead(data, out XboxArrival arrival)) return XboxResult.InvalidMessage; Console.WriteLine($"New client connected with ID {arrival.SerialNumber:X12}"); @@ -201,7 +201,7 @@ private unsafe XboxResult HandleArrival(ReadOnlySpan data) /// private unsafe XboxResult HandleStatus(ReadOnlySpan data) { - if (data.Length < sizeof(DeviceStatus) || !MemoryMarshal.TryRead(data, out DeviceStatus status)) + if (data.Length < sizeof(XboxStatus) || !MemoryMarshal.TryRead(data, out XboxStatus status)) return XboxResult.InvalidMessage; if (!status.Connected) @@ -225,22 +225,22 @@ private XboxResult HandleDescriptor(ReadOnlySpan data) deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, XboxDevice.MapperMode); // Send final set of initialization messages - Debug.Assert(Descriptor.OutputCommands.Contains(DeviceConfiguration.CommandId)); + Debug.Assert(Descriptor.OutputCommands.Contains(XboxConfiguration.CommandId)); var result = SendMessage(PowerOnDevice); if (result != XboxResult.Success) return result; - if (Descriptor.OutputCommands.Contains(LedControl.CommandId)) + if (Descriptor.OutputCommands.Contains(XboxLedControl.CommandId)) { result = SendMessage(EnableLed); if (result != XboxResult.Success) return result; } - if (Descriptor.OutputCommands.Contains(Authentication.CommandId)) + if (Descriptor.OutputCommands.Contains(XboxAuthentication.CommandId)) { // Authentication is not and will not be implemented, we just automatically pass all devices - result = SendMessage(Authentication.SuccessMessage); + result = SendMessage(XboxAuthentication.SuccessMessage); if (result != XboxResult.Success) return result; } @@ -250,11 +250,11 @@ private XboxResult HandleDescriptor(ReadOnlySpan data) private unsafe XboxResult HandleKeystroke(ReadOnlySpan data) { - if (data.Length % sizeof(Keystroke) != 0) + if (data.Length % sizeof(XboxKeystroke) != 0) return XboxResult.InvalidMessage; // Multiple keystrokes can be sent in a single message - var keys = MemoryMarshal.Cast(data); + var keys = MemoryMarshal.Cast(data); foreach (var key in keys) { deviceMapper.HandleKeystroke(key); @@ -274,26 +274,26 @@ internal unsafe XboxResult SendMessage(XboxMessage message) return SendMessage(message.Header, ref message.Data); } - internal unsafe XboxResult SendMessage(CommandHeader header) + internal unsafe XboxResult SendMessage(XboxCommandHeader header) { SetUpHeader(ref header); return Parent.SendMessage(header); } - internal unsafe XboxResult SendMessage(CommandHeader header, ref T data) + internal unsafe XboxResult SendMessage(XboxCommandHeader header, ref T data) where T : unmanaged { SetUpHeader(ref header); return Parent.SendMessage(header, ref data); } - internal XboxResult SendMessage(CommandHeader header, Span data) + internal XboxResult SendMessage(XboxCommandHeader header, Span data) { SetUpHeader(ref header); return Parent.SendMessage(header, data); } - private void SetUpHeader(ref CommandHeader header) + private void SetUpHeader(ref XboxCommandHeader header) { header.Client = ClientId; diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 2787d5e..8f23293 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -58,7 +58,7 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) while (data.Length > 0) { // Command header - if (!CommandHeader.TryParse(data, out var header, out int headerLength)) + if (!XboxCommandHeader.TryParse(data, out var header, out int headerLength)) { return XboxResult.InvalidMessage; } @@ -110,12 +110,12 @@ internal unsafe XboxResult SendMessage(XboxMessage message) return SendMessage(message.Header, ref message.Data); } - internal unsafe XboxResult SendMessage(CommandHeader header) + internal unsafe XboxResult SendMessage(XboxCommandHeader header) { return SendMessage(header, Span.Empty); } - internal unsafe XboxResult SendMessage(CommandHeader header, ref T data) + internal unsafe XboxResult SendMessage(XboxCommandHeader header, ref T data) where T : unmanaged { // Create a byte buffer for the given data @@ -124,10 +124,10 @@ internal unsafe XboxResult SendMessage(CommandHeader header, ref T data) } // TODO: Span instead of ReadOnlySpan since the WinUSB lib doesn't use ReadOnlySpan for writing atm - internal XboxResult SendMessage(CommandHeader header, Span data) + internal XboxResult SendMessage(XboxCommandHeader header, Span data) { // For devices handled by Pcap and not over USB - if (maxPacketSize < CommandHeader.MinimumByteLength) + if (maxPacketSize < XboxCommandHeader.MinimumByteLength) return XboxResult.Success; // Initialize lengths diff --git a/Program/PacketParsing/DeviceGuids.cs b/Program/PacketParsing/XboxDeviceGuids.cs similarity index 95% rename from Program/PacketParsing/DeviceGuids.cs rename to Program/PacketParsing/XboxDeviceGuids.cs index 0348472..1c50d11 100644 --- a/Program/PacketParsing/DeviceGuids.cs +++ b/Program/PacketParsing/XboxDeviceGuids.cs @@ -5,7 +5,7 @@ namespace RB4InstrumentMapper.Parsing /// /// Xbox device interface GUIDs. /// - internal static class DeviceGuids + internal static class XboxDeviceGuids { public static readonly Guid XboxInputDevice = Guid.Parse("9776FF56-9BFD-4581-AD45-B645BBA526D6"); public static readonly Guid XboxNavigationController = Guid.Parse("B8F31FE7-7386-40E9-A9F8-2F21263ACFB7"); diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index c69b192..d226c6c 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -129,24 +129,24 @@ - - - - - - - - - - + + + + + + + - - + + + + + - + From ef5442cf80c87a22246c5b9ef2e5b3c3eefbf06e Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 20:57:33 -0600 Subject: [PATCH 202/437] Hook up WinUSB backend to UI --- Program/MainWindow/MainWindow.xaml.cs | 132 +++++++++++------- .../PacketParsing/Backends/WinUsbBackend.cs | 46 +++++- 2 files changed, 125 insertions(+), 53 deletions(-) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 9326cd8..04ff458 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -160,6 +160,9 @@ private void Window_Loaded(object sender, RoutedEventArgs e) return; } + + WinUsbBackend.DeviceAddedOrRemoved += WinUsbDeviceAddedOrRemoved; + WinUsbBackend.Initialize(); } /// @@ -173,6 +176,9 @@ private void Window_Closed(object sender, EventArgs e) StopCapture(); } + WinUsbBackend.Uninitialize(); + WinUsbBackend.DeviceAddedOrRemoved -= WinUsbDeviceAddedOrRemoved; + // Close the log files Logging.CloseAll(); @@ -238,11 +244,17 @@ private void PopulatePcapDropdown() Console.WriteLine($"Discovered {pcapDeviceList.Count} Pcap devices."); } - private void SetStartButtonEnabled() + private void SetStartButtonState() { startButton.IsEnabled = - controllerDeviceTypeCombo.SelectedIndex != (int)ControllerType.None && - pcapDeviceCombo.SelectedIndex != -1; + WinUsbBackend.DeviceCount > 0 || + (controllerDeviceTypeCombo.SelectedIndex != (int)ControllerType.None && + pcapDeviceCombo.SelectedIndex != -1); + } + + private void WinUsbDeviceAddedOrRemoved() + { + uiDispatcher.Invoke(SetStartButtonState); } /// @@ -250,41 +262,10 @@ private void SetStartButtonEnabled() /// private void StartCapture() { - // Check if a device has been selected - if (pcapSelectedDevice == null) - { - Console.WriteLine("Please select a Pcap device from the Pcap dropdown."); - return; - } - - // Check if the device is still present - var pcapDeviceList = CaptureDeviceList.Instance; - pcapDeviceList.Refresh(); - bool deviceStillPresent = false; - foreach (var device in pcapDeviceList) - { - if (device.Name == pcapSelectedDevice.Name) - { - deviceStillPresent = true; - break; - } - } - - if (!deviceStillPresent) + if (!StartPcapCapture() || !StartWinUsbCapture()) { - // Invalidate selected device (but not the saved preference) - pcapSelectedDevice = null; - - // Notify user - MessageBox.Show( - "Pcap device list has changed and the selected device is no longer present.\nPlease re-select your device from the list and try again.", - "Pcap Device Not Found", - MessageBoxButton.OK, - MessageBoxImage.Exclamation - ); - - // Force a refresh - PopulatePcapDropdown(); + StopPcapCapture(); + StopWinUsbCapture(); return; } @@ -311,12 +292,6 @@ private void StartCapture() Console.WriteLine("Disabled packet logging for this capture session."); } } - - // Start capture - PcapBackend.LogPackets = packetDebug; - PcapBackend.OnCaptureStop += OnCaptureStopped; - PcapBackend.StartCapture(pcapSelectedDevice); - Console.WriteLine($"Listening on {pcapSelectedDevice.GetDisplayName()}..."); } private void OnCaptureStopped() @@ -330,7 +305,8 @@ private void OnCaptureStopped() /// private void StopCapture() { - PcapBackend.StopCapture(); + StopPcapCapture(); + StopWinUsbCapture(); // Store whether or not the packet log was created bool doPacketLogMessage = Logging.PacketLogExists; @@ -361,6 +337,68 @@ private void StopCapture() } } + private bool StartPcapCapture() + { + // Ignore if no device is selected + if (pcapSelectedDevice == null) + return true; + + // Check if the device is still present + var pcapDeviceList = CaptureDeviceList.Instance; + pcapDeviceList.Refresh(); + bool deviceStillPresent = false; + foreach (var device in pcapDeviceList) + { + if (device.Name == pcapSelectedDevice.Name) + { + deviceStillPresent = true; + break; + } + } + + if (!deviceStillPresent) + { + // Invalidate selected device (but not the saved preference) + pcapSelectedDevice = null; + + // Notify user + MessageBox.Show( + "Pcap device list has changed and the selected device is no longer present.\nPlease re-select your device from the list and try again.", + "Pcap Device Not Found", + MessageBoxButton.OK, + MessageBoxImage.Exclamation + ); + + // Force a refresh + PopulatePcapDropdown(); + return false; + } + + // Start capture + PcapBackend.LogPackets = packetDebug; + PcapBackend.OnCaptureStop += OnCaptureStopped; + PcapBackend.StartCapture(pcapSelectedDevice); + Console.WriteLine($"Listening on {pcapSelectedDevice.GetDisplayName()}..."); + + return true; + } + + private bool StartWinUsbCapture() + { + WinUsbBackend.Start(); + return true; + } + + private void StopPcapCapture() + { + PcapBackend.StopCapture(); + } + + private void StopWinUsbCapture() + { + WinUsbBackend.Stop(); + } + /// /// Handles Pcap device selection changes. /// @@ -372,7 +410,7 @@ private void pcapDeviceCombo_SelectionChanged(object sender, SelectionChangedEve if (!(pcapDeviceCombo.SelectedItem is ComboBoxItem selection)) { // Disable start button - startButton.IsEnabled = false; + SetStartButtonState(); // Clear saved device Settings.Default.pcapDevice = String.Empty; @@ -392,7 +430,7 @@ private void pcapDeviceCombo_SelectionChanged(object sender, SelectionChangedEve Console.WriteLine($"Selected Pcap device {pcapSelectedDevice.GetDisplayName()}"); // Enable start button - SetStartButtonEnabled(); + SetStartButtonState(); // Remember selected Pcap device's name Settings.Default.pcapDevice = pcapSelectedDevice.Name; @@ -524,7 +562,7 @@ private void controllerDeviceTypeCombo_SelectionChanged(object sender, Selection Settings.Default.Save(); // Enable start button - SetStartButtonEnabled(); + SetStartButtonState(); } /// diff --git a/Program/PacketParsing/Backends/WinUsbBackend.cs b/Program/PacketParsing/Backends/WinUsbBackend.cs index de72556..94f4993 100644 --- a/Program/PacketParsing/Backends/WinUsbBackend.cs +++ b/Program/PacketParsing/Backends/WinUsbBackend.cs @@ -1,18 +1,22 @@ +using System; using System.Collections.Generic; using Nefarius.Drivers.WinUSB; using Nefarius.Utilities.DeviceManagement.PnP; -// TODO: Doesn't actually work yet, need to send data back to the device - namespace RB4InstrumentMapper.Parsing { public static class WinUsbBackend { private static readonly DeviceNotificationListener watcher = new DeviceNotificationListener(); - private static readonly Dictionary devices - = new Dictionary(); + private static readonly Dictionary devices = new Dictionary(); - public static void Start() + public static int DeviceCount => devices.Count; + + public static event Action DeviceAddedOrRemoved; + + private static bool started = false; + + public static void Initialize() { foreach (var deviceInfo in USBDevice.GetDevices(DeviceInterfaceIds.UsbDevice)) { @@ -24,7 +28,7 @@ public static void Start() watcher.StartListen(DeviceInterfaceIds.UsbDevice); } - public static void Stop() + public static void Uninitialize() { watcher.StopListen(); watcher.DeviceArrived -= DeviceArrived; @@ -37,6 +41,32 @@ public static void Stop() devices.Clear(); } + public static void Start() + { + if (started) + return; + + foreach (var device in devices.Values) + { + device.StartReading(); + } + + started = true; + } + + public static void Stop() + { + if (!started) + return; + + foreach (var device in devices.Values) + { + device.StopReading(); + } + + started = false; + } + private static void DeviceArrived(DeviceEventArgs args) { AddDevice(args.SymLink); @@ -54,6 +84,8 @@ private static void AddDevice(string devicePath) return; devices.Add(devicePath, device); + + DeviceAddedOrRemoved?.Invoke(); } private static void RemoveDevice(string devicePath) @@ -63,6 +95,8 @@ private static void RemoveDevice(string devicePath) devices.Remove(devicePath); device.Dispose(); + + DeviceAddedOrRemoved?.Invoke(); } } } \ No newline at end of file From 36492fce34362a3d71729e66cc215494825d6d58 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 20:59:02 -0600 Subject: [PATCH 203/437] Fix USB device disconnection --- Program/PacketParsing/Backends/XboxWinUsbDevice.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index c9212a7..6743ac4 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -69,7 +69,6 @@ public void StopReading() return; readPackets = false; - mainInterface.InPipe.Abort(); readThread.Join(); readThread = null; } @@ -104,6 +103,8 @@ private void ReadThread() break; } } + + readPackets = false; } private int ReadPacket(Span readBuffer) From 8c0e870cf7fc78082da729dcc65b0cc647742bf2 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 21:02:04 -0600 Subject: [PATCH 204/437] Better read/write logging --- Program/PacketParsing/Backends/XboxWinUsbDevice.cs | 5 +++-- Program/PacketParsing/XboxDevice.cs | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index 6743ac4..4582bec 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -94,8 +94,9 @@ private void ReadThread() } // Process packet data - Debug.WriteLine(ParsingUtils.ToString(readBuffer)); - var result = HandlePacket(readBuffer.Slice(0, bytesRead)); + var packet = readBuffer.Slice(0, bytesRead); + Debug.WriteLine($"-> {ParsingUtils.ToString(packet)}"); + var result = HandlePacket(packet); switch (result) { case XboxResult.InvalidMessage: diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 8f23293..28ca0ac 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -152,6 +152,8 @@ internal XboxResult SendMessage(XboxCommandHeader header, Span data) return XboxResult.InvalidMessage; } + Debug.WriteLine($"<- {ParsingUtils.ToString(packetBuffer)}"); + // Attempt a few times const int retryThreshold = 3; int retryCount = 0; From b5a2d6061fd2e3894d324a1af9659b80fbdff523 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 21:11:03 -0600 Subject: [PATCH 205/437] of course a single missing character breaks the whole thing lol --- Program/PacketParsing/XboxClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index b3133e5..66c2d54 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -187,7 +187,7 @@ private XboxResult HandleSystemCommand(byte commandId, ReadOnlySpan comman /// private unsafe XboxResult HandleArrival(ReadOnlySpan data) { - if (data.Length < sizeof(XboxArrival) || MemoryMarshal.TryRead(data, out XboxArrival arrival)) + if (data.Length < sizeof(XboxArrival) || !MemoryMarshal.TryRead(data, out XboxArrival arrival)) return XboxResult.InvalidMessage; Console.WriteLine($"New client connected with ID {arrival.SerialNumber:X12}"); From bff6d20504e1c076e3bb024514583c616a254fff Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 21:19:08 -0600 Subject: [PATCH 206/437] Adjust message error logging and log disconnect message --- Program/PacketParsing/Backends/XboxWinUsbDevice.cs | 7 +++---- Program/PacketParsing/XboxDevice.cs | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index 4582bec..8174e89 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -97,11 +97,10 @@ private void ReadThread() var packet = readBuffer.Slice(0, bytesRead); Debug.WriteLine($"-> {ParsingUtils.ToString(packet)}"); var result = HandlePacket(packet); - switch (result) + if (result == XboxResult.Disconnected) { - case XboxResult.InvalidMessage: - Debug.WriteLine($"Invalid packet received!"); - break; + Debug.WriteLine("Disconnection message received, stopping device read"); + break; } } diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 28ca0ac..2b2b89a 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -87,8 +87,7 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) case XboxResult.Disconnected: return clientResult; default: - if (data.Length < 1) - return clientResult; + Debug.WriteLine($"Error handling message: {clientResult}"); break; } From 8b6074cd000944a01046f27ee4a7ad9ec8ed1b85 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 21:23:24 -0600 Subject: [PATCH 207/437] More proper client disconnection handling --- Program/PacketParsing/Backends/XboxWinUsbDevice.cs | 7 +------ Program/PacketParsing/Packets/XboxCommandHeader.cs | 2 ++ Program/PacketParsing/XboxDevice.cs | 5 ++++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index 8174e89..5f82948 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -96,12 +96,7 @@ private void ReadThread() // Process packet data var packet = readBuffer.Slice(0, bytesRead); Debug.WriteLine($"-> {ParsingUtils.ToString(packet)}"); - var result = HandlePacket(packet); - if (result == XboxResult.Disconnected) - { - Debug.WriteLine("Disconnection message received, stopping device read"); - break; - } + HandlePacket(packet); } readPackets = false; diff --git a/Program/PacketParsing/Packets/XboxCommandHeader.cs b/Program/PacketParsing/Packets/XboxCommandHeader.cs index e9c559d..a0de8d0 100644 --- a/Program/PacketParsing/Packets/XboxCommandHeader.cs +++ b/Program/PacketParsing/Packets/XboxCommandHeader.cs @@ -27,6 +27,8 @@ internal struct XboxCommandHeader { public const int MinimumByteLength = 4; + public const byte PrimaryClient = 0; + public byte CommandId; public byte Flags_Client; public byte SequenceCount; diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 2b2b89a..8f9ad7b 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -85,7 +85,10 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) case XboxResult.Pending: break; case XboxResult.Disconnected: - return clientResult; + client.Dispose(); + clients.Remove(header.Client); + Debug.WriteLine($"Client {header.Client} disconnected"); + break; default: Debug.WriteLine($"Error handling message: {clientResult}"); break; From c7b89d79dc503a7db0eb734f91044bd9cd2adb53 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 21:25:33 -0600 Subject: [PATCH 208/437] Use the correct endpoint when writing --- Program/PacketParsing/Backends/XboxWinUsbDevice.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index 5f82948..eea96bc 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -119,7 +119,7 @@ protected override XboxResult SendPacket(Span data) { try { - mainInterface.InPipe.Write(data); + mainInterface.OutPipe.Write(data); return XboxResult.Success; } catch (Exception ex) From 0bce81a2d816adf0de4d2a97bbc80886acf5632c Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 21:42:02 -0600 Subject: [PATCH 209/437] Move constant message definitions to their respective types --- .../Packets/System/XboxConfiguration.cs | 13 ++++++ .../Packets/System/XboxDescriptor.cs | 12 ++++- .../Packets/System/XboxLedControl.cs | 14 ++++++ Program/PacketParsing/XboxClient.cs | 45 ++----------------- 4 files changed, 41 insertions(+), 43 deletions(-) diff --git a/Program/PacketParsing/Packets/System/XboxConfiguration.cs b/Program/PacketParsing/Packets/System/XboxConfiguration.cs index 861beb2..8a06f47 100644 --- a/Program/PacketParsing/Packets/System/XboxConfiguration.cs +++ b/Program/PacketParsing/Packets/System/XboxConfiguration.cs @@ -16,6 +16,19 @@ internal struct XboxConfiguration { public const byte CommandId = 0x05; + public static readonly XboxMessage PowerOnDevice = new XboxMessage() + { + Header = new XboxCommandHeader() + { + CommandId = CommandId, + Flags = XboxCommandFlags.SystemCommand, + }, + Data = new XboxConfiguration() + { + SubCommand = XboxConfigurationCommand.PowerOn, + } + }; + public XboxConfigurationCommand SubCommand; } diff --git a/Program/PacketParsing/Packets/System/XboxDescriptor.cs b/Program/PacketParsing/Packets/System/XboxDescriptor.cs index ef7455f..a5a021e 100644 --- a/Program/PacketParsing/Packets/System/XboxDescriptor.cs +++ b/Program/PacketParsing/Packets/System/XboxDescriptor.cs @@ -11,7 +11,7 @@ namespace RB4InstrumentMapper.Parsing /// /// A large amount of the descriptor data is ignored, only data necessary for identifying device types is read. /// - public class XboxDescriptor + internal class XboxDescriptor { [StructLayout(LayoutKind.Sequential, Pack = 1)] private struct Header @@ -38,6 +38,16 @@ private struct Offsets private ushort unk3; } + public static readonly XboxMessage GetDescriptor = new XboxMessage() + { + Header = new XboxCommandHeader() + { + CommandId = CommandId, + Flags = XboxCommandFlags.SystemCommand, + }, + // Header only, no data + }; + public const byte CommandId = 0x04; public HashSet InputCommands { get; private set; } diff --git a/Program/PacketParsing/Packets/System/XboxLedControl.cs b/Program/PacketParsing/Packets/System/XboxLedControl.cs index 23134e4..6a228da 100644 --- a/Program/PacketParsing/Packets/System/XboxLedControl.cs +++ b/Program/PacketParsing/Packets/System/XboxLedControl.cs @@ -18,6 +18,20 @@ internal struct XboxLedControl { public const byte CommandId = 0x0a; + public static readonly XboxMessage EnableLed = new XboxMessage() + { + Header = new XboxCommandHeader() + { + CommandId = CommandId, + Flags = XboxCommandFlags.SystemCommand, + }, + Data = new XboxLedControl() + { + Mode = XboxLedMode.On, + Brightness = 0x14 + } + }; + private byte unknown; public XboxLedMode Mode; diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 66c2d54..9c6a2f7 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -10,45 +10,6 @@ namespace RB4InstrumentMapper.Parsing /// internal class XboxClient : IDisposable { - #region Message definitions - private static readonly XboxMessage GetDescriptor = new XboxMessage() - { - Header = new XboxCommandHeader() - { - CommandId = XboxDescriptor.CommandId, - Flags = XboxCommandFlags.SystemCommand, - }, - // Header only, no data - }; - - private static readonly XboxMessage PowerOnDevice = new XboxMessage() - { - Header = new XboxCommandHeader() - { - CommandId = XboxConfiguration.CommandId, - Flags = XboxCommandFlags.SystemCommand, - }, - Data = new XboxConfiguration() - { - SubCommand = XboxConfigurationCommand.PowerOn, - } - }; - - private static readonly XboxMessage EnableLed = new XboxMessage() - { - Header = new XboxCommandHeader() - { - CommandId = XboxLedControl.CommandId, - Flags = XboxCommandFlags.SystemCommand, - }, - Data = new XboxLedControl() - { - Mode = XboxLedMode.On, - Brightness = 0x14 - } - }; - #endregion - /// /// The parent device of the client. /// @@ -193,7 +154,7 @@ private unsafe XboxResult HandleArrival(ReadOnlySpan data) Console.WriteLine($"New client connected with ID {arrival.SerialNumber:X12}"); // Kick off descriptor request - return SendMessage(GetDescriptor); + return SendMessage(XboxDescriptor.GetDescriptor); } /// @@ -226,13 +187,13 @@ private XboxResult HandleDescriptor(ReadOnlySpan data) // Send final set of initialization messages Debug.Assert(Descriptor.OutputCommands.Contains(XboxConfiguration.CommandId)); - var result = SendMessage(PowerOnDevice); + var result = SendMessage(XboxConfiguration.PowerOnDevice); if (result != XboxResult.Success) return result; if (Descriptor.OutputCommands.Contains(XboxLedControl.CommandId)) { - result = SendMessage(EnableLed); + result = SendMessage(XboxLedControl.EnableLed); if (result != XboxResult.Success) return result; } From e4480457641cf940d6a41e842889cdb209bfdd39 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 22:05:12 -0600 Subject: [PATCH 210/437] Reset USB device when stopping capture and on app exit Fixes Stop blocking until new message is received --- Program/PacketParsing/Backends/XboxWinUsbDevice.cs | 2 ++ .../Packets/System/XboxConfiguration.cs | 13 +++++++++++++ Program/PacketParsing/XboxClient.cs | 5 +++++ 3 files changed, 20 insertions(+) diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index eea96bc..c130de4 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -65,6 +65,8 @@ public void StartReading() public void StopReading() { + // Always reset device + SendMessage(XboxConfiguration.ResetDevice); if (!readPackets) return; diff --git a/Program/PacketParsing/Packets/System/XboxConfiguration.cs b/Program/PacketParsing/Packets/System/XboxConfiguration.cs index 8a06f47..0f7ae77 100644 --- a/Program/PacketParsing/Packets/System/XboxConfiguration.cs +++ b/Program/PacketParsing/Packets/System/XboxConfiguration.cs @@ -29,6 +29,19 @@ internal struct XboxConfiguration } }; + public static readonly XboxMessage ResetDevice = new XboxMessage() + { + Header = new XboxCommandHeader() + { + CommandId = CommandId, + Flags = XboxCommandFlags.SystemCommand, + }, + Data = new XboxConfiguration() + { + SubCommand = XboxConfigurationCommand.Reset, + } + }; + public XboxConfigurationCommand SubCommand; } diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 9c6a2f7..6d6a4df 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -25,6 +25,7 @@ internal class XboxClient : IDisposable /// public byte ClientId { get; } + private bool arrivalReceived = false; private IDeviceMapper deviceMapper; private readonly Dictionary previousReceiveSequence = new Dictionary(); @@ -148,10 +149,14 @@ private XboxResult HandleSystemCommand(byte commandId, ReadOnlySpan comman /// private unsafe XboxResult HandleArrival(ReadOnlySpan data) { + if (arrivalReceived) + return XboxResult.Success; + if (data.Length < sizeof(XboxArrival) || !MemoryMarshal.TryRead(data, out XboxArrival arrival)) return XboxResult.InvalidMessage; Console.WriteLine($"New client connected with ID {arrival.SerialNumber:X12}"); + arrivalReceived = true; // Kick off descriptor request return SendMessage(XboxDescriptor.GetDescriptor); From 3e45e428e848be8a790d3a33e57493ffc855a5de Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 22:42:38 -0600 Subject: [PATCH 211/437] Fix a couple issues with (dis)connecting devices after starting capture --- Program/PacketParsing/Backends/WinUsbBackend.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Program/PacketParsing/Backends/WinUsbBackend.cs b/Program/PacketParsing/Backends/WinUsbBackend.cs index 94f4993..f5f5e84 100644 --- a/Program/PacketParsing/Backends/WinUsbBackend.cs +++ b/Program/PacketParsing/Backends/WinUsbBackend.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using Nefarius.Drivers.WinUSB; using Nefarius.Utilities.DeviceManagement.PnP; @@ -79,23 +80,31 @@ private static void DeviceRemoved(DeviceEventArgs args) private static void AddDevice(string devicePath) { + // Paths are case-insensitive + devicePath = devicePath.ToLowerInvariant(); var device = XboxWinUsbDevice.TryCreate(devicePath); if (device == null) return; devices.Add(devicePath, device); + if (started) + device.StartReading(); + Debug.WriteLine($"Added device {devicePath}"); DeviceAddedOrRemoved?.Invoke(); } private static void RemoveDevice(string devicePath) { + // Paths are case-insensitive + devicePath = devicePath.ToLowerInvariant(); if (!devices.TryGetValue(devicePath, out var device)) return; devices.Remove(devicePath); device.Dispose(); + Debug.WriteLine($"Removed device {devicePath}"); DeviceAddedOrRemoved?.Invoke(); } } From 4569c15f477e91b31ee55d6f2a7202e3fa061b63 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 22 Aug 2023 22:43:43 -0600 Subject: [PATCH 212/437] Fix Stop button disabling when disconnecting all USB devices --- Program/MainWindow/MainWindow.xaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 04ff458..110ce2e 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -248,6 +248,7 @@ private void SetStartButtonState() { startButton.IsEnabled = WinUsbBackend.DeviceCount > 0 || + packetCaptureActive || (controllerDeviceTypeCombo.SelectedIndex != (int)ControllerType.None && pcapDeviceCombo.SelectedIndex != -1); } From 265f7a55af94147679f45420b8a133c4453daadc Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 23 Aug 2023 03:45:32 -0600 Subject: [PATCH 213/437] Greatly refactor device mapper architecture --- Program/PacketParsing/Mappers/DeviceMapper.cs | 92 ++++++++++++++ .../PacketParsing/Mappers/IDeviceMapper.cs | 22 ---- .../PacketParsing/Mappers/MapperFactory.cs | 94 +++++++++------ .../Mappers/{ => ViGEm}/DrumsVigemMapper.cs | 51 +++----- .../{ => ViGEm}/FallbackVigemMapper.cs | 11 +- .../Mappers/{ => ViGEm}/GamepadVigemMapper.cs | 11 +- .../Mappers/{ => ViGEm}/GuitarVigemMapper.cs | 11 +- .../Mappers/ViGEm/VigemMapper.cs | 87 ++++++++++++++ Program/PacketParsing/Mappers/VigemMapper.cs | 112 ------------------ .../Mappers/{ => vJoy}/DrumsVjoyMapper.cs | 13 +- .../Mappers/{ => vJoy}/FallbackVjoyMapper.cs | 13 +- .../Mappers/{ => vJoy}/GamepadVjoyMapper.cs | 13 +- .../Mappers/{ => vJoy}/GuitarVjoyMapper.cs | 13 +- .../Mappers/{ => vJoy}/VjoyMapper.cs | 51 ++------ Program/PacketParsing/XboxClient.cs | 14 ++- Program/RB4InstrumentMapper.csproj | 22 ++-- 16 files changed, 311 insertions(+), 319 deletions(-) create mode 100644 Program/PacketParsing/Mappers/DeviceMapper.cs delete mode 100644 Program/PacketParsing/Mappers/IDeviceMapper.cs rename Program/PacketParsing/Mappers/{ => ViGEm}/DrumsVigemMapper.cs (82%) rename Program/PacketParsing/Mappers/{ => ViGEm}/FallbackVigemMapper.cs (86%) rename Program/PacketParsing/Mappers/{ => ViGEm}/GamepadVigemMapper.cs (90%) rename Program/PacketParsing/Mappers/{ => ViGEm}/GuitarVigemMapper.cs (90%) create mode 100644 Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs delete mode 100644 Program/PacketParsing/Mappers/VigemMapper.cs rename Program/PacketParsing/Mappers/{ => vJoy}/DrumsVjoyMapper.cs (87%) rename Program/PacketParsing/Mappers/{ => vJoy}/FallbackVjoyMapper.cs (82%) rename Program/PacketParsing/Mappers/{ => vJoy}/GamepadVjoyMapper.cs (86%) rename Program/PacketParsing/Mappers/{ => vJoy}/GuitarVjoyMapper.cs (86%) rename Program/PacketParsing/Mappers/{ => vJoy}/VjoyMapper.cs (71%) diff --git a/Program/PacketParsing/Mappers/DeviceMapper.cs b/Program/PacketParsing/Mappers/DeviceMapper.cs new file mode 100644 index 0000000..4291cf6 --- /dev/null +++ b/Program/PacketParsing/Mappers/DeviceMapper.cs @@ -0,0 +1,92 @@ +using System; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// A mapper which maps inputs from a physical device to a virtual controller. + /// + internal abstract class DeviceMapper : IDisposable + { + protected readonly XboxClient client; + protected readonly bool mapGuideButton; + + protected bool disposed = false; + + /// + /// Initializes a new device mapper with the given parent client, + /// and option of whether or not to map the guide button. + /// + public DeviceMapper(XboxClient client, bool mapGuide) + { + this.client = client; + mapGuideButton = mapGuide; + } + + ~DeviceMapper() + { + Dispose(false); + } + + /// + /// Handles an incoming packet. + /// + public virtual XboxResult HandleMessage(byte command, ReadOnlySpan data) + { + CheckDisposed(); + return OnMessageReceived(command, data); + } + + /// + /// Handles a keystroke message. + /// + public virtual XboxResult HandleKeystroke(XboxKeystroke key) + { + CheckDisposed(); + + if (key.Keycode == XboxKeyCode.LeftWindows && mapGuideButton) + { + MapGuideButton(key.Pressed); + } + + return XboxResult.Success; + } + + protected abstract XboxResult OnMessageReceived(byte command, ReadOnlySpan data); + protected abstract void MapGuideButton(bool pressed); + + /// + /// Disposes the mapper and any resources it uses. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + DisposeManagedResources(); + } + + DisposeUnmanagedResources(); + + disposed = true; + } + + protected void CheckDisposed() + { + if (disposed) + throw new ObjectDisposedException("this"); + } + + protected virtual void DisposeManagedResources() + { + } + + protected virtual void DisposeUnmanagedResources() + { + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/IDeviceMapper.cs b/Program/PacketParsing/Mappers/IDeviceMapper.cs deleted file mode 100644 index 9d083ed..0000000 --- a/Program/PacketParsing/Mappers/IDeviceMapper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace RB4InstrumentMapper.Parsing -{ - /// - /// Common interface for device mappers. - /// - internal interface IDeviceMapper : IDisposable - { - /// - /// Whether or not the guide button should be mapped. - /// - bool MapGuideButton { get; set; } - - /// - /// Handles an incoming packet. - /// - XboxResult HandlePacket(byte command, ReadOnlySpan data); - - XboxResult HandleKeystroke(XboxKeystroke key); - } -} \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 4570a09..9c6a531 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -6,23 +6,27 @@ namespace RB4InstrumentMapper.Parsing { /// - /// Factory for device mappers. + /// Creates a device mapper for a client. /// internal static class MapperFactory { + private delegate DeviceMapper CreateMapperForMode(MappingMode mode, XboxClient client, bool mapGuide); + private delegate DeviceMapper CreateMapper(XboxClient client, bool mapGuide); + // Device interface GUIDs to check when getting the device mapper - private static readonly Dictionary> guidToMapper = new Dictionary>() + private static readonly Dictionary guidToMapper = new Dictionary() { - { XboxDeviceGuids.MadCatzGuitar, GetGuitarMapper }, - { XboxDeviceGuids.PdpGuitar, GetGuitarMapper }, + { XboxDeviceGuids.MadCatzGuitar, GetGuitarMapper }, + { XboxDeviceGuids.PdpGuitar, GetGuitarMapper }, { XboxDeviceGuids.MadCatzDrumkit, GetDrumsMapper }, - { XboxDeviceGuids.PdpDrumkit, GetDrumsMapper }, + { XboxDeviceGuids.PdpDrumkit, GetDrumsMapper }, #if DEBUG - { XboxDeviceGuids.XboxGamepad, GetGamepadMapper }, + { XboxDeviceGuids.XboxGamepad, GetGamepadMapper }, #endif }; - public static IDeviceMapper GetMapper(IEnumerable interfaceGuids, MappingMode mode) + public static DeviceMapper GetMapper(IEnumerable interfaceGuids, MappingMode mode, + XboxClient client, bool mapGuide) { // Get unique interface GUID Guid interfaceGuid = default; @@ -39,7 +43,7 @@ public static IDeviceMapper GetMapper(IEnumerable interfaceGuids, MappingM { Console.WriteLine($"- {guid2}"); } - return GetFallbackMapper(mode); + return GetFallbackMapper(mode, client, mapGuide); } interfaceGuid = guid; @@ -53,7 +57,7 @@ public static IDeviceMapper GetMapper(IEnumerable interfaceGuids, MappingM { Console.WriteLine($"- {guid2}"); } - return GetFallbackMapper(mode); + return GetFallbackMapper(mode, client, mapGuide); } // Get mapper creation delegate for interface GUID @@ -61,49 +65,35 @@ public static IDeviceMapper GetMapper(IEnumerable interfaceGuids, MappingM { Console.WriteLine($"Could not get a specific mapper for interface GUID {interfaceGuid}! Using fallback mapper instead."); Console.WriteLine($"Consider filing a GitHub issue with the GUID above so that it can be assigned the correct mapper."); - return GetFallbackMapper(mode); + return GetFallbackMapper(mode, client, mapGuide); } - return func(mode); + return func(mode, client, mapGuide); } -#if DEBUG - private static IDeviceMapper GetGamepadMapper(MappingMode mode) - => GetMapper(mode, $"Created new {mode} gamepad mapper"); -#endif - - private static IDeviceMapper GetGuitarMapper(MappingMode mode) - => GetMapper(mode, $"Created new {mode} guitar mapper"); - - private static IDeviceMapper GetDrumsMapper(MappingMode mode) - => GetMapper(mode, $"Created new {mode} drumkit mapper"); - - public static IDeviceMapper GetFallbackMapper(MappingMode mode) - => GetMapper(mode, $"Created new fallback {mode} mapper"); - - private static IDeviceMapper GetMapper(MappingMode mode, string creationMessage) - where TVigem : class, IDeviceMapper, new() - where TVjoy : class, IDeviceMapper, new() + private static DeviceMapper GetMapper(MappingMode mode, XboxClient client, bool mapGuide, + CreateMapper createVigem, CreateMapper createVjoy) { try { - IDeviceMapper mapper; + DeviceMapper mapper; switch (mode) { case MappingMode.ViGEmBus: - mapper = VigemClient.AreDevicesAvailable ? new TVigem() : null; + mapper = VigemClient.AreDevicesAvailable ? createVigem(client, mapGuide) : null; break; case MappingMode.vJoy: - mapper = VjoyClient.AreDevicesAvailable ? new TVjoy() : null; + mapper = VjoyClient.AreDevicesAvailable ? createVjoy(client, mapGuide) : null; // Check if all devices have been used if (mapper != null && !VjoyClient.AreDevicesAvailable) Console.WriteLine("vJoy device limit reached, no new devices will be handled."); break; - default: throw new NotImplementedException($"Unhandled mapping mode {mode}!"); + default: + throw new NotImplementedException($"Unhandled mapping mode {mode}!"); } if (mapper != null) - Console.WriteLine(creationMessage); + Console.WriteLine($"Created new {mapper.GetType()} mapper"); return mapper; } catch (Exception ex) @@ -112,5 +102,43 @@ private static IDeviceMapper GetMapper(MappingMode mode, string c return null; } } + +#if DEBUG + public static DeviceMapper GetGamepadMapper(MappingMode mode, XboxClient client, bool mapGuide) + => GetMapper(mode, client, mapGuide, VigemGamepadMapper, VjoyGamepadMapper); + + private static DeviceMapper VigemGamepadMapper(XboxClient client, bool mapGuide) + => new GamepadVigemMapper(client, mapGuide); + + private static DeviceMapper VjoyGamepadMapper(XboxClient client, bool mapGuide) + => new GamepadVigemMapper(client, mapGuide); +#endif + + public static DeviceMapper GetGuitarMapper(MappingMode mode, XboxClient client, bool mapGuide) + => GetMapper(mode, client, mapGuide, VigemGuitarMapper, VjoyGuitarMapper); + + private static DeviceMapper VigemGuitarMapper(XboxClient client, bool mapGuide) + => new GuitarVigemMapper(client, mapGuide); + + private static DeviceMapper VjoyGuitarMapper(XboxClient client, bool mapGuide) + => new GuitarVigemMapper(client, mapGuide); + + public static DeviceMapper GetDrumsMapper(MappingMode mode, XboxClient client, bool mapGuide) + => GetMapper(mode, client, mapGuide, VigemDrumsMapper, VjoyDrumsMapper); + + private static DeviceMapper VigemDrumsMapper(XboxClient client, bool mapGuide) + => new DrumsVigemMapper(client, mapGuide); + + private static DeviceMapper VjoyDrumsMapper(XboxClient client, bool mapGuide) + => new DrumsVigemMapper(client, mapGuide); + + public static DeviceMapper GetFallbackMapper(MappingMode mode, XboxClient client, bool mapGuide) + => GetMapper(mode, client, mapGuide, VigemFallbackMapper, VjoyFallbackMapper); + + private static DeviceMapper VigemFallbackMapper(XboxClient client, bool mapGuide) + => new FallbackVigemMapper(client, mapGuide); + + private static DeviceMapper VjoyFallbackMapper(XboxClient client, bool mapGuide) + => new FallbackVigemMapper(client, mapGuide); } } \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs b/Program/PacketParsing/Mappers/ViGEm/DrumsVigemMapper.cs similarity index 82% rename from Program/PacketParsing/Mappers/DrumsVigemMapper.cs rename to Program/PacketParsing/Mappers/ViGEm/DrumsVigemMapper.cs index 17bd4d5..a9ebf8c 100644 --- a/Program/PacketParsing/Mappers/DrumsVigemMapper.cs +++ b/Program/PacketParsing/Mappers/ViGEm/DrumsVigemMapper.cs @@ -10,14 +10,12 @@ namespace RB4InstrumentMapper.Parsing /// internal class DrumsVigemMapper : VigemMapper { - public DrumsVigemMapper() : base() + public DrumsVigemMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) { } - /// - /// Handles an incoming packet. - /// - protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) { switch (command) { @@ -38,9 +36,6 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan // The current state of the d-pad mask from the hit yellow/blue cymbals private int dpadMask; - /// - /// Parses an input report. - /// private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length != sizeof(XboxDrumInput) || !MemoryMarshal.TryRead(data, out XboxDrumInput drumReport)) @@ -152,34 +147,26 @@ internal static void HandleReport(IXbox360Controller device, in XboxDrumInput re Xbox360Axis.RightThumbY, ByteToVelocityNegative((byte)(greenPad | greenCym)) ); + } - /// - /// Scales a byte to a drums velocity value. - /// - short ByteToVelocity(byte value) - { - // Scale the value to fill the byte - value = (byte)(value * 0x11); + private static short ByteToVelocity(byte value) + { + // Scale the value to fill the byte + value = (byte)(value * 0x11); - return (short)( - // Bitwise invert to flip the value, then shift down one to exclude the sign bit - (~value.ScaleToUInt16()) >> 1 - ); - } + // Bitwise invert to flip the value, then shift down one to exclude the sign bit + int scaled = ~value.ScaleToUInt16(); + return (short)(scaled >> 1); + } - /// - /// Scales a byte to a negative drums velocity value. - /// - short ByteToVelocityNegative(byte value) - { - // Scale the value to fill the byte - value = (byte)(value * 0x11); + private static short ByteToVelocityNegative(byte value) + { + // Scale the value to fill the byte + value = (byte)(value * 0x11); - return (short)( - // Bitwise invert to flip the value, then shift down one to exclude the sign bit, then add our own - ((~value.ScaleToUInt16()) >> 1) | 0x8000 - ); - } + // Bitwise invert to flip the value, then shift down one to exclude the sign bit, then add our own + int scaled = ~value.ScaleToUInt16(); + return (short)((scaled >> 1) | 0x8000); } } } diff --git a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/ViGEm/FallbackVigemMapper.cs similarity index 86% rename from Program/PacketParsing/Mappers/FallbackVigemMapper.cs rename to Program/PacketParsing/Mappers/ViGEm/FallbackVigemMapper.cs index b9d01ba..c92186d 100644 --- a/Program/PacketParsing/Mappers/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/ViGEm/FallbackVigemMapper.cs @@ -8,14 +8,12 @@ namespace RB4InstrumentMapper.Parsing /// internal class FallbackVigemMapper : VigemMapper { - public FallbackVigemMapper() : base() + public FallbackVigemMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) { } - /// - /// Handles an incoming packet. - /// - protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) { switch (command) { @@ -37,9 +35,6 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan // The current state of the d-pad mask from the hit yellow/blue cymbals private int dpadMask; - /// - /// Parses an input report. - /// private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length == sizeof(XboxGuitarInput) && MemoryMarshal.TryRead(data, out XboxGuitarInput guitarReport)) diff --git a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs b/Program/PacketParsing/Mappers/ViGEm/GamepadVigemMapper.cs similarity index 90% rename from Program/PacketParsing/Mappers/GamepadVigemMapper.cs rename to Program/PacketParsing/Mappers/ViGEm/GamepadVigemMapper.cs index 628de00..bf2b357 100644 --- a/Program/PacketParsing/Mappers/GamepadVigemMapper.cs +++ b/Program/PacketParsing/Mappers/ViGEm/GamepadVigemMapper.cs @@ -12,14 +12,12 @@ namespace RB4InstrumentMapper.Parsing /// internal class GamepadVigemMapper : VigemMapper { - public GamepadVigemMapper() : base() + public GamepadVigemMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) { } - /// - /// Handles an incoming packet. - /// - protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) { switch (command) { @@ -31,9 +29,6 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan } } - /// - /// Parses an input report. - /// private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length < sizeof(XboxGamepadInput) || !MemoryMarshal.TryRead(data, out XboxGamepadInput gamepadReport)) diff --git a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs b/Program/PacketParsing/Mappers/ViGEm/GuitarVigemMapper.cs similarity index 90% rename from Program/PacketParsing/Mappers/GuitarVigemMapper.cs rename to Program/PacketParsing/Mappers/ViGEm/GuitarVigemMapper.cs index 241160d..68f9864 100644 --- a/Program/PacketParsing/Mappers/GuitarVigemMapper.cs +++ b/Program/PacketParsing/Mappers/ViGEm/GuitarVigemMapper.cs @@ -10,14 +10,12 @@ namespace RB4InstrumentMapper.Parsing /// internal class GuitarVigemMapper : VigemMapper { - public GuitarVigemMapper() : base() + public GuitarVigemMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) { } - /// - /// Handles an incoming packet. - /// - protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) { switch (command) { @@ -29,9 +27,6 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan } } - /// - /// Parses an input report. - /// private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length != sizeof(XboxGuitarInput) || !MemoryMarshal.TryRead(data, out XboxGuitarInput guitarReport)) diff --git a/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs b/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs new file mode 100644 index 0000000..b75d26e --- /dev/null +++ b/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs @@ -0,0 +1,87 @@ +using System; +using Nefarius.ViGEm.Client.Targets; +using Nefarius.ViGEm.Client.Targets.Xbox360; +using RB4InstrumentMapper.Vigem; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// A mapper that maps to a ViGEmBus device. + /// + internal abstract class VigemMapper : DeviceMapper + { + /// + /// The device to map to. + /// + protected IXbox360Controller device; + + /// + /// Whether or not the emulated Xbox 360 controller has connected fully. + /// + protected bool deviceConnected = false; + + public VigemMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) + { + device = VigemClient.CreateDevice(); + device.FeedbackReceived += DeviceConnected; + device.Connect(); + device.AutoSubmitReport = false; + } + + // Temporary event handler to ensure device connection + private void DeviceConnected(object sender, Xbox360FeedbackReceivedEventArgs args) + { + // Device has connected + deviceConnected = true; + + // Log the user index + Console.WriteLine($"Created new ViGEmBus device with user index {args.LedNumber}"); + + // Unregister the event handler + (sender as IXbox360Controller).FeedbackReceived -= DeviceConnected; + } + + /// + /// Handles an incoming packet. + /// + public override XboxResult HandleMessage(byte command, ReadOnlySpan data) + { + CheckDisposed(); + + if (!deviceConnected) + return XboxResult.Pending; + + return OnMessageReceived(command, data); + } + + protected override void MapGuideButton(bool pressed) + { + device.SetButtonState(Xbox360Button.Guide, pressed); + device.SubmitReport(); + } + + protected override void DisposeManagedResources() + { + if (device != null) + { + // Reset report + try + { + device.ResetReport(); + device.SubmitReport(); + } + catch { } + + // Disconnect device + try + { + device.Disconnect(); + } + catch { } + } + + device = null; + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/VigemMapper.cs b/Program/PacketParsing/Mappers/VigemMapper.cs deleted file mode 100644 index af62e73..0000000 --- a/Program/PacketParsing/Mappers/VigemMapper.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Nefarius.ViGEm.Client.Targets; -using Nefarius.ViGEm.Client.Targets.Xbox360; -using RB4InstrumentMapper.Vigem; - -namespace RB4InstrumentMapper.Parsing -{ - /// - /// A mapper that maps to a ViGEmBus device. - /// - internal abstract class VigemMapper : IDeviceMapper - { - public bool MapGuideButton { get; set; } = false; - - /// - /// The device to map to. - /// - protected IXbox360Controller device; - - /// - /// Whether or not the emulated Xbox 360 controller has connected fully. - /// - protected bool deviceConnected = false; - - public VigemMapper() - { - device = VigemClient.CreateDevice(); - device.FeedbackReceived += DeviceConnected; - device.Connect(); - device.AutoSubmitReport = false; - } - - /// - /// Performs cleanup on object finalization. - /// - ~VigemMapper() - { - Dispose(false); - } - - /// - /// Temporary event handler for logging the user index of a ViGEm device. - /// - private void DeviceConnected(object sender, Xbox360FeedbackReceivedEventArgs args) - { - // Device has connected - deviceConnected = true; - - // Log the user index - Console.WriteLine($"Created new ViGEmBus device with user index {args.LedNumber}"); - - // Unregister the event handler - (sender as IXbox360Controller).FeedbackReceived -= DeviceConnected; - } - - /// - /// Handles an incoming packet. - /// - public XboxResult HandlePacket(byte command, ReadOnlySpan data) - { - if (device == null) - throw new ObjectDisposedException(nameof(device)); - - if (!deviceConnected) - return XboxResult.Pending; - - return OnPacketReceived(command, data); - } - - protected abstract XboxResult OnPacketReceived(byte command, ReadOnlySpan data); - - public XboxResult HandleKeystroke(XboxKeystroke key) - { - if (key.Keycode == XboxKeyCode.LeftWindows && MapGuideButton) - { - device.SetButtonState(Xbox360Button.Guide, key.Pressed); - device.SubmitReport(); - } - - return XboxResult.Success; - } - - /// - /// Performs cleanup for the object. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - // Reset report - try - { - device?.ResetReport(); - device?.SubmitReport(); - } - catch - { } - - // Disconnect device - try { device?.Disconnect(); } catch {} - device = null; - } - } - } -} \ No newline at end of file diff --git a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs b/Program/PacketParsing/Mappers/vJoy/DrumsVjoyMapper.cs similarity index 87% rename from Program/PacketParsing/Mappers/DrumsVjoyMapper.cs rename to Program/PacketParsing/Mappers/vJoy/DrumsVjoyMapper.cs index b5bb01c..fad2512 100644 --- a/Program/PacketParsing/Mappers/DrumsVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/vJoy/DrumsVjoyMapper.cs @@ -10,14 +10,12 @@ namespace RB4InstrumentMapper.Parsing /// internal class DrumsVjoyMapper : VjoyMapper { - public DrumsVjoyMapper() : base() + public DrumsVjoyMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) { } - /// - /// Handles an incoming packet. - /// - protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) { switch (command) { @@ -29,10 +27,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan } } - /// - /// Parses an input report. - /// - public unsafe XboxResult ParseInput(ReadOnlySpan data) + private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length != sizeof(XboxDrumInput) || !MemoryMarshal.TryRead(data, out XboxDrumInput guitarReport)) return XboxResult.InvalidMessage; diff --git a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/vJoy/FallbackVjoyMapper.cs similarity index 82% rename from Program/PacketParsing/Mappers/FallbackVjoyMapper.cs rename to Program/PacketParsing/Mappers/vJoy/FallbackVjoyMapper.cs index 25e7682..4c7557e 100644 --- a/Program/PacketParsing/Mappers/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/vJoy/FallbackVjoyMapper.cs @@ -9,14 +9,12 @@ namespace RB4InstrumentMapper.Parsing /// internal class FallbackVjoyMapper : VjoyMapper { - public FallbackVjoyMapper() : base() + public FallbackVjoyMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) { } - /// - /// Handles an incoming packet. - /// - protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) { switch (command) { @@ -33,10 +31,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan } } - /// - /// Parses an input report. - /// - public unsafe XboxResult ParseInput(ReadOnlySpan data) + private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length == sizeof(XboxGuitarInput) && MemoryMarshal.TryRead(data, out XboxGuitarInput guitarReport)) { diff --git a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs b/Program/PacketParsing/Mappers/vJoy/GamepadVjoyMapper.cs similarity index 86% rename from Program/PacketParsing/Mappers/GamepadVjoyMapper.cs rename to Program/PacketParsing/Mappers/vJoy/GamepadVjoyMapper.cs index eeb8ec2..d49d11a 100644 --- a/Program/PacketParsing/Mappers/GamepadVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/vJoy/GamepadVjoyMapper.cs @@ -12,14 +12,12 @@ namespace RB4InstrumentMapper.Parsing /// internal class GamepadVjoyMapper : VjoyMapper { - public GamepadVjoyMapper() : base() + public GamepadVjoyMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) { } - /// - /// Handles an incoming packet. - /// - protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) { switch (command) { @@ -31,10 +29,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan } } - /// - /// Parses an input report. - /// - public unsafe XboxResult ParseInput(ReadOnlySpan data) + private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length < sizeof(XboxGamepadInput) || !MemoryMarshal.TryRead(data, out XboxGamepadInput gamepadReport)) return XboxResult.InvalidMessage; diff --git a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs b/Program/PacketParsing/Mappers/vJoy/GuitarVjoyMapper.cs similarity index 86% rename from Program/PacketParsing/Mappers/GuitarVjoyMapper.cs rename to Program/PacketParsing/Mappers/vJoy/GuitarVjoyMapper.cs index 0f3ee7b..ec1717e 100644 --- a/Program/PacketParsing/Mappers/GuitarVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/vJoy/GuitarVjoyMapper.cs @@ -10,14 +10,12 @@ namespace RB4InstrumentMapper.Parsing /// internal class GuitarVjoyMapper : VjoyMapper { - public GuitarVjoyMapper() : base() + public GuitarVjoyMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) { } - /// - /// Handles an incoming packet. - /// - protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan data) + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) { switch (command) { @@ -29,10 +27,7 @@ protected override XboxResult OnPacketReceived(byte command, ReadOnlySpan } } - /// - /// Parses an input report. - /// - public unsafe XboxResult ParseInput(ReadOnlySpan data) + private unsafe XboxResult ParseInput(ReadOnlySpan data) { if (data.Length != sizeof(XboxGuitarInput) || !MemoryMarshal.TryRead(data, out XboxGuitarInput guitarReport)) return XboxResult.InvalidMessage; diff --git a/Program/PacketParsing/Mappers/VjoyMapper.cs b/Program/PacketParsing/Mappers/vJoy/VjoyMapper.cs similarity index 71% rename from Program/PacketParsing/Mappers/VjoyMapper.cs rename to Program/PacketParsing/Mappers/vJoy/VjoyMapper.cs index 20b56d5..834ac23 100644 --- a/Program/PacketParsing/Mappers/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/vJoy/VjoyMapper.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; using RB4InstrumentMapper.Vjoy; using vJoyInterfaceWrap; @@ -8,14 +7,13 @@ namespace RB4InstrumentMapper.Parsing /// /// A mapper that maps to a vJoy device. /// - internal abstract class VjoyMapper : IDeviceMapper + internal abstract class VjoyMapper : DeviceMapper { - public bool MapGuideButton { get; set; } = false; - protected vJoy.JoystickState state = new vJoy.JoystickState(); protected uint deviceId = 0; - public VjoyMapper() + public VjoyMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) { deviceId = VjoyClient.GetNextAvailableID(); if (deviceId == 0) @@ -32,36 +30,10 @@ public VjoyMapper() Console.WriteLine($"Acquired vJoy device with ID of {deviceId}"); } - /// - /// Performs cleanup on object finalization. - /// - ~VjoyMapper() - { - Dispose(false); - } - - /// - /// Handles an incoming packet. - /// - public XboxResult HandlePacket(byte command, ReadOnlySpan data) + protected override void MapGuideButton(bool pressed) { - if (deviceId == 0) - throw new ObjectDisposedException("this"); - - return OnPacketReceived(command, data); - } - - protected abstract XboxResult OnPacketReceived(byte command, ReadOnlySpan data); - - public XboxResult HandleKeystroke(XboxKeystroke key) - { - if (key.Keycode == XboxKeyCode.LeftWindows && MapGuideButton) - { - state.SetButton(VjoyButton.Fourteen, key.Pressed); - VjoyClient.UpdateDevice(deviceId, ref state); - } - - return XboxResult.Success; + state.SetButton(VjoyButton.Fourteen, pressed); + VjoyClient.UpdateDevice(deviceId, ref state); } // vJoy axes range from 0x0000 to 0x8000, but are exposed as full ints for some reason @@ -135,16 +107,7 @@ protected static void ParseDpad(ref vJoy.JoystickState state, XboxGamepadButton state.bHats = (uint)direction; } - /// - /// Performs cleanup for the vJoy mapper. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) + protected override void DisposeUnmanagedResources() { // Reset report state.Reset(); diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 6d6a4df..040a0a3 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -25,8 +25,10 @@ internal class XboxClient : IDisposable /// public byte ClientId { get; } + private DeviceMapper deviceMapper; + private readonly bool mapGuideButton; + private bool arrivalReceived = false; - private IDeviceMapper deviceMapper; private readonly Dictionary previousReceiveSequence = new Dictionary(); private readonly Dictionary previousSendSequence = new Dictionary(); @@ -35,10 +37,12 @@ internal class XboxClient : IDisposable { XboxDescriptor.CommandId, new XboxChunkBuffer() }, }; - public XboxClient(XboxDevice parent, byte clientId) + public XboxClient(XboxDevice parent, byte clientId, bool mapGuide = false) { Parent = parent; ClientId = clientId; + + mapGuideButton = mapGuide; } ~XboxClient() @@ -110,7 +114,7 @@ internal unsafe XboxResult HandleMessage(XboxCommandHeader header, ReadOnlySpan< // Non-system commands are handled by the mapper if (deviceMapper == null) { - deviceMapper = MapperFactory.GetFallbackMapper(XboxDevice.MapperMode); + deviceMapper = MapperFactory.GetFallbackMapper(XboxDevice.MapperMode, this, mapGuideButton); if (deviceMapper == null) { // No more devices available, do nothing @@ -121,7 +125,7 @@ internal unsafe XboxResult HandleMessage(XboxCommandHeader header, ReadOnlySpan< Console.WriteLine("Reconnect it (or hit Start before connecting it) to ensure correct behavior."); } - return deviceMapper.HandlePacket(header.CommandId, commandData); + return deviceMapper.HandleMessage(header.CommandId, commandData); } private XboxResult HandleSystemCommand(byte commandId, ReadOnlySpan commandData) @@ -188,7 +192,7 @@ private XboxResult HandleDescriptor(ReadOnlySpan data) return XboxResult.InvalidMessage; Descriptor = descriptor; - deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, XboxDevice.MapperMode); + deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, XboxDevice.MapperMode, this, mapGuideButton); // Send final set of initialization messages Debug.Assert(Descriptor.OutputCommands.Contains(XboxConfiguration.CommandId)); diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index d226c6c..3a875df 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -117,18 +117,18 @@ - - - - - - - - - + + + + + + + + + + + - - From 7c4b7757914fd516b3fa71fac23112206ea48e61 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 23 Aug 2023 05:50:36 -0600 Subject: [PATCH 214/437] Implement support for the wireless legacy adapter --- .../PacketParsing/Mappers/MapperFactory.cs | 18 +++ .../Mappers/WirelessLegacyMapper.cs | 148 ++++++++++++++++++ .../XboxWirelessLegacyDeviceConnect.cs | 63 ++++++++ .../XboxWirelessLegacyDeviceDisconnect.cs | 15 ++ .../WirelessLegacy/XboxWirelessLegacyInput.cs | 18 +++ .../XboxWirelessLegacyRequestDevices.cs | 20 +++ Program/PacketParsing/XboxDeviceGuids.cs | 2 + Program/RB4InstrumentMapper.csproj | 5 + 8 files changed, 289 insertions(+) create mode 100644 Program/PacketParsing/Mappers/WirelessLegacyMapper.cs create mode 100644 Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyDeviceConnect.cs create mode 100644 Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyDeviceDisconnect.cs create mode 100644 Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyInput.cs create mode 100644 Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyRequestDevices.cs diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 9c6a531..5437b39 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -20,6 +20,9 @@ internal static class MapperFactory { XboxDeviceGuids.PdpGuitar, GetGuitarMapper }, { XboxDeviceGuids.MadCatzDrumkit, GetDrumsMapper }, { XboxDeviceGuids.PdpDrumkit, GetDrumsMapper }, + + { XboxDeviceGuids.MadCatzLegacyWireless, GetWirelessLegacyMapper }, + #if DEBUG { XboxDeviceGuids.XboxGamepad, GetGamepadMapper }, #endif @@ -132,6 +135,21 @@ private static DeviceMapper VigemDrumsMapper(XboxClient client, bool mapGuide) private static DeviceMapper VjoyDrumsMapper(XboxClient client, bool mapGuide) => new DrumsVigemMapper(client, mapGuide); + public static DeviceMapper GetWirelessLegacyMapper(MappingMode mode, XboxClient client, bool mapGuide) + { + try + { + var mapper = new WirelessLegacyMapper(mode, client, mapGuide); + Console.WriteLine($"Created new {nameof(WirelessLegacyMapper)} mapper"); + return mapper; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to create mapper for device: {ex.GetFirstLine()}"); + return null; + } + } + public static DeviceMapper GetFallbackMapper(MappingMode mode, XboxClient client, bool mapGuide) => GetMapper(mode, client, mapGuide, VigemFallbackMapper, VjoyFallbackMapper); diff --git a/Program/PacketParsing/Mappers/WirelessLegacyMapper.cs b/Program/PacketParsing/Mappers/WirelessLegacyMapper.cs new file mode 100644 index 0000000..18f5beb --- /dev/null +++ b/Program/PacketParsing/Mappers/WirelessLegacyMapper.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Maps devices connected to a RB4 wireless legacy adapter to virtual controllers. + /// + internal class WirelessLegacyMapper : DeviceMapper + { + // Mappers are not guaranteed to be created for each device, unknown subtypes will be ignored and have none + private readonly Dictionary mappers = new Dictionary(); + + private readonly MappingMode mappingMode; + + public WirelessLegacyMapper(MappingMode mode, XboxClient client, bool mapGuide) + : base(client, mapGuide) + { + mappingMode = mode; + + client.SendMessage(XboxWirelessLegacyRequestDevices.RequestDevices); + } + + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) + { + switch (command) + { + case XboxWirelessLegacyInputHeader.CommandId: + return HandleInput(data); + + case XboxWirelessLegacyDeviceConnect.CommandId: + return HandleConnection(data); + + case XboxWirelessLegacyDeviceDisconnect.CommandId: + return HandleDisconnection(data); + } + + return XboxResult.Success; + } + + private unsafe XboxResult HandleInput(ReadOnlySpan data) + { + if (data.Length < sizeof(XboxWirelessLegacyInputHeader) || + !MemoryMarshal.TryRead(data, out XboxWirelessLegacyInputHeader header)) + return XboxResult.InvalidMessage; + + // Find the mapper for the given user index + byte userIndex = header.UserIndex; + if (!mappers.TryGetValue(userIndex, out var mapper)) + { + Debug.WriteLine($"Missing mapper for user index {userIndex}!"); + return XboxResult.InvalidMessage; + } + + data = data.Slice(sizeof(XboxWirelessLegacyInputHeader)); + return mapper?.HandleMessage(XboxWirelessLegacyInputHeader.CommandId, data) ?? XboxResult.Success; + } + + private XboxResult HandleConnection(ReadOnlySpan data) + { + if (!XboxWirelessLegacyDeviceConnect.TryParse(data, out var connect)) + return XboxResult.InvalidMessage; + + // Find the mapper for the given user index + byte userIndex = connect.UserIndex; + if (mappers.TryGetValue(userIndex, out var mapper)) + { + Debug.WriteLine($"Mapper already exists for user index {userIndex}! Overwriting."); + mapper?.Dispose(); + } + + mappers[userIndex] = GetMapperForDevice(connect); + return XboxResult.Success; + } + + private unsafe XboxResult HandleDisconnection(ReadOnlySpan data) + { + if (data.Length < sizeof(XboxWirelessLegacyDeviceDisconnect) || + !MemoryMarshal.TryRead(data, out XboxWirelessLegacyDeviceDisconnect disconnect)) + return XboxResult.InvalidMessage; + + // Find the mapper for the given user index + byte userIndex = disconnect.UserIndex; + if (!mappers.TryGetValue(userIndex, out var mapper)) + { + Debug.WriteLine($"Missing mapper for user index {userIndex}!"); + return XboxResult.InvalidMessage; + } + + mapper?.Dispose(); + mappers.Remove(userIndex); + return XboxResult.Success; + } + + public override XboxResult HandleKeystroke(XboxKeystroke key) + { + foreach (var mapper in mappers.Values) + { + var result = mapper.HandleKeystroke(key); + if (result != XboxResult.Success) + return result; + } + + return XboxResult.Success; + } + + // Handled by the override above + protected override void MapGuideButton(bool pressed) { } + + private DeviceMapper GetMapperForDevice(XboxWirelessLegacyDeviceConnect connect) + { + var subtype = connect.DeviceSubtype; + switch (subtype) + { +#if DEBUG + case XInputDeviceSubtype.Gamepad: + return MapperFactory.GetGamepadMapper(mappingMode, client, mapGuideButton); +#endif + + case XInputDeviceSubtype.Guitar: + case XInputDeviceSubtype.GuitarAlternate: + case XInputDeviceSubtype.GuitarBass: + return MapperFactory.GetGuitarMapper(mappingMode, client, mapGuideButton); + + case XInputDeviceSubtype.Drums: + return MapperFactory.GetGuitarMapper(mappingMode, client, mapGuideButton); + + default: + Debug.WriteLine($"Unsupported XInput subtype {subtype} on user index {connect.UserIndex}!"); + Console.WriteLine($"The Xbox 360 controller on user index {connect.UserIndex + 1} of the legacy adapter has an unsupported subtype ({subtype})!"); + Console.WriteLine("If you think it should be supported, restart capture with packet logging to a file enabled, and create a GitHub issue with the log file attached."); + return null; + } + } + + protected override void DisposeManagedResources() + { + foreach (var mapper in mappers.Values) + { + mapper.Dispose(); + } + + mappers.Clear(); + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyDeviceConnect.cs b/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyDeviceConnect.cs new file mode 100644 index 0000000..f306572 --- /dev/null +++ b/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyDeviceConnect.cs @@ -0,0 +1,63 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Possible subtypes for XInput devices. + /// + internal enum XInputDeviceSubtype : byte + { + Unknown = 0, + Gamepad = 1, + Wheel = 2, + ArcadeStick = 3, + FlightStick = 4, + DancePad = 5, + Guitar = 6, + GuitarAlternate = 7, + Drums = 8, + GuitarBass = 11, + Keyboard = 15, + ArcadePad = 19, + Turntable = 23, + } + + /// + /// Reports info about a device that has just connected to a wireless legacy adapter. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal unsafe struct XboxWirelessLegacyDeviceConnect + { + public const byte CommandId = 0x22; + + public const int MinimumLength = 6; + + public byte UserIndex; + + public byte DeviceType; + private ushort vendorId; // big-endian + private byte unknown; + private byte deviceSubtype; + private fixed char name[124]; + + public ushort VendorId => (ushort)((vendorId & 0xFF) << 8 | (vendorId & 0xFF00) >> 8); + public XInputDeviceSubtype DeviceSubtype => (XInputDeviceSubtype)(deviceSubtype & 0x7F); + + public static bool TryParse(ReadOnlySpan buffer, out XboxWirelessLegacyDeviceConnect connectInfo) + { + connectInfo = new XboxWirelessLegacyDeviceConnect(); + + if (buffer.Length < MinimumLength) + return false; + + // Create a byte buffer reference and copy the message buffer into it + var writeBuffer = new Span(Unsafe.AsPointer(ref connectInfo), sizeof(XboxWirelessLegacyDeviceConnect)); + if (buffer.Length > sizeof(XboxWirelessLegacyDeviceConnect)) + buffer = buffer.Slice(0, sizeof(XboxWirelessLegacyDeviceConnect)); + + return buffer.TryCopyTo(writeBuffer); + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyDeviceDisconnect.cs b/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyDeviceDisconnect.cs new file mode 100644 index 0000000..5ad901c --- /dev/null +++ b/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyDeviceDisconnect.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Notifies when a device has disconnected from a wireless legacy adapter. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct XboxWirelessLegacyDeviceDisconnect + { + public const byte CommandId = 0x23; + + public byte UserIndex; + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyInput.cs b/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyInput.cs new file mode 100644 index 0000000..8c1ccf5 --- /dev/null +++ b/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyInput.cs @@ -0,0 +1,18 @@ +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// The input report header used by the wireless legacy adapter. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct XboxWirelessLegacyInputHeader + { + public const byte CommandId = 0x20; + + public ushort Buttons; + public byte UserIndex; + + private byte unknown; + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyRequestDevices.cs b/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyRequestDevices.cs new file mode 100644 index 0000000..f27bccc --- /dev/null +++ b/Program/PacketParsing/Packets/WirelessLegacy/XboxWirelessLegacyRequestDevices.cs @@ -0,0 +1,20 @@ +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Requests info for devices currently connected to a wireless legacy adapter. + /// + internal static class XboxWirelessLegacyRequestDevices + { + public const byte CommandId = 0x24; + + public static readonly XboxMessage RequestDevices = new XboxMessage() + { + Header = new XboxCommandHeader() + { + CommandId = CommandId, + Flags = XboxCommandFlags.None, + }, + // No data + }; + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxDeviceGuids.cs b/Program/PacketParsing/XboxDeviceGuids.cs index 1c50d11..a63f620 100644 --- a/Program/PacketParsing/XboxDeviceGuids.cs +++ b/Program/PacketParsing/XboxDeviceGuids.cs @@ -13,6 +13,8 @@ internal static class XboxDeviceGuids public static readonly Guid MadCatzGuitar = Guid.Parse("0D2AE438-7F7D-4933-8693-30FC55018E77"); public static readonly Guid MadCatzDrumkit = Guid.Parse("06182893-CCE0-4B85-9271-0A10DBAB7E07"); + public static readonly Guid MadCatzLegacyWireless = Guid.Parse("AF259D0F-76B0-4CDB-BFD1-CEA8C0A8F5EE"); + public static readonly Guid PdpGuitar = Guid.Parse("1A266AF6-3A46-45E3-B9B6-0F2C0B2C1EBE"); public static readonly Guid PdpDrumkit = Guid.Parse("A503F9B0-955E-47C4-A2ED-B1336FA7703E"); } diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 3a875df..43c0399 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -129,6 +129,7 @@ + @@ -140,6 +141,10 @@ + + + + From eeeafeb266bed1c9dd5c934a6a39b65b7b15569d Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 23 Aug 2023 05:55:13 -0600 Subject: [PATCH 215/437] Fix a couple incorrect accessibility modifiers --- Program/PacketParsing/Packets/System/XboxKeystroke.cs | 2 +- Program/PacketParsing/XboxDevice.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Program/PacketParsing/Packets/System/XboxKeystroke.cs b/Program/PacketParsing/Packets/System/XboxKeystroke.cs index 38bfb0c..1a49d5e 100644 --- a/Program/PacketParsing/Packets/System/XboxKeystroke.cs +++ b/Program/PacketParsing/Packets/System/XboxKeystroke.cs @@ -16,7 +16,7 @@ internal enum XboxKeystrokeFlags : byte /// /// These mirror those in the Win32 API; for brevity, only the ones used are defined here. /// - public enum XboxKeyCode : byte + internal enum XboxKeyCode : byte { LeftWindows = 0x5B, // Used for the guide button } diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 8f9ad7b..09ce8c3 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -5,13 +5,13 @@ namespace RB4InstrumentMapper.Parsing { - public enum MappingMode + internal enum MappingMode { ViGEmBus = 1, vJoy = 2 } - public enum XboxResult + internal enum XboxResult { Success, Pending, @@ -22,7 +22,7 @@ public enum XboxResult /// /// An Xbox device. /// - public class XboxDevice : IDisposable + internal class XboxDevice : IDisposable { public static MappingMode MapperMode; From c2a10933f8139e2e9dfeb01066af1d05c774ac0f Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 23 Aug 2023 06:51:46 -0600 Subject: [PATCH 216/437] Refactor backend settings --- Program/MainWindow/MainWindow.xaml.cs | 8 ++--- .../PacketParsing/Backends/BackendSettings.cs | 29 +++++++++++++++++++ Program/PacketParsing/Backends/PcapBackend.cs | 11 ++----- .../Backends/XboxWinUsbDevice.cs | 6 ++-- Program/PacketParsing/XboxClient.cs | 9 ++---- Program/PacketParsing/XboxDevice.cs | 19 +++++------- Program/RB4InstrumentMapper.csproj | 1 + 7 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 Program/PacketParsing/Backends/BackendSettings.cs diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 110ce2e..f56cefe 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -376,7 +376,7 @@ private bool StartPcapCapture() } // Start capture - PcapBackend.LogPackets = packetDebug; + BackendSettings.LogPackets = packetDebug; PcapBackend.OnCaptureStop += OnCaptureStopped; PcapBackend.StartCapture(pcapSelectedDevice); Console.WriteLine($"Listening on {pcapSelectedDevice.GetDisplayName()}..."); @@ -534,7 +534,7 @@ private void controllerDeviceTypeCombo_SelectionChanged(object sender, Selection case 0: if (VjoyClient.GetAvailableDeviceCount() > 0) { - XboxDevice.MapperMode = MappingMode.vJoy; + BackendSettings.MapperMode = MappingMode.vJoy; Settings.Default.controllerDeviceType = (int)ControllerType.vJoy; } else @@ -548,12 +548,12 @@ private void controllerDeviceTypeCombo_SelectionChanged(object sender, Selection // ViGEmBus case 1: - XboxDevice.MapperMode = MappingMode.ViGEmBus; + BackendSettings.MapperMode = MappingMode.ViGEmBus; Settings.Default.controllerDeviceType = (int)ControllerType.VigemBus; break; default: - XboxDevice.MapperMode = 0; + BackendSettings.MapperMode = 0; Settings.Default.controllerDeviceType = (int)ControllerType.None; break; } diff --git a/Program/PacketParsing/Backends/BackendSettings.cs b/Program/PacketParsing/Backends/BackendSettings.cs new file mode 100644 index 0000000..62c457f --- /dev/null +++ b/Program/PacketParsing/Backends/BackendSettings.cs @@ -0,0 +1,29 @@ +namespace RB4InstrumentMapper.Parsing +{ + public enum MappingMode + { + ViGEmBus = 1, + vJoy = 2 + } + + /// + /// Backend for handling controllers via Pcap. + /// + public static class BackendSettings + { + /// + /// The controller emulator to use. + /// + public static MappingMode MapperMode { get; set; } + + /// + /// Whether or not packets should be logged to the console. + /// + public static bool LogPackets { get; set; } = false; + + /// + /// Whether or not the guide button is mapped. + /// + public static bool MapGuideButton { get; set; } = false; + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index b3462d4..6b69979 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -44,11 +44,6 @@ public ulong DeviceId /// public static class PcapBackend { - /// - /// Whether or not packets should be logged to the console. - /// - public static bool LogPackets { get; set; } = false; - /// /// Event fired when packet capture stops automatically. /// @@ -125,7 +120,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) } // Debugging (if enabled) - if (LogPackets) + if (BackendSettings.LogPackets) { string packetLogString = $"{packet.Header.Timeval.Date:yyyy-MM-dd hh:mm:ss.fff} [{packet.Data.Length}] " + $"{ParsingUtils.ToString(headerData)} | {ParsingUtils.ToString(packetData)}"; @@ -137,7 +132,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) ulong deviceId = header.DeviceId; if (!devices.TryGetValue(deviceId, out var device)) { - device = new XboxDevice(); + device = new XboxDevice(BackendSettings.MapperMode, BackendSettings.MapGuideButton); devices.Add(deviceId, device); Console.WriteLine($"Device with ID {deviceId:X12} was connected"); } @@ -153,7 +148,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) Console.WriteLine($"Device with ID {deviceId:X12} was disconnected"); break; case XboxResult.InvalidMessage: - if (LogPackets) + if (BackendSettings.LogPackets) { string invalidMessage = $"Invalid packet received!"; Console.WriteLine(invalidMessage); diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index c130de4..959076d 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -20,8 +20,8 @@ internal class XboxWinUsbDevice : XboxDevice private Thread readThread; private volatile bool readPackets = false; - private XboxWinUsbDevice(USBDevice usb, USBInterface @interface) - : base(@interface.OutPipe.MaximumPacketSize) + private XboxWinUsbDevice(USBDevice usb, USBInterface @interface, MappingMode mode, bool mapGuide) + : base(mode, mapGuide, @interface.OutPipe.MaximumPacketSize) { usbDevice = usb; mainInterface = @interface; @@ -50,7 +50,7 @@ public static XboxWinUsbDevice TryCreate(string devicePath) return null; } - return new XboxWinUsbDevice(usbDevice, mainInterface); + return new XboxWinUsbDevice(usbDevice, mainInterface, BackendSettings.MapperMode, BackendSettings.MapGuideButton); } public void StartReading() diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 040a0a3..0268768 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -26,7 +26,6 @@ internal class XboxClient : IDisposable public byte ClientId { get; } private DeviceMapper deviceMapper; - private readonly bool mapGuideButton; private bool arrivalReceived = false; @@ -37,12 +36,10 @@ internal class XboxClient : IDisposable { XboxDescriptor.CommandId, new XboxChunkBuffer() }, }; - public XboxClient(XboxDevice parent, byte clientId, bool mapGuide = false) + public XboxClient(XboxDevice parent, byte clientId) { Parent = parent; ClientId = clientId; - - mapGuideButton = mapGuide; } ~XboxClient() @@ -114,7 +111,7 @@ internal unsafe XboxResult HandleMessage(XboxCommandHeader header, ReadOnlySpan< // Non-system commands are handled by the mapper if (deviceMapper == null) { - deviceMapper = MapperFactory.GetFallbackMapper(XboxDevice.MapperMode, this, mapGuideButton); + deviceMapper = MapperFactory.GetFallbackMapper(Parent.MappingMode, this, Parent.MapGuideButton); if (deviceMapper == null) { // No more devices available, do nothing @@ -192,7 +189,7 @@ private XboxResult HandleDescriptor(ReadOnlySpan data) return XboxResult.InvalidMessage; Descriptor = descriptor; - deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, XboxDevice.MapperMode, this, mapGuideButton); + deviceMapper = MapperFactory.GetMapper(descriptor.InterfaceGuids, Parent.MappingMode, this, Parent.MapGuideButton); // Send final set of initialization messages Debug.Assert(Descriptor.OutputCommands.Contains(XboxConfiguration.CommandId)); diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 09ce8c3..e200166 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -5,12 +5,6 @@ namespace RB4InstrumentMapper.Parsing { - internal enum MappingMode - { - ViGEmBus = 1, - vJoy = 2 - } - internal enum XboxResult { Success, @@ -24,8 +18,6 @@ internal enum XboxResult /// internal class XboxDevice : IDisposable { - public static MappingMode MapperMode; - /// /// The clients currently on the device. /// @@ -33,13 +25,18 @@ internal class XboxDevice : IDisposable private readonly int maxPacketSize; - public XboxDevice() : this(maxPacketSize: 0) + public MappingMode MappingMode { get; } + public bool MapGuideButton { get; } + + public XboxDevice(MappingMode mode, bool mapGuide) : this(mode, mapGuide, 0) { } - protected XboxDevice(int maxPacketSize) + protected XboxDevice(MappingMode mode, bool mapGuide, int maxPacket) { - this.maxPacketSize = maxPacketSize; + MappingMode = mode; + MapGuideButton = mapGuide; + maxPacketSize = maxPacket; } ~XboxDevice() diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index 43c0399..e1a93ed 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -114,6 +114,7 @@ + From ac4ea0cf831ceb51ac72996f1a5b487255fd833e Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 23 Aug 2023 09:40:10 -0600 Subject: [PATCH 217/437] Fix double-reset/potential reset loop when stopping capture --- Program/PacketParsing/Backends/XboxWinUsbDevice.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index 959076d..1a86532 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -168,7 +168,9 @@ protected override void ReleaseManagedResources() { base.ReleaseManagedResources(); - StopReading(); + if (readThread != null) + StopReading(); + usbDevice?.Dispose(); usbDevice = null; mainInterface = null; From 03a9477b6b3137de7a9c4ea7c093789f2fbbc842 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 23 Aug 2023 09:56:35 -0600 Subject: [PATCH 218/437] Refactor a bunch of logging stuff --- Program/Logging.cs | 113 +++++++----------- Program/MainWindow/MainWindow.xaml.cs | 5 +- .../PacketParsing/Backends/BackendSettings.cs | 5 + Program/PacketParsing/Backends/PcapBackend.cs | 42 +++---- .../Backends/XboxWinUsbDevice.cs | 10 +- .../PacketParsing/Mappers/MapperFactory.cs | 26 ++-- .../Mappers/ViGEm/VigemMapper.cs | 2 +- .../Mappers/WirelessLegacyMapper.cs | 11 +- .../PacketParsing/Mappers/vJoy/VjoyMapper.cs | 2 +- Program/PacketParsing/PacketLogging.cs | 63 ++++++++++ .../Packets/System/XboxDescriptor.cs | 2 +- .../Packets/XboxCommandHeader.cs | 4 +- Program/PacketParsing/Packets/XboxPacket.cs | 28 +++++ Program/PacketParsing/XboxClient.cs | 17 +-- Program/PacketParsing/XboxDevice.cs | 13 +- Program/RB4InstrumentMapper.csproj | 2 + 16 files changed, 207 insertions(+), 138 deletions(-) create mode 100644 Program/PacketParsing/PacketLogging.cs create mode 100644 Program/PacketParsing/Packets/XboxPacket.cs diff --git a/Program/Logging.cs b/Program/Logging.cs index d040e6a..989b617 100644 --- a/Program/Logging.cs +++ b/Program/Logging.cs @@ -18,10 +18,7 @@ public static class Logging /// /// Gets whether or not the main log exists. /// - public static bool MainLogExists - { - get => mainLog != null; - } + public static bool MainLogExists => mainLog != null; private static bool allowMainLogCreation = true; @@ -33,10 +30,7 @@ public static bool MainLogExists /// /// Gets whether or not a packet log exists. /// - public static bool PacketLogExists - { - get => packetLog != null; - } + public static bool PacketLogExists => packetLog != null; /// /// The path to the folder to write logs to. @@ -45,8 +39,9 @@ public static bool PacketLogExists /// Currently %USERPROFILE%\Documents\RB4InstrumentMapper\Logs /// public static readonly string LogFolderPath = Path.Combine( - System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - "RB4InstrumentMapper\\Logs" + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + "RB4InstrumentMapper", + "Logs" ); /// @@ -56,8 +51,9 @@ public static bool PacketLogExists /// Currently %USERPROFILE%\Documents\RB4InstrumentMapper\PacketLogs /// public static readonly string PacketLogFolderPath = Path.Combine( - System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - "RB4InstrumentMapper\\PacketLogs" + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + "RB4InstrumentMapper", + "PacketLogs" ); /// @@ -95,25 +91,19 @@ private static StreamWriter CreateFileStream(string folderPath) /// public static bool CreateMainLog() { - if (allowMainLogCreation && mainLog == null) - { - mainLog = CreateFileStream(LogFolderPath); - if (mainLog != null) - { - Console.WriteLine("Created main log file."); - return true; - } - else - { - // Log could not be created, don't allow creating it again to prevent console spam - allowMainLogCreation = false; - return false; - } - } - else + if (!allowMainLogCreation || mainLog != null) + return true; + + mainLog = CreateFileStream(LogFolderPath); + if (mainLog == null) { + // Log could not be created, don't allow creating it again to prevent console spam + allowMainLogCreation = false; return false; } + + Console.WriteLine("Created main log file."); + return true; } /// @@ -121,23 +111,15 @@ public static bool CreateMainLog() /// public static bool CreatePacketLog() { + if (packetLog != null) + return true; + + packetLog = CreateFileStream(PacketLogFolderPath); if (packetLog == null) - { - packetLog = CreateFileStream(PacketLogFolderPath); - if (packetLog != null) - { - Console.WriteLine("Created packet log file."); - return true; - } - else - { - return false; - } - } - else - { return false; - } + + Console.WriteLine("Created packet log file."); + return true; } /// @@ -148,32 +130,25 @@ public static void Main_WriteLine(string text) // Create log file if it hasn't been made yet CreateMainLog(); - mainLog?.WriteLine(text); + mainLog?.WriteLine(GetMessageHeader(text)); } /// - /// Writes an exception, and any additonal info, to the log. + /// Writes an exception, and any context, to the log. /// - public static void Main_WriteException(Exception ex, string addtlInfo = null) + public static void Main_WriteException(Exception ex, string context = null) { // Create log file if it hasn't been made yet CreateMainLog(); - mainLog?.WriteException(ex, addtlInfo); + mainLog?.WriteException(ex, context); } public static void Packet_WriteLine(string text) { // Don't create log file if it hasn't been made yet // Packet log should be created manually - packetLog?.WriteLine(text); - } - - public static void Packet_Write(string text) - { - // Don't create log file if it hasn't been made yet - // Packet log should be created manually - packetLog?.Write(text); + packetLog?.WriteLine(GetMessageHeader(text)); } /// @@ -213,7 +188,7 @@ public static void CloseAll() /// public static string GetFirstLine(this Exception ex) { - return ex?.ToString().Split('\n')[0]; + return ex.ToString().Split('\n')[0]; } // Extension method for logging exceptions to streams in a customized manner @@ -226,24 +201,24 @@ public static string GetFirstLine(this Exception ex) /// /// The exception to log. /// - /// - /// Additional info to log after the stack trace. + /// + /// Additional context for the exception. /// - public static void WriteException(this StreamWriter stream, Exception ex, string addtlInfo = null) + public static void WriteException(this StreamWriter stream, Exception ex, string context = null) { - // Current date and time, formatted in Year/Month/Date Hour:Minute:Second with the invariant culture - string timestamp = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture); - - // Log - stream.WriteLine($"[{timestamp}] EXCEPTION:"); + stream.WriteLine(GetMessageHeader("EXCEPTION")); stream.WriteLine("------------------------------"); + // Prevent writing an empty line if context is not provided + if (context != null) + stream.WriteLine(context); stream.WriteLine(ex.ToString()); - // Prevent writing an empty line if additional info is not provided - if (addtlInfo != null) - { - stream.WriteLine(addtlInfo); - } stream.WriteLine("------------------------------"); } + + private static string GetMessageHeader(string message) + { + string timestamp = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss", CultureInfo.InvariantCulture); + return $"[{timestamp}] {message}"; + } } } diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index f56cefe..712d631 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -156,7 +156,7 @@ private void Window_Loaded(object sender, RoutedEventArgs e) Application.Current.Shutdown(); // Log exception - Logging.Main_WriteException(ex); + Logging.Main_WriteException(ex, "Failed to load Pcap interface!"); return; } @@ -290,6 +290,7 @@ private void StartCapture() if (!Logging.CreatePacketLog()) { packetDebugLog = false; + // Remaining context for this message is inside of the log creation Console.WriteLine("Disabled packet logging for this capture session."); } } @@ -736,7 +737,7 @@ public static void OnUnhandledException(object sender, UnhandledExceptionEventAr Logging.Main_WriteLine("-------------------"); Logging.Main_WriteLine("UNHANDLED EXCEPTION"); Logging.Main_WriteLine("-------------------"); - Logging.Main_WriteException(unhandledException); + Logging.Main_WriteException(unhandledException, "Unhandled exception!"); // Complete the message buffer message.AppendLine("A log of the error has been created, do you want to open it?"); diff --git a/Program/PacketParsing/Backends/BackendSettings.cs b/Program/PacketParsing/Backends/BackendSettings.cs index 62c457f..add3105 100644 --- a/Program/PacketParsing/Backends/BackendSettings.cs +++ b/Program/PacketParsing/Backends/BackendSettings.cs @@ -21,6 +21,11 @@ public static class BackendSettings /// public static bool LogPackets { get; set; } = false; + /// + /// Whether or not verbose errors should be logged to the console. + /// + public static bool PrintVerboseErrors { get; set; } = false; + /// /// Whether or not the guide button is mapped. /// diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 6b69979..3bc9b36 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -119,42 +119,31 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) return; } - // Debugging (if enabled) - if (BackendSettings.LogPackets) - { - string packetLogString = $"{packet.Header.Timeval.Date:yyyy-MM-dd hh:mm:ss.fff} [{packet.Data.Length}] " + - $"{ParsingUtils.ToString(headerData)} | {ParsingUtils.ToString(packetData)}"; - Console.WriteLine(packetLogString); - Logging.Packet_WriteLine(packetLogString); - } - // Check if device ID has been encountered yet ulong deviceId = header.DeviceId; if (!devices.TryGetValue(deviceId, out var device)) { device = new XboxDevice(BackendSettings.MapperMode, BackendSettings.MapGuideButton); devices.Add(deviceId, device); - Console.WriteLine($"Device with ID {deviceId:X12} was connected"); + PacketLogging.PrintMessage($"Device with ID {deviceId:X12} was connected"); } + var xboxPacket = new XboxPacket() + { + DirectionIn = true, + Time = packet.Header.Timeval.Date, + Header = headerData, + Data = packetData, + }; + try { - var result = device.HandlePacket(packetData); - switch (result) + var result = device.HandlePacket(xboxPacket); + if (result == XboxResult.Disconnected) { - case XboxResult.Disconnected: - device.Dispose(); - devices.Remove(deviceId); - Console.WriteLine($"Device with ID {deviceId:X12} was disconnected"); - break; - case XboxResult.InvalidMessage: - if (BackendSettings.LogPackets) - { - string invalidMessage = $"Invalid packet received!"; - Console.WriteLine(invalidMessage); - Logging.Packet_WriteLine(invalidMessage); - } - break; + device.Dispose(); + devices.Remove(deviceId); + PacketLogging.PrintMessage($"Device with ID {deviceId:X12} was disconnected"); } } catch (ThreadAbortException) @@ -164,8 +153,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) } catch (Exception ex) { - Console.WriteLine($"Error while handling packet: {ex.GetFirstLine()}"); - Logging.Main_WriteException(ex, "Context: Unhandled error during packet handling"); + PacketLogging.PrintException("Error while handling packet!", ex); // Stop capture StopCapture(); diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index 1a86532..ad962fb 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -96,9 +96,9 @@ private void ReadThread() } // Process packet data - var packet = readBuffer.Slice(0, bytesRead); - Debug.WriteLine($"-> {ParsingUtils.ToString(packet)}"); - HandlePacket(packet); + var packetData = readBuffer.Slice(0, bytesRead); + var xboxPacket = new XboxPacket(packetData, directionIn: true); + HandlePacket(xboxPacket); } readPackets = false; @@ -112,7 +112,7 @@ private int ReadPacket(Span readBuffer) } catch (Exception ex) { - Debug.WriteLine($"Error while reading packet: {ex}"); + PacketLogging.PrintVerboseException("Error while reading packet!", ex); return -1; } } @@ -126,7 +126,7 @@ protected override XboxResult SendPacket(Span data) } catch (Exception ex) { - Debug.WriteLine($"Error while sending packet: {ex}"); + PacketLogging.PrintVerboseException("Error while sending packet!", ex); return XboxResult.InvalidMessage; } } diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 5437b39..7e14cb7 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -40,11 +40,11 @@ public static DeviceMapper GetMapper(IEnumerable interfaceGuids, MappingMo if (interfaceGuid != default) { - Console.WriteLine($"More than one unique interface GUID found! Cannot get specific mapper, using fallback mapper instead."); - Console.WriteLine($"Consider filing a GitHub issue with the GUIDs below so that this can be addressed:"); + PacketLogging.PrintMessage($"More than one unique interface GUID found! Cannot get specific mapper, using fallback mapper instead."); + PacketLogging.PrintMessage($"Consider filing a GitHub issue with the GUIDs below so that this can be addressed:"); foreach (var guid2 in interfaceGuids) { - Console.WriteLine($"- {guid2}"); + PacketLogging.PrintMessage($"- {guid2}"); } return GetFallbackMapper(mode, client, mapGuide); } @@ -54,11 +54,11 @@ public static DeviceMapper GetMapper(IEnumerable interfaceGuids, MappingMo if (interfaceGuid == default) { - Console.WriteLine($"Could not find interface GUID for device! Using fallback mapper instead."); - Console.WriteLine($"Consider filing a GitHub issue with the GUIDs below so that this can be addressed:"); + PacketLogging.PrintMessage($"Could not find interface GUID for device! Using fallback mapper instead."); + PacketLogging.PrintMessage($"Consider filing a GitHub issue with the GUIDs below so that this can be addressed:"); foreach (var guid2 in interfaceGuids) { - Console.WriteLine($"- {guid2}"); + PacketLogging.PrintMessage($"- {guid2}"); } return GetFallbackMapper(mode, client, mapGuide); } @@ -66,8 +66,8 @@ public static DeviceMapper GetMapper(IEnumerable interfaceGuids, MappingMo // Get mapper creation delegate for interface GUID if (!guidToMapper.TryGetValue(interfaceGuid, out var func)) { - Console.WriteLine($"Could not get a specific mapper for interface GUID {interfaceGuid}! Using fallback mapper instead."); - Console.WriteLine($"Consider filing a GitHub issue with the GUID above so that it can be assigned the correct mapper."); + PacketLogging.PrintMessage($"Could not get a specific mapper for interface GUID {interfaceGuid}! Using fallback mapper instead."); + PacketLogging.PrintMessage($"Consider filing a GitHub issue with the GUID above so that it can be assigned the correct mapper."); return GetFallbackMapper(mode, client, mapGuide); } @@ -89,19 +89,19 @@ private static DeviceMapper GetMapper(MappingMode mode, XboxClient client, bool mapper = VjoyClient.AreDevicesAvailable ? createVjoy(client, mapGuide) : null; // Check if all devices have been used if (mapper != null && !VjoyClient.AreDevicesAvailable) - Console.WriteLine("vJoy device limit reached, no new devices will be handled."); + PacketLogging.PrintMessage("vJoy device limit reached, no new devices will be handled."); break; default: throw new NotImplementedException($"Unhandled mapping mode {mode}!"); } if (mapper != null) - Console.WriteLine($"Created new {mapper.GetType()} mapper"); + PacketLogging.PrintMessage($"Created new {mapper.GetType().Name}"); return mapper; } catch (Exception ex) { - Console.WriteLine($"Failed to create mapper for device: {ex.GetFirstLine()}"); + PacketLogging.PrintMessage($"Failed to create mapper for device: {ex.GetFirstLine()}"); return null; } } @@ -140,12 +140,12 @@ public static DeviceMapper GetWirelessLegacyMapper(MappingMode mode, XboxClient try { var mapper = new WirelessLegacyMapper(mode, client, mapGuide); - Console.WriteLine($"Created new {nameof(WirelessLegacyMapper)} mapper"); + PacketLogging.PrintMessage($"Created new {nameof(WirelessLegacyMapper)} mapper"); return mapper; } catch (Exception ex) { - Console.WriteLine($"Failed to create mapper for device: {ex.GetFirstLine()}"); + PacketLogging.PrintMessage($"Failed to create mapper for device: {ex.GetFirstLine()}"); return null; } } diff --git a/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs b/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs index b75d26e..b550c08 100644 --- a/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs @@ -36,7 +36,7 @@ private void DeviceConnected(object sender, Xbox360FeedbackReceivedEventArgs arg deviceConnected = true; // Log the user index - Console.WriteLine($"Created new ViGEmBus device with user index {args.LedNumber}"); + PacketLogging.PrintMessage($"Created new ViGEmBus device with user index {args.LedNumber}"); // Unregister the event handler (sender as IXbox360Controller).FeedbackReceived -= DeviceConnected; diff --git a/Program/PacketParsing/Mappers/WirelessLegacyMapper.cs b/Program/PacketParsing/Mappers/WirelessLegacyMapper.cs index 18f5beb..e1796d5 100644 --- a/Program/PacketParsing/Mappers/WirelessLegacyMapper.cs +++ b/Program/PacketParsing/Mappers/WirelessLegacyMapper.cs @@ -50,7 +50,7 @@ private unsafe XboxResult HandleInput(ReadOnlySpan data) byte userIndex = header.UserIndex; if (!mappers.TryGetValue(userIndex, out var mapper)) { - Debug.WriteLine($"Missing mapper for user index {userIndex}!"); + PacketLogging.PrintVerboseError($"Missing mapper for wireless legacy user index {userIndex}!"); return XboxResult.InvalidMessage; } @@ -67,7 +67,7 @@ private XboxResult HandleConnection(ReadOnlySpan data) byte userIndex = connect.UserIndex; if (mappers.TryGetValue(userIndex, out var mapper)) { - Debug.WriteLine($"Mapper already exists for user index {userIndex}! Overwriting."); + PacketLogging.PrintVerboseError($"Mapper already exists for legacy adapter user index {userIndex}! Overwriting."); mapper?.Dispose(); } @@ -85,7 +85,7 @@ private unsafe XboxResult HandleDisconnection(ReadOnlySpan data) byte userIndex = disconnect.UserIndex; if (!mappers.TryGetValue(userIndex, out var mapper)) { - Debug.WriteLine($"Missing mapper for user index {userIndex}!"); + PacketLogging.PrintVerboseError($"Missing mapper for legacy adapter user index {userIndex}!"); return XboxResult.InvalidMessage; } @@ -128,9 +128,8 @@ private DeviceMapper GetMapperForDevice(XboxWirelessLegacyDeviceConnect connect) return MapperFactory.GetGuitarMapper(mappingMode, client, mapGuideButton); default: - Debug.WriteLine($"Unsupported XInput subtype {subtype} on user index {connect.UserIndex}!"); - Console.WriteLine($"The Xbox 360 controller on user index {connect.UserIndex + 1} of the legacy adapter has an unsupported subtype ({subtype})!"); - Console.WriteLine("If you think it should be supported, restart capture with packet logging to a file enabled, and create a GitHub issue with the log file attached."); + PacketLogging.PrintMessage($"User index {connect.UserIndex + 1} on the legacy adapter has an unsupported subtype ({subtype})!"); + PacketLogging.PrintMessage("If you think it should be supported, restart capture with packet logging to a file enabled, go through all of the inputs, and create a GitHub issue with the log file attached."); return null; } } diff --git a/Program/PacketParsing/Mappers/vJoy/VjoyMapper.cs b/Program/PacketParsing/Mappers/vJoy/VjoyMapper.cs index 834ac23..af101e2 100644 --- a/Program/PacketParsing/Mappers/vJoy/VjoyMapper.cs +++ b/Program/PacketParsing/Mappers/vJoy/VjoyMapper.cs @@ -27,7 +27,7 @@ public VjoyMapper(XboxClient client, bool mapGuide) } state.bDevice = (byte)deviceId; - Console.WriteLine($"Acquired vJoy device with ID of {deviceId}"); + PacketLogging.PrintMessage($"Acquired vJoy device with ID of {deviceId}"); } protected override void MapGuideButton(bool pressed) diff --git a/Program/PacketParsing/PacketLogging.cs b/Program/PacketParsing/PacketLogging.cs new file mode 100644 index 0000000..a01b0fc --- /dev/null +++ b/Program/PacketParsing/PacketLogging.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics; + +namespace RB4InstrumentMapper.Parsing +{ + internal static class PacketLogging + { + public static void LogPacket(XboxPacket packet) + { + if (!BackendSettings.LogPackets) + return; + + string packetString = packet.ToString(); + LogMessage(packetString); + } + + public static void LogMessage(string message) + { + Debug.WriteLine(message); + Console.WriteLine(message); + Logging.Packet_WriteLine(message); + } + + public static void PrintMessage(string message) + { + Debug.WriteLine(message); + Console.WriteLine(message); + } + + public static void PrintVerboseError(string message) + { + // Always log errors to debug + Debug.WriteLine(message); + if (!BackendSettings.PrintVerboseErrors) + return; + + Console.WriteLine(message); + } + + public static void PrintException(string message, Exception ex) + { + Debug.WriteLine(message); + Debug.WriteLine(ex); + Logging.Main_WriteException(ex, message); + Console.WriteLine(message); + Console.WriteLine(ex.GetFirstLine()); + } + + public static void PrintVerboseException(string message, Exception ex) + { + // Always log errors to debug/log + Debug.WriteLine(message); + Debug.WriteLine(ex); + Logging.Main_WriteException(ex, message); + + if (!BackendSettings.PrintVerboseErrors) + return; + + Console.WriteLine(message); + Console.WriteLine(ex.GetFirstLine()); + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/System/XboxDescriptor.cs b/Program/PacketParsing/Packets/System/XboxDescriptor.cs index a5a021e..36ac55a 100644 --- a/Program/PacketParsing/Packets/System/XboxDescriptor.cs +++ b/Program/PacketParsing/Packets/System/XboxDescriptor.cs @@ -95,7 +95,7 @@ private unsafe bool Parse(ReadOnlySpan data) Debug.Fail($"Buffer size is smaller than size listed in header! Buffer size: {data.Length}, listed size: {header.DataLength}"); return false; } - Debug.WriteLineIf(data.Length != header.DataLength, $"Buffer size is not the same as size listed in header! Buffer size: {data.Length}, listed size: {header.DataLength}"); + Debug.Assert(data.Length == header.DataLength, $"Buffer size is not the same as size listed in header! Buffer size: {data.Length}, listed size: {header.DataLength}"); data = data.Slice(header.HeaderLength); // Data offsets diff --git a/Program/PacketParsing/Packets/XboxCommandHeader.cs b/Program/PacketParsing/Packets/XboxCommandHeader.cs index a0de8d0..323e730 100644 --- a/Program/PacketParsing/Packets/XboxCommandHeader.cs +++ b/Program/PacketParsing/Packets/XboxCommandHeader.cs @@ -164,7 +164,7 @@ private static bool DecodeLEB128(ReadOnlySpan data, out int result, out in // Detect length sequences longer than 4 bytes if ((value & 0x80) != 0) { - Debug.WriteLine($"Variable-length value is greater than 4 bytes! Buffer: {ParsingUtils.ToString(data)}"); + Debug.Fail($"Variable-length value is greater than 4 bytes! Buffer: {ParsingUtils.ToString(data)}"); byteLength = 0; result = 0; return false; @@ -199,7 +199,7 @@ private static bool EncodeLEB128(Span buffer, int value, out int byteLengt // Detect values too large to encode if (value > 0x7F) { - Debug.WriteLine($"Value to encode ({value}) is greater than allowed!"); + Debug.Fail($"Value to encode ({value}) is greater than allowed!"); return false; } diff --git a/Program/PacketParsing/Packets/XboxPacket.cs b/Program/PacketParsing/Packets/XboxPacket.cs new file mode 100644 index 0000000..695f039 --- /dev/null +++ b/Program/PacketParsing/Packets/XboxPacket.cs @@ -0,0 +1,28 @@ +using System; + +namespace RB4InstrumentMapper.Parsing +{ + internal ref struct XboxPacket + { + public bool DirectionIn; + public DateTime Time; + public ReadOnlySpan Header; + public ReadOnlySpan Data; + + public XboxPacket(ReadOnlySpan data, bool directionIn) + { + Data = data; + DirectionIn = directionIn; + + Header = ReadOnlySpan.Empty; + Time = DateTime.Now; + } + + public override string ToString() + { + return Header.IsEmpty + ? $"[{Time:yyyy-MM-dd hh:mm:ss.fff}] [{Data.Length:N2}] {(DirectionIn ? "->" : "<-")} {ParsingUtils.ToString(Data)}" + : $"[{Time:yyyy-MM-dd hh:mm:ss.fff}] [{Header.Length + Data.Length:N2}] {(DirectionIn ? "->" : "<-")} {ParsingUtils.ToString(Header)} | {ParsingUtils.ToString(Data)}"; + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxClient.cs b/Program/PacketParsing/XboxClient.cs index 0268768..a6a8dfc 100644 --- a/Program/PacketParsing/XboxClient.cs +++ b/Program/PacketParsing/XboxClient.cs @@ -15,6 +15,11 @@ internal class XboxClient : IDisposable /// public XboxDevice Parent { get; } + /// + /// The arrival info of the client. + /// + public XboxArrival Arrival { get; private set; } + /// /// The descriptor of the client. /// @@ -27,8 +32,6 @@ internal class XboxClient : IDisposable private DeviceMapper deviceMapper; - private bool arrivalReceived = false; - private readonly Dictionary previousReceiveSequence = new Dictionary(); private readonly Dictionary previousSendSequence = new Dictionary(); private readonly Dictionary chunkBuffers = new Dictionary() @@ -118,8 +121,8 @@ internal unsafe XboxResult HandleMessage(XboxCommandHeader header, ReadOnlySpan< return XboxResult.Success; } - Console.WriteLine("Warning: This device was not encountered during its initial connection! It will use the fallback mapper instead of one specific to its device interface."); - Console.WriteLine("Reconnect it (or hit Start before connecting it) to ensure correct behavior."); + PacketLogging.PrintMessage("Warning: This device was not encountered during its initial connection! It will use the fallback mapper instead of one specific to its device interface."); + PacketLogging.PrintMessage("Reconnect it (or hit Start before connecting it) to ensure correct behavior."); } return deviceMapper.HandleMessage(header.CommandId, commandData); @@ -150,14 +153,14 @@ private XboxResult HandleSystemCommand(byte commandId, ReadOnlySpan comman /// private unsafe XboxResult HandleArrival(ReadOnlySpan data) { - if (arrivalReceived) + if (Arrival.SerialNumber != 0) return XboxResult.Success; if (data.Length < sizeof(XboxArrival) || !MemoryMarshal.TryRead(data, out XboxArrival arrival)) return XboxResult.InvalidMessage; - Console.WriteLine($"New client connected with ID {arrival.SerialNumber:X12}"); - arrivalReceived = true; + PacketLogging.PrintMessage($"New client connected with ID {arrival.SerialNumber:X12}"); + Arrival = arrival; // Kick off descriptor request return SendMessage(XboxDescriptor.GetDescriptor); diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index e200166..457b73a 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -47,11 +47,15 @@ protected XboxDevice(MappingMode mode, bool mapGuide, int maxPacket) /// /// Handles an incoming packet for this device. /// - public unsafe XboxResult HandlePacket(ReadOnlySpan data) + public unsafe XboxResult HandlePacket(XboxPacket packet) { + // Debugging (if enabled) + PacketLogging.LogPacket(packet); + // Some devices may send multiple messages in a single packet, placing them back-to-back // The header length is very important in these scenarios, as it determines which bytes are part of the message // and where the next message's header begins. + var data = packet.Data; while (data.Length > 0) { // Command header @@ -84,10 +88,10 @@ public unsafe XboxResult HandlePacket(ReadOnlySpan data) case XboxResult.Disconnected: client.Dispose(); clients.Remove(header.Client); - Debug.WriteLine($"Client {header.Client} disconnected"); + PacketLogging.PrintMessage($"Client {client.Arrival.SerialNumber} disconnected"); break; default: - Debug.WriteLine($"Error handling message: {clientResult}"); + PacketLogging.PrintVerboseError($"Error handling message: {clientResult}"); break; } @@ -151,7 +155,8 @@ internal XboxResult SendMessage(XboxCommandHeader header, Span data) return XboxResult.InvalidMessage; } - Debug.WriteLine($"<- {ParsingUtils.ToString(packetBuffer)}"); + var xboxPacket = new XboxPacket(packetBuffer, directionIn: false); + PacketLogging.LogPacket(xboxPacket); // Attempt a few times const int retryThreshold = 3; diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index e1a93ed..a00697e 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -149,7 +149,9 @@ + + From 791efc944c144e990c7a2e3b40a757721df16a32 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 23 Aug 2023 10:05:51 -0600 Subject: [PATCH 219/437] Always map the guide button on WinUSB devices --- Program/PacketParsing/Backends/XboxWinUsbDevice.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs index ad962fb..464d56b 100644 --- a/Program/PacketParsing/Backends/XboxWinUsbDevice.cs +++ b/Program/PacketParsing/Backends/XboxWinUsbDevice.cs @@ -20,8 +20,8 @@ internal class XboxWinUsbDevice : XboxDevice private Thread readThread; private volatile bool readPackets = false; - private XboxWinUsbDevice(USBDevice usb, USBInterface @interface, MappingMode mode, bool mapGuide) - : base(mode, mapGuide, @interface.OutPipe.MaximumPacketSize) + private XboxWinUsbDevice(USBDevice usb, USBInterface @interface, MappingMode mode) + : base(mode, mapGuide: true, @interface.OutPipe.MaximumPacketSize) { usbDevice = usb; mainInterface = @interface; @@ -50,7 +50,7 @@ public static XboxWinUsbDevice TryCreate(string devicePath) return null; } - return new XboxWinUsbDevice(usbDevice, mainInterface, BackendSettings.MapperMode, BackendSettings.MapGuideButton); + return new XboxWinUsbDevice(usbDevice, mainInterface, BackendSettings.MapperMode); } public void StartReading() From 40988d68b12d614afc784077a5b314445d059b49 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 23 Aug 2023 13:41:57 -0600 Subject: [PATCH 220/437] Implement GHL guitar support --- .../PacketParsing/Mappers/MapperFactory.cs | 12 ++ .../Mappers/ViGEm/FallbackVigemMapper.cs | 9 +- .../Mappers/ViGEm/GHLGuitarVigemMapper.cs | 108 ++++++++++++++ .../Mappers/ViGEm/VigemMapper.cs | 8 +- .../Mappers/vJoy/FallbackVjoyMapper.cs | 9 +- .../Mappers/vJoy/GHLGuitarVjoyMapper.cs | 122 ++++++++++++++++ .../Packets/GHLGuitar/XboxGHLGuitarInput.cs | 95 ++++++++++++ .../Packets/GHLGuitar/XboxGHLGuitarOutput.cs | 136 ++++++++++++++++++ Program/PacketParsing/XboxDeviceGuids.cs | 2 + Program/RB4InstrumentMapper.csproj | 4 + 10 files changed, 502 insertions(+), 3 deletions(-) create mode 100644 Program/PacketParsing/Mappers/ViGEm/GHLGuitarVigemMapper.cs create mode 100644 Program/PacketParsing/Mappers/vJoy/GHLGuitarVjoyMapper.cs create mode 100644 Program/PacketParsing/Packets/GHLGuitar/XboxGHLGuitarInput.cs create mode 100644 Program/PacketParsing/Packets/GHLGuitar/XboxGHLGuitarOutput.cs diff --git a/Program/PacketParsing/Mappers/MapperFactory.cs b/Program/PacketParsing/Mappers/MapperFactory.cs index 7e14cb7..f8a72cf 100644 --- a/Program/PacketParsing/Mappers/MapperFactory.cs +++ b/Program/PacketParsing/Mappers/MapperFactory.cs @@ -18,8 +18,11 @@ internal static class MapperFactory { { XboxDeviceGuids.MadCatzGuitar, GetGuitarMapper }, { XboxDeviceGuids.PdpGuitar, GetGuitarMapper }, + { XboxDeviceGuids.MadCatzDrumkit, GetDrumsMapper }, { XboxDeviceGuids.PdpDrumkit, GetDrumsMapper }, + + { XboxDeviceGuids.ActivisionGuitarHeroLive, GetGHLGuitarMapper }, { XboxDeviceGuids.MadCatzLegacyWireless, GetWirelessLegacyMapper }, @@ -135,6 +138,15 @@ private static DeviceMapper VigemDrumsMapper(XboxClient client, bool mapGuide) private static DeviceMapper VjoyDrumsMapper(XboxClient client, bool mapGuide) => new DrumsVigemMapper(client, mapGuide); + public static DeviceMapper GetGHLGuitarMapper(MappingMode mode, XboxClient client, bool mapGuide) + => GetMapper(mode, client, mapGuide, VigemGHLGuitarMapper, VjoyGHLGuitarMapper); + + private static DeviceMapper VigemGHLGuitarMapper(XboxClient client, bool mapGuide) + => new GHLGuitarVigemMapper(client, mapGuide); + + private static DeviceMapper VjoyGHLGuitarMapper(XboxClient client, bool mapGuide) + => new GHLGuitarVigemMapper(client, mapGuide); + public static DeviceMapper GetWirelessLegacyMapper(MappingMode mode, XboxClient client, bool mapGuide) { try diff --git a/Program/PacketParsing/Mappers/ViGEm/FallbackVigemMapper.cs b/Program/PacketParsing/Mappers/ViGEm/FallbackVigemMapper.cs index c92186d..0062781 100644 --- a/Program/PacketParsing/Mappers/ViGEm/FallbackVigemMapper.cs +++ b/Program/PacketParsing/Mappers/ViGEm/FallbackVigemMapper.cs @@ -13,7 +13,7 @@ public FallbackVigemMapper(XboxClient client, bool mapGuide) { } - protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) + protected override unsafe XboxResult OnMessageReceived(byte command, ReadOnlySpan data) { switch (command) { @@ -25,6 +25,13 @@ protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan // #endif return ParseInput(data); + case XboxGHLGuitarInput.CommandId: + if (data.Length != sizeof(XboxGHLGuitarInput) || !MemoryMarshal.TryRead(data, out XboxGHLGuitarInput guitarReport)) + return XboxResult.InvalidMessage; + + GHLGuitarVigemMapper.HandleReport(device, guitarReport); + return XboxResult.Success; + default: return XboxResult.Success; } diff --git a/Program/PacketParsing/Mappers/ViGEm/GHLGuitarVigemMapper.cs b/Program/PacketParsing/Mappers/ViGEm/GHLGuitarVigemMapper.cs new file mode 100644 index 0000000..ba0e3cb --- /dev/null +++ b/Program/PacketParsing/Mappers/ViGEm/GHLGuitarVigemMapper.cs @@ -0,0 +1,108 @@ +using System; +using System.Runtime.InteropServices; +using Nefarius.ViGEm.Client.Targets; +using Nefarius.ViGEm.Client.Targets.Xbox360; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Maps GHL guitar inputs to a ViGEmBus device. + /// + internal class GHLGuitarVigemMapper : VigemMapper + { + private XboxGHLGuitarKeepAlive keepAlive; + + public GHLGuitarVigemMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) + { + keepAlive = new XboxGHLGuitarKeepAlive(client); + + // Set player LED + var setLeds = XboxGHLGuitarSetPlayerLeds.Create(GetPlayerLeds()); + client.SendMessage(setLeds); + } + + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) + { + switch (command) + { + case XboxGHLGuitarInput.CommandId: + return ParseInput(data); + + default: + return XboxResult.Success; + } + } + + private unsafe XboxResult ParseInput(ReadOnlySpan data) + { + if (data.Length != sizeof(XboxGHLGuitarInput) || !MemoryMarshal.TryRead(data, out XboxGHLGuitarInput guitarReport)) + return XboxResult.InvalidMessage; + + HandleReport(device, guitarReport); + + // Send data + device.SubmitReport(); + return XboxResult.Success; + } + + private XboxGHLGuitarPlayerLeds GetPlayerLeds() + { + switch (userIndex) + { + case 1: return XboxGHLGuitarPlayerLeds.Player1; + case 2: return XboxGHLGuitarPlayerLeds.Player2; + case 3: return XboxGHLGuitarPlayerLeds.Player3; + case 4: return XboxGHLGuitarPlayerLeds.Player4; + + default: + return XboxGHLGuitarPlayerLeds.All; + } + } + + /// + /// Maps guitar input data to an Xbox 360 controller. + /// + internal static void HandleReport(IXbox360Controller device, in XboxGHLGuitarInput report) + { + // Menu buttons + device.SetButtonState(Xbox360Button.Start, report.Pause); + device.SetButtonState(Xbox360Button.Back, report.HeroPower); + device.SetButtonState(Xbox360Button.LeftThumb, report.GHTV); + + // Dpad/strum + device.SetButtonState(Xbox360Button.Up, report.DpadUp | report.StrumUp); + device.SetButtonState(Xbox360Button.Down, report.DpadDown | report.StrumDown); + device.SetButtonState(Xbox360Button.Left, report.DpadLeft); + device.SetButtonState(Xbox360Button.Right, report.DpadRight); + + short strum = report.StrumUp ? short.MaxValue + : report.StrumDown ? short.MinValue + : (short)0; + device.SetAxisValue(Xbox360Axis.LeftThumbY, strum); + + // Frets + device.SetButtonState(Xbox360Button.A, report.Black1); + device.SetButtonState(Xbox360Button.B, report.Black2); + device.SetButtonState(Xbox360Button.Y, report.Black3); + device.SetButtonState(Xbox360Button.X, report.White1); + device.SetButtonState(Xbox360Button.LeftShoulder, report.White2); + device.SetButtonState(Xbox360Button.RightShoulder, report.White3); + + // Whammy + // Swapped compared to other guitars; also rests at center instead of negative end + device.SetAxisValue(Xbox360Axis.RightThumbY, report.WhammyBar.ScaleToInt16()); + // Tilt + // Swapped compared to other guitars + device.SetAxisValue(Xbox360Axis.RightThumbX, report.Tilt.ScaleToInt16()); + } + + protected override void DisposeManagedResources() + { + base.DisposeManagedResources(); + + keepAlive?.Dispose(); + keepAlive = null; + } + } +} diff --git a/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs b/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs index b550c08..e16a5e6 100644 --- a/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs +++ b/Program/PacketParsing/Mappers/ViGEm/VigemMapper.cs @@ -20,6 +20,11 @@ internal abstract class VigemMapper : DeviceMapper /// protected bool deviceConnected = false; + /// + /// The LED number for the emulated Xbox 360 controller. + /// + protected byte userIndex; + public VigemMapper(XboxClient client, bool mapGuide) : base(client, mapGuide) { @@ -36,7 +41,8 @@ private void DeviceConnected(object sender, Xbox360FeedbackReceivedEventArgs arg deviceConnected = true; // Log the user index - PacketLogging.PrintMessage($"Created new ViGEmBus device with user index {args.LedNumber}"); + userIndex = args.LedNumber; + PacketLogging.PrintMessage($"Created new ViGEmBus device with user index {userIndex}"); // Unregister the event handler (sender as IXbox360Controller).FeedbackReceived -= DeviceConnected; diff --git a/Program/PacketParsing/Mappers/vJoy/FallbackVjoyMapper.cs b/Program/PacketParsing/Mappers/vJoy/FallbackVjoyMapper.cs index 4c7557e..98582b9 100644 --- a/Program/PacketParsing/Mappers/vJoy/FallbackVjoyMapper.cs +++ b/Program/PacketParsing/Mappers/vJoy/FallbackVjoyMapper.cs @@ -14,7 +14,7 @@ public FallbackVjoyMapper(XboxClient client, bool mapGuide) { } - protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) + protected override unsafe XboxResult OnMessageReceived(byte command, ReadOnlySpan data) { switch (command) { @@ -26,6 +26,13 @@ protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan // #endif return ParseInput(data); + case XboxGHLGuitarInput.CommandId: + if (data.Length != sizeof(XboxGHLGuitarInput) || !MemoryMarshal.TryRead(data, out XboxGHLGuitarInput guitarReport)) + return XboxResult.InvalidMessage; + + GHLGuitarVjoyMapper.HandleReport(ref state, guitarReport); + return XboxResult.Success; + default: return XboxResult.Success; } diff --git a/Program/PacketParsing/Mappers/vJoy/GHLGuitarVjoyMapper.cs b/Program/PacketParsing/Mappers/vJoy/GHLGuitarVjoyMapper.cs new file mode 100644 index 0000000..3af72af --- /dev/null +++ b/Program/PacketParsing/Mappers/vJoy/GHLGuitarVjoyMapper.cs @@ -0,0 +1,122 @@ +using System; +using System.Runtime.InteropServices; +using RB4InstrumentMapper.Vjoy; +using vJoyInterfaceWrap; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Maps GHL guitar inputs to a vJoy device. + /// + internal class GHLGuitarVjoyMapper : VjoyMapper + { + private XboxGHLGuitarKeepAlive keepAlive; + + public GHLGuitarVjoyMapper(XboxClient client, bool mapGuide) + : base(client, mapGuide) + { + keepAlive = new XboxGHLGuitarKeepAlive(client); + + // Set player LED + var setLeds = XboxGHLGuitarSetPlayerLeds.Create(GetPlayerLeds()); + client.SendMessage(setLeds); + } + + protected override XboxResult OnMessageReceived(byte command, ReadOnlySpan data) + { + switch (command) + { + case XboxGHLGuitarInput.CommandId: + return ParseInput(data); + + default: + return XboxResult.Success; + } + } + + private unsafe XboxResult ParseInput(ReadOnlySpan data) + { + if (data.Length != sizeof(XboxGHLGuitarInput) || !MemoryMarshal.TryRead(data, out XboxGHLGuitarInput guitarReport)) + return XboxResult.InvalidMessage; + + HandleReport(ref state, guitarReport); + + // Send data + VjoyClient.UpdateDevice(deviceId, ref state); + return XboxResult.Success; + } + + private XboxGHLGuitarPlayerLeds GetPlayerLeds() + { + switch (deviceId) + { + case 1: return XboxGHLGuitarPlayerLeds.Player1; + case 2: return XboxGHLGuitarPlayerLeds.Player2; + case 3: return XboxGHLGuitarPlayerLeds.Player3; + case 4: return XboxGHLGuitarPlayerLeds.Player4; + case 5: return XboxGHLGuitarPlayerLeds.Player5; + case 6: return XboxGHLGuitarPlayerLeds.Player6; + case 7: return XboxGHLGuitarPlayerLeds.Player7; + case 8: return XboxGHLGuitarPlayerLeds.Player8; + case 9: return XboxGHLGuitarPlayerLeds.Player9; + case 10: return XboxGHLGuitarPlayerLeds.Player10; + case 11: return XboxGHLGuitarPlayerLeds.Player11; + case 12: return XboxGHLGuitarPlayerLeds.Player12; + case 13: return XboxGHLGuitarPlayerLeds.Player13; + case 14: return XboxGHLGuitarPlayerLeds.Player14; + case 15: return XboxGHLGuitarPlayerLeds.Player15; + + // If someone connects this many devices it's their problem lol + case 16: + default: + return XboxGHLGuitarPlayerLeds.All; + } + } + + /// + /// Maps GHL guitar input data to a vJoy device. + /// + internal static void HandleReport(ref vJoy.JoystickState state, XboxGHLGuitarInput report) + { + // Menu buttons + state.SetButton(VjoyButton.Fifteen, report.Pause); + state.SetButton(VjoyButton.Sixteen, report.HeroPower); + state.SetButton(VjoyButton.Twelve, report.GHTV); + + // D-pad/strum + // It would be more efficient to directly map the GHL value to the vJoy value, + // but that doesn't account for the strum bar being on its own axis + XboxGamepadButton dpad = 0; + if (report.DpadUp | report.StrumUp) dpad |= XboxGamepadButton.DpadUp; + if (report.DpadDown | report.StrumDown) dpad |= XboxGamepadButton.DpadDown; + if (report.DpadLeft) dpad |= XboxGamepadButton.DpadLeft; + if (report.DpadRight) dpad |= XboxGamepadButton.DpadRight; + ParseDpad(ref state, dpad); + + // Frets + state.SetButton(VjoyButton.One, report.Black1); + state.SetButton(VjoyButton.Two, report.Black2); + state.SetButton(VjoyButton.Three, report.Black3); + state.SetButton(VjoyButton.Four, report.White1); + state.SetButton(VjoyButton.Five, report.White2); + state.SetButton(VjoyButton.Six, report.White3); + + // Whammy + // Value ranges from 128 (not pressed) to 255 (fully pressed) + byte whammy = (byte)((report.WhammyBar - 0x80) * 2); + SetAxis(ref state.AxisY, whammy); + + // Tilt + // Value ranges from 0 to 255 + SetAxis(ref state.AxisZ, report.Tilt); + } + + protected override void DisposeManagedResources() + { + base.DisposeManagedResources(); + + keepAlive?.Dispose(); + keepAlive = null; + } + } +} diff --git a/Program/PacketParsing/Packets/GHLGuitar/XboxGHLGuitarInput.cs b/Program/PacketParsing/Packets/GHLGuitar/XboxGHLGuitarInput.cs new file mode 100644 index 0000000..c809e7e --- /dev/null +++ b/Program/PacketParsing/Packets/GHLGuitar/XboxGHLGuitarInput.cs @@ -0,0 +1,95 @@ +using System; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + /// + /// Button flags for GHL guitars. + /// + [Flags] + internal enum XboxGHLGuitarButton : ushort + { + White1 = 0x0001, + Black1 = 0x0002, + Black2 = 0x0004, + Black3 = 0x0008, + White2 = 0x0010, + White3 = 0x0020, + + Select = 0x0100, + Start = 0x0200, + GHTV = 0x0400, + + // Already handled by the guide button messages + // DpadCenter = 0x1000, + } + + /// + /// D-pad states for GHL guitars. + /// + public enum XboxGHLGuitarDpad : byte + { + // vJoy continuous PoV hat values range from 0 to 35999 (measured in 1/100 of a degree). + // The value is measured clockwise, with up being 0. + Neutral = 0x0F, + Up = 0, + UpRight = 1, + Right = 2, + DownRight = 3, + Down = 4, + DownLeft = 5, + Left = 6, + UpLeft = 7 + } + + /// + /// An input report from a guitar. + /// + [StructLayout(LayoutKind.Explicit, Pack = 1)] + internal struct XboxGHLGuitarInput + { + public const byte CommandId = 0x21; + + public const byte StrumbarCenter = 0x80; + + // For reference; PS3 stick axes put up as 0x00 and down as 0xFF + // public const byte StrumbarUp = 0x00; + // public const byte StrumbarDown = 0xFF; + + [FieldOffset(0)] + public XboxGHLGuitarButton Buttons; + + [FieldOffset(2)] + public XboxGHLGuitarDpad Dpad; + + [FieldOffset(4)] + public byte StrumBar; + + [FieldOffset(6)] + public byte WhammyBar; + + [FieldOffset(19)] + public byte Tilt; + + public bool Black1 => (Buttons & XboxGHLGuitarButton.Black1) != 0; + public bool Black2 => (Buttons & XboxGHLGuitarButton.Black2) != 0; + public bool Black3 => (Buttons & XboxGHLGuitarButton.Black3) != 0; + public bool White1 => (Buttons & XboxGHLGuitarButton.White1) != 0; + public bool White2 => (Buttons & XboxGHLGuitarButton.White2) != 0; + public bool White3 => (Buttons & XboxGHLGuitarButton.White3) != 0; + + public bool HeroPower => (Buttons & XboxGHLGuitarButton.Select) != 0; + public bool Pause => (Buttons & XboxGHLGuitarButton.Start) != 0; + public bool GHTV => (Buttons & XboxGHLGuitarButton.GHTV) != 0; + + // public bool DpadCenter => (Buttons & XboxGHLGuitarButton.DpadCenter) != 0; + + public bool DpadUp => Dpad == XboxGHLGuitarDpad.Up || Dpad == XboxGHLGuitarDpad.UpLeft || Dpad == XboxGHLGuitarDpad.UpRight; + public bool DpadDown => Dpad == XboxGHLGuitarDpad.Down || Dpad == XboxGHLGuitarDpad.DownLeft || Dpad == XboxGHLGuitarDpad.DownRight; + public bool DpadLeft => Dpad == XboxGHLGuitarDpad.Left || Dpad == XboxGHLGuitarDpad.UpLeft || Dpad == XboxGHLGuitarDpad.DownLeft; + public bool DpadRight => Dpad == XboxGHLGuitarDpad.Right || Dpad == XboxGHLGuitarDpad.UpRight || Dpad == XboxGHLGuitarDpad.DownRight; + + public bool StrumUp => StrumBar < StrumbarCenter; + public bool StrumDown => StrumBar > StrumbarCenter; + } +} \ No newline at end of file diff --git a/Program/PacketParsing/Packets/GHLGuitar/XboxGHLGuitarOutput.cs b/Program/PacketParsing/Packets/GHLGuitar/XboxGHLGuitarOutput.cs new file mode 100644 index 0000000..aa922dc --- /dev/null +++ b/Program/PacketParsing/Packets/GHLGuitar/XboxGHLGuitarOutput.cs @@ -0,0 +1,136 @@ +using System; +using System.Threading; +using System.Runtime.InteropServices; + +namespace RB4InstrumentMapper.Parsing +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal unsafe struct XboxGHLGuitarOutput + { + public const byte CommandId = 0x22; + + public byte SubCommand; + public fixed byte Data[7]; + } + + [Flags] + internal enum XboxGHLGuitarPlayerLeds : byte + { + None = 0, + + Led1 = 0x01, + Led2 = 0x02, + Led3 = 0x04, + Led4 = 0x08, + + All = Led1 | Led2 | Led3 | Led4, + + Player1 = Led1, + Player2 = Led2, + Player3 = Led3, + Player4 = Led4, + Player5 = Led1 | Led2, + Player6 = Led1 | Led3, + Player7 = Led1 | Led4, + Player8 = Led2 | Led3, + Player9 = Led2 | Led4, + Player10 = Led3 | Led4, + Player11 = Led1 | Led2 | Led3, + Player12 = Led1 | Led2 | Led4, + Player13 = Led1 | Led3 | Led4, + Player14 = Led2 | Led3 | Led4, + Player15 = All, + + // If someone connects this many devices it's their problem that this is the same as 15 lol + Player16 = All, + } + + internal static class XboxGHLGuitarSetPlayerLeds + { + public const byte SubCommand = 0x01; + + public static unsafe XboxMessage Create(XboxGHLGuitarPlayerLeds leds) + { + var output = new XboxGHLGuitarOutput() + { + SubCommand = SubCommand, + }; + + output.Data[0] = 0x08; + output.Data[1] = (byte)leds; + + return new XboxMessage() + { + Header = new XboxCommandHeader() + { + CommandId = XboxGHLGuitarOutput.CommandId, + Flags = XboxCommandFlags.None, + }, + Data = output, + }; + } + } + + internal class XboxGHLGuitarKeepAlive : IDisposable + { + public const byte SubCommand = 0x02; + public const int SendPeriodMilliseconds = 8000; + + public static readonly XboxMessage Message = CreateMessage(); + + private readonly XboxClient client; + private readonly Timer sendTimer; + + public unsafe XboxGHLGuitarKeepAlive(XboxClient client) + { + this.client = client; + sendTimer = new Timer(SendKeepAlive, null, 0, SendPeriodMilliseconds); + } + + ~XboxGHLGuitarKeepAlive() + { + Dispose(false); + } + + private static unsafe XboxMessage CreateMessage() + { + var output = new XboxGHLGuitarOutput() + { + SubCommand = SubCommand, + }; + + // Unknown magic data + output.Data[0] = 0x08; + output.Data[1] = 0x0A; + + return new XboxMessage() + { + Header = new XboxCommandHeader() + { + CommandId = XboxGHLGuitarOutput.CommandId, + Flags = XboxCommandFlags.None, + }, + Data = output, + }; + } + + private void SendKeepAlive(object _) => client.SendMessage(Message); + + /// + /// Disposes the mapper and any resources it uses. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + sendTimer.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/Program/PacketParsing/XboxDeviceGuids.cs b/Program/PacketParsing/XboxDeviceGuids.cs index a63f620..52e99c7 100644 --- a/Program/PacketParsing/XboxDeviceGuids.cs +++ b/Program/PacketParsing/XboxDeviceGuids.cs @@ -17,5 +17,7 @@ internal static class XboxDeviceGuids public static readonly Guid PdpGuitar = Guid.Parse("1A266AF6-3A46-45E3-B9B6-0F2C0B2C1EBE"); public static readonly Guid PdpDrumkit = Guid.Parse("A503F9B0-955E-47C4-A2ED-B1336FA7703E"); + + public static readonly Guid ActivisionGuitarHeroLive = Guid.Parse("FD12FDD9-8E73-47C7-A231-96268C38009A"); } } diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index a00697e..cda50d4 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -122,11 +122,13 @@ + + @@ -134,6 +136,8 @@ + + From 6555a7f11aa19e88824abddf95389f88efacf853 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 24 Aug 2023 05:25:54 -0600 Subject: [PATCH 221/437] Upgrade installer to WiX v4 --- Installer/Product.wxs | 37 +++++++------- .../RB4InstrumentMapperInstaller.wixproj | 49 +++---------------- README.md | 2 +- 3 files changed, 25 insertions(+), 63 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 219ab00..797c853 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -1,24 +1,23 @@ - - - + + - - + UpgradeCode="94bef546-701f-4571-9828-d4fa39b2ea84" + InstallerVersion="200" + Scope="perMachine"> - - + @@ -28,18 +27,16 @@ - + - - - - - - - - - + + + + + + + @@ -120,7 +117,7 @@ Type="integer" Value="1" KeyPath="yes" /> - - + + - Debug x64 - 3.10 - 047562bb-6d63-4259-8e2e-0a5e834190b1 - 2.0 - RB4InstrumentMapperInstaller - Package RB4InstrumentMapperInstaller - - bin\$(Configuration)\ - obj\$(Configuration)\ - Debug - - - bin\$(Configuration)\ - obj\$(Configuration)\ - - - false - + - + + + - - - - - $(WixToolPath)WixUIExtension.dll - WixUIExtension - - - - - - - - + \ No newline at end of file diff --git a/README.md b/README.md index 5a58c46..b2589cb 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Additional documentation is available in the [PlasticBand documentation reposito To build this program, you will need: - Visual Studio (or MSBuild + your code editor of choice). -- [WiX Toolset](https://wixtoolset.org/) if you wish to build the installer. +- [WiX Toolset v4](https://wixtoolset.org/) if you wish to build the installer. ## License From 71307ecc9c8a6315a8af68650ab31627e73b0aae Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 24 Aug 2023 21:17:49 -0600 Subject: [PATCH 222/437] Convert program project file to SDK style --- Program/RB4InstrumentMapper.csproj | 246 +++++------------------------ Program/packages.config | 13 -- RB4InstrumentMapper.sln | 30 ++-- README.md | 2 +- 4 files changed, 50 insertions(+), 241 deletions(-) delete mode 100644 Program/packages.config diff --git a/Program/RB4InstrumentMapper.csproj b/Program/RB4InstrumentMapper.csproj index cda50d4..ef0f908 100644 --- a/Program/RB4InstrumentMapper.csproj +++ b/Program/RB4InstrumentMapper.csproj @@ -1,225 +1,53 @@ - - - + + - Debug - AnyCPU - {93041197-1E1B-4084-B1DD-C8363A588C48} + net472 + x64 WinExe - RB4InstrumentMapper - RB4InstrumentMapper - icon.ico - v4.7.2 - 512 - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 4 - true - true + true + true true + false + true + icon.ico - - AnyCPU - true - portable - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - true - bin\x64\Debug\ - DEBUG;TRACE - portable - x64 - 7.3 - prompt - true - - - bin\x64\Release\ - TRACE - true - pdbonly - x64 - 7.3 - prompt - true - + - - packages\Nefarius.Drivers.WinUSB.4.3.83\lib\net472\Nefarius.Drivers.WinUSB.dll - - - packages\Nefarius.Utilities.DeviceManagement.3.14.305\lib\netstandard2.0\Nefarius.Utilities.DeviceManagement.dll - - - packages\Nefarius.ViGEm.Client.1.21.256\lib\netstandard2.0\Nefarius.ViGEm.Client.dll - - - packages\PacketDotNet.1.4.7\lib\net47\PacketDotNet.dll - - - packages\SharpPcap.6.2.5\lib\netstandard2.0\SharpPcap.dll - - - packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - - packages\System.Memory.4.5.5\lib\net461\System.Memory.dll - - - - packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - - packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - - packages\System.Text.Encoding.CodePages.7.0.0\lib\net462\System.Text.Encoding.CodePages.dll - - + + + + + + + + + + + + + + true False Dependencies\x64\vJoyInterfaceWrap.dll - - - - - - - - - - 4.0 - - - - - - - - MSBuild:Compile - Designer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MSBuild:Compile - Designer - - - App.xaml - Code - - - - MainWindow.xaml - Code - - - - - Code - - - True - True - Resources.resx - - - True - Settings.settings - True - - - ResXFileCodeGenerator - Resources.Designer.cs - - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - - - + + - - + + true + False + Dependencies\x86\vJoyInterfaceWrap.dll + + + - - + + + + - - - copy $(ProjectDir)Dependencies\x64\vJoyInterface.dll $(TargetDir). - \ No newline at end of file diff --git a/Program/packages.config b/Program/packages.config deleted file mode 100644 index febb7df..0000000 --- a/Program/packages.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/RB4InstrumentMapper.sln b/RB4InstrumentMapper.sln index 8fe25c1..11acd10 100644 --- a/RB4InstrumentMapper.sln +++ b/RB4InstrumentMapper.sln @@ -1,43 +1,37 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31729.503 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RB4InstrumentMapper", "Program\RB4InstrumentMapper.csproj", "{93041197-1E1B-4084-B1DD-C8363A588C48}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RB4InstrumentMapper", "Program\RB4InstrumentMapper.csproj", "{93041197-1E1B-4084-B1DD-C8363A588C48}" EndProject -Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "RB4InstrumentMapperInstaller", "Installer\RB4InstrumentMapperInstaller.wixproj", "{047562BB-6D63-4259-8E2E-0A5E834190B1}" +Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "RB4InstrumentMapperInstaller", "Installer\RB4InstrumentMapperInstaller.wixproj", "{047562BB-6D63-4259-8E2E-0A5E834190B1}" ProjectSection(ProjectDependencies) = postProject {93041197-1E1B-4084-B1DD-C8363A588C48} = {93041197-1E1B-4084-B1DD-C8363A588C48} EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|Any CPU.ActiveCfg = Debug|x64 - {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|Any CPU.Build.0 = Debug|x64 - {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|x64.ActiveCfg = Debug|x64 - {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|x64.Build.0 = Debug|x64 + {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|x64.ActiveCfg = Debug|Any CPU + {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|x64.Build.0 = Debug|Any CPU {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|x86.ActiveCfg = Debug|Any CPU {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|x86.Build.0 = Debug|Any CPU - {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|Any CPU.ActiveCfg = Release|Any CPU - {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|Any CPU.Build.0 = Release|Any CPU - {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|x64.ActiveCfg = Release|x64 - {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|x64.Build.0 = Release|x64 + {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|x64.ActiveCfg = Release|Any CPU + {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|x64.Build.0 = Release|Any CPU {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|x86.ActiveCfg = Release|Any CPU {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|x86.Build.0 = Release|Any CPU - {047562BB-6D63-4259-8E2E-0A5E834190B1}.Debug|Any CPU.ActiveCfg = Debug|x86 - {047562BB-6D63-4259-8E2E-0A5E834190B1}.Debug|x64.ActiveCfg = Debug|x86 + {047562BB-6D63-4259-8E2E-0A5E834190B1}.Debug|x64.ActiveCfg = Debug|x64 + {047562BB-6D63-4259-8E2E-0A5E834190B1}.Debug|x64.Build.0 = Debug|x64 {047562BB-6D63-4259-8E2E-0A5E834190B1}.Debug|x86.ActiveCfg = Debug|x86 {047562BB-6D63-4259-8E2E-0A5E834190B1}.Debug|x86.Build.0 = Debug|x86 - {047562BB-6D63-4259-8E2E-0A5E834190B1}.Release|Any CPU.ActiveCfg = Release|x86 - {047562BB-6D63-4259-8E2E-0A5E834190B1}.Release|x64.ActiveCfg = Release|x86 + {047562BB-6D63-4259-8E2E-0A5E834190B1}.Release|x64.ActiveCfg = Release|x64 + {047562BB-6D63-4259-8E2E-0A5E834190B1}.Release|x64.Build.0 = Release|x64 {047562BB-6D63-4259-8E2E-0A5E834190B1}.Release|x86.ActiveCfg = Release|x86 {047562BB-6D63-4259-8E2E-0A5E834190B1}.Release|x86.Build.0 = Release|x86 EndGlobalSection diff --git a/README.md b/README.md index b2589cb..0c8d3ce 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ Additional documentation is available in the [PlasticBand documentation reposito To build this program, you will need: -- Visual Studio (or MSBuild + your code editor of choice). +- Visual Studio, or MSBuild/[the .NET SDK](https://dotnet.microsoft.com/en-us/download) + your code editor of choice. - [WiX Toolset v4](https://wixtoolset.org/) if you wish to build the installer. ## License From 59d303fe5fd1df3c6713efd99011352a84745a71 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 24 Aug 2023 21:52:52 -0600 Subject: [PATCH 223/437] Update installer binaries dir and add missing file references --- Installer/Product.wxs | 11 ++++++++++- Installer/RB4InstrumentMapperInstaller.wixproj | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 797c853..9b701d8 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -41,7 +41,10 @@ - + + + + @@ -75,6 +78,12 @@ + + + + + + diff --git a/Installer/RB4InstrumentMapperInstaller.wixproj b/Installer/RB4InstrumentMapperInstaller.wixproj index 413f706..370fb32 100644 --- a/Installer/RB4InstrumentMapperInstaller.wixproj +++ b/Installer/RB4InstrumentMapperInstaller.wixproj @@ -1,13 +1,13 @@ - x64 + x64 RB4InstrumentMapperInstaller - + From 1cef73f2204809364d465229773f72eb2012d8a7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 25 Aug 2023 04:00:43 -0600 Subject: [PATCH 224/437] Move unhandled exception event handler to App --- Program/App.xaml.cs | 69 +++++++++++++++++++++++---- Program/MainWindow/MainWindow.xaml.cs | 59 ----------------------- 2 files changed, 61 insertions(+), 67 deletions(-) diff --git a/Program/App.xaml.cs b/Program/App.xaml.cs index 43dfb10..6bea865 100644 --- a/Program/App.xaml.cs +++ b/Program/App.xaml.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; +using System; using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.IO; using System.Text; -using System.Threading.Tasks; using System.Windows; +using RB4InstrumentMapper.Properties; namespace RB4InstrumentMapper { @@ -17,5 +11,64 @@ namespace RB4InstrumentMapper /// public partial class App : Application { + protected override void OnStartup(StartupEventArgs e) + { + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + } + + /// + /// Logs unhandled exceptions to a file and prompts the user with the exception message. + /// + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) + { + // The unhandled exception + var unhandledException = args.ExceptionObject as Exception; + + // MessageBox message + var message = new StringBuilder(); + message.AppendLine("An unhandled error has occured:"); + message.AppendLine(); + message.AppendLine(unhandledException.GetFirstLine()); + message.AppendLine(); + + // Create log if it hasn't been created yet + Logging.CreateMainLog(); + // Use an alternate message if log couldn't be created + if (Logging.MainLogExists) + { + // Log exception + Logging.Main_WriteLine("-------------------"); + Logging.Main_WriteLine("UNHANDLED EXCEPTION"); + Logging.Main_WriteLine("-------------------"); + Logging.Main_WriteException(unhandledException, "Unhandled exception!"); + + // Complete the message buffer + message.AppendLine("A log of the error has been created, do you want to open it?"); + + // Display message + var result = MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.YesNo, MessageBoxImage.Error); + // If user requested to, open the log + if (result == MessageBoxResult.Yes) + { + Process.Start(Logging.LogFolderPath); + } + } + else + { + // Complete the message buffer + message.AppendLine("An error log was unable to be created."); + + // Display message + MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.OK, MessageBoxImage.Error); + } + + // Close the log files + Logging.CloseAll(); + // Save settings + Settings.Default.Save(); + + // Close program + Shutdown(); + } } } diff --git a/Program/MainWindow/MainWindow.xaml.cs b/Program/MainWindow/MainWindow.xaml.cs index 712d631..3afc6a5 100644 --- a/Program/MainWindow/MainWindow.xaml.cs +++ b/Program/MainWindow/MainWindow.xaml.cs @@ -59,9 +59,6 @@ private enum ControllerType public MainWindow() { - // Assign event handler for unhandled exceptions - AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; - InitializeComponent(); var version = Assembly.GetEntryAssembly().GetName().Version; @@ -712,61 +709,5 @@ private void pcapAutoDetectButton_Click(object sender, RoutedEventArgs e) // Refresh the dropdown PopulatePcapDropdown(); } - - /// - /// Logs unhandled exceptions to a file and prompts the user with the exception message. - /// - public static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) - { - // The unhandled exception - var unhandledException = args.ExceptionObject as Exception; - - // MessageBox message - var message = new StringBuilder(); - message.AppendLine("An unhandled error has occured:"); - message.AppendLine(); - message.AppendLine(unhandledException.GetFirstLine()); - message.AppendLine(); - - // Create log if it hasn't been created yet - Logging.CreateMainLog(); - // Use an alternate message if log couldn't be created - if (Logging.MainLogExists) - { - // Log exception - Logging.Main_WriteLine("-------------------"); - Logging.Main_WriteLine("UNHANDLED EXCEPTION"); - Logging.Main_WriteLine("-------------------"); - Logging.Main_WriteException(unhandledException, "Unhandled exception!"); - - // Complete the message buffer - message.AppendLine("A log of the error has been created, do you want to open it?"); - - // Display message - var result = MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.YesNo, MessageBoxImage.Error); - // If user requested to, open the log - if (result == MessageBoxResult.Yes) - { - Process.Start(Logging.LogFolderPath); - } - } - else - { - // Complete the message buffer - message.AppendLine("An error log was unable to be created."); - - // Display message - MessageBox.Show(message.ToString(), "Unhandled Error", MessageBoxButton.OK, MessageBoxImage.Error); - } - - // Close the log files - Logging.CloseAll(); - // Save settings - Settings.Default.Save(); - - // Close program - MessageBox.Show("The program will now shut down.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); - Application.Current.Shutdown(); - } } } From 7cbf544554fa8e8679abfd2ef2c141529405b2df Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 25 Aug 2023 04:42:30 -0600 Subject: [PATCH 225/437] Bump version to 4.0.0.0 --- Installer/Product.wxs | 2 +- Program/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Installer/Product.wxs b/Installer/Product.wxs index 9b701d8..a5f1fe0 100644 --- a/Installer/Product.wxs +++ b/Installer/Product.wxs @@ -4,7 +4,7 @@ Date: Fri, 25 Aug 2023 04:49:15 -0600 Subject: [PATCH 226/437] Remove setting for mapping the guide button --- Program/PacketParsing/Backends/BackendSettings.cs | 5 ----- Program/PacketParsing/Backends/PcapBackend.cs | 2 +- Program/PacketParsing/Backends/WinUsbBackend.cs | 4 ++-- Program/PacketParsing/XboxDevice.cs | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Program/PacketParsing/Backends/BackendSettings.cs b/Program/PacketParsing/Backends/BackendSettings.cs index add3105..110b27a 100644 --- a/Program/PacketParsing/Backends/BackendSettings.cs +++ b/Program/PacketParsing/Backends/BackendSettings.cs @@ -25,10 +25,5 @@ public static class BackendSettings /// Whether or not verbose errors should be logged to the console. /// public static bool PrintVerboseErrors { get; set; } = false; - - /// - /// Whether or not the guide button is mapped. - /// - public static bool MapGuideButton { get; set; } = false; } } \ No newline at end of file diff --git a/Program/PacketParsing/Backends/PcapBackend.cs b/Program/PacketParsing/Backends/PcapBackend.cs index 3bc9b36..ebe8c71 100644 --- a/Program/PacketParsing/Backends/PcapBackend.cs +++ b/Program/PacketParsing/Backends/PcapBackend.cs @@ -123,7 +123,7 @@ private static unsafe void OnPacketArrival(object sender, PacketCapture packet) ulong deviceId = header.DeviceId; if (!devices.TryGetValue(deviceId, out var device)) { - device = new XboxDevice(BackendSettings.MapperMode, BackendSettings.MapGuideButton); + device = new XboxDevice(BackendSettings.MapperMode); devices.Add(deviceId, device); PacketLogging.PrintMessage($"Device with ID {deviceId:X12} was connected"); } diff --git a/Program/PacketParsing/Backends/WinUsbBackend.cs b/Program/PacketParsing/Backends/WinUsbBackend.cs index f5f5e84..9397a44 100644 --- a/Program/PacketParsing/Backends/WinUsbBackend.cs +++ b/Program/PacketParsing/Backends/WinUsbBackend.cs @@ -90,7 +90,7 @@ private static void AddDevice(string devicePath) if (started) device.StartReading(); - Debug.WriteLine($"Added device {devicePath}"); + PacketLogging.PrintMessage($"Added device {devicePath}"); DeviceAddedOrRemoved?.Invoke(); } @@ -104,7 +104,7 @@ private static void RemoveDevice(string devicePath) devices.Remove(devicePath); device.Dispose(); - Debug.WriteLine($"Removed device {devicePath}"); + PacketLogging.PrintMessage($"Removed device {devicePath}"); DeviceAddedOrRemoved?.Invoke(); } } diff --git a/Program/PacketParsing/XboxDevice.cs b/Program/PacketParsing/XboxDevice.cs index 457b73a..ac136b0 100644 --- a/Program/PacketParsing/XboxDevice.cs +++ b/Program/PacketParsing/XboxDevice.cs @@ -28,7 +28,7 @@ internal class XboxDevice : IDisposable public MappingMode MappingMode { get; } public bool MapGuideButton { get; } - public XboxDevice(MappingMode mode, bool mapGuide) : this(mode, mapGuide, 0) + public XboxDevice(MappingMode mode) : this(mode, mapGuide: false, 0) { } From 239f1d0707b2235131f550e24d41a8d111f50e76 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 25 Aug 2023 07:37:01 -0600 Subject: [PATCH 227/437] Remove x86 binaries and configurations Previous project setup didn't compile right with how it was set up, easiest solution was to kill x86 (was never really supported in the first place lol) --- Program/Dependencies/x86/vJoyInterface.dll | Bin 584704 -> 0 bytes .../Dependencies/x86/vJoyInterfaceWrap.dll | Bin 15872 -> 0 bytes Program/RB4InstrumentMapper.csproj | 18 ++++-------------- RB4InstrumentMapper.sln | 10 ---------- 4 files changed, 4 insertions(+), 24 deletions(-) delete mode 100644 Program/Dependencies/x86/vJoyInterface.dll delete mode 100644 Program/Dependencies/x86/vJoyInterfaceWrap.dll diff --git a/Program/Dependencies/x86/vJoyInterface.dll b/Program/Dependencies/x86/vJoyInterface.dll deleted file mode 100644 index ba94762b8c763a4b61068c78c82646eecab18132..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 584704 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

%Ddw|PMDUx(7B2|#>lAsyGk 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�@) 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%0D=MIg|>3Ugd} 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

tIvNO7$N{xWlg^d_e6E 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

R^;Dl5T&>@fo<4cwd+E1b?iA`P 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 diff --git a/Program/Dependencies/x86/vJoyInterfaceWrap.dll b/Program/Dependencies/x86/vJoyInterfaceWrap.dll deleted file mode 100644 index e00f683223f9d64269728b0e05b37747552fd757..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15872 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 true icon.ico + + This project only supports x64 platforms! @@ -23,31 +25,19 @@ - - true False Dependencies\x64\vJoyInterfaceWrap.dll - - - - - - true - False - Dependencies\x86\vJoyInterfaceWrap.dll - - + - - + \ No newline at end of file diff --git a/RB4InstrumentMapper.sln b/RB4InstrumentMapper.sln index 11acd10..6f5f6c3 100644 --- a/RB4InstrumentMapper.sln +++ b/RB4InstrumentMapper.sln @@ -13,27 +13,17 @@ EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 Release|x64 = Release|x64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|x64.ActiveCfg = Debug|Any CPU {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|x64.Build.0 = Debug|Any CPU - {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|x86.ActiveCfg = Debug|Any CPU - {93041197-1E1B-4084-B1DD-C8363A588C48}.Debug|x86.Build.0 = Debug|Any CPU {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|x64.ActiveCfg = Release|Any CPU {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|x64.Build.0 = Release|Any CPU - {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|x86.ActiveCfg = Release|Any CPU - {93041197-1E1B-4084-B1DD-C8363A588C48}.Release|x86.Build.0 = Release|Any CPU {047562BB-6D63-4259-8E2E-0A5E834190B1}.Debug|x64.ActiveCfg = Debug|x64 {047562BB-6D63-4259-8E2E-0A5E834190B1}.Debug|x64.Build.0 = Debug|x64 - {047562BB-6D63-4259-8E2E-0A5E834190B1}.Debug|x86.ActiveCfg = Debug|x86 - {047562BB-6D63-4259-8E2E-0A5E834190B1}.Debug|x86.Build.0 = Debug|x86 {047562BB-6D63-4259-8E2E-0A5E834190B1}.Release|x64.ActiveCfg = Release|x64 {047562BB-6D63-4259-8E2E-0A5E834190B1}.Release|x64.Build.0 = Release|x64 - {047562BB-6D63-4259-8E2E-0A5E834190B1}.Release|x86.ActiveCfg = Release|x86 - {047562BB-6D63-4259-8E2E-0A5E834190B1}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 7c8f0f999dd2e8c94313c8601544101e2d981238 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 25 Aug 2023 07:39:34 -0600 Subject: [PATCH 228/437] Also log verbose errors to the main log --- Program/PacketParsing/PacketLogging.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Program/PacketParsing/PacketLogging.cs b/Program/PacketParsing/PacketLogging.cs index a01b0fc..1aea9f3 100644 --- a/Program/PacketParsing/PacketLogging.cs +++ b/Program/PacketParsing/PacketLogging.cs @@ -29,8 +29,9 @@ public static void PrintMessage(string message) public static void PrintVerboseError(string message) { - // Always log errors to debug + // Always log errors to debug/log Debug.WriteLine(message); + Logging.Main_WriteLine(message); if (!BackendSettings.PrintVerboseErrors) return; From 7ccfa744bb1fe4fe26624b69228bb1a10e362f12 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 25 Aug 2023 11:17:15 -0600 Subject: [PATCH 229/437] Redesign and refactor main window --- Program/App.config | 9 + Program/MainWindow/MainWindow.xaml | 55 ++- Program/MainWindow/MainWindow.xaml.cs | 348 +++++++++++------- .../PacketParsing/Backends/WinUsbBackend.cs | 12 + Program/Properties/Settings.Designer.cs | 36 ++ Program/Properties/Settings.settings | 9 + 6 files changed, 314 insertions(+), 155 deletions(-) diff --git a/Program/App.config b/Program/App.config index a636340..ebcfc6a 100644 --- a/Program/App.config +++ b/Program/App.config @@ -19,6 +19,15 @@ False + + False + + + True + + + True + -1 diff --git a/Program/MainWindow/MainWindow.xaml b/Program/MainWindow/MainWindow.xaml index f42b1af..b756249 100644 --- a/Program/MainWindow/MainWindow.xaml +++ b/Program/MainWindow/MainWindow.xaml @@ -5,29 +5,48 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:RB4InstrumentMapper" mc:Ignorable="d" - Title="RB4InstrumentMapper" Height="499" Width="800" Loaded="Window_Loaded" Closed="Window_Closed" ResizeMode="CanMinimize" MinWidth="800" MinHeight="2"> + Title="RB4InstrumentMapper" + Height="540" Width="800" MinWidth="800" MinHeight="2" ResizeMode="CanMinimize" + Loaded="Window_Loaded" Closed="Window_Closed"> + -