├── .config └── dotnet-tools.json ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── ButtonStateHistory.cs ├── ButtonStateValue.cs ├── ButtonType.cs ├── CONTRIBUTING.md ├── CommonTextures.cs ├── Config ├── ButtonMapping.cs ├── ButtonMappingSet.cs ├── ButtonMappingType.cs ├── DisplayConfig.cs ├── DisplayLayoutStyle.cs ├── GamepadConfig.cs ├── GeneralSettings.cs ├── JoystickConfig.cs ├── MisterConfig.cs ├── RetroSpyConfig.cs ├── RetroSpyControllerType.cs ├── RetrySpyButtonMappingSet.cs ├── Usb2SnesConfig.cs ├── Usb2SnesGame.cs ├── Usb2SnesGameList.cs └── ViewerConfig.cs ├── Content ├── Content.mgcb ├── a_button.png ├── b_button.png ├── c_button.png ├── circle_button.png ├── cross_button.png ├── d_button.png ├── down_button.png ├── empty_button.png ├── illegal_input.png ├── l1_button.png ├── l2_button.png ├── left_button.png ├── left_shoulder_button.png ├── lt_button.png ├── mode_button.png ├── r1_button.png ├── r2_button.png ├── right_button.png ├── right_shoulder_button.png ├── rt_button.png ├── select_button.png ├── square_button.png ├── start_button.png ├── triangle_button.png ├── up_button.png ├── x_button.png ├── y_button.png └── z_button.png ├── DPadState.cs ├── EnumExtensions.cs ├── Fonts └── DroidSans.ttf ├── GameState.cs ├── GamepadStyle.cs ├── Hooks └── KeyboardHook.cs ├── Icon.bmp ├── Icon.ico ├── InputHelper.cs ├── InputMode.cs ├── InputVisualizer.cs ├── InputVisualizer.csproj ├── InputVisualizer.sln ├── LICENSE.MonoGame.md ├── LICENSE.Myra.md ├── LICENSE.Renci.SSH.md ├── LICENSE.md ├── MouseButtonType.cs ├── Program.cs ├── README.md ├── RetroSpy ├── ControllerConnectionFailedArgs.cs ├── ISSHControllerReader.cs ├── MiSTerReader.cs ├── Playstation2.cs ├── SSHControllerReader.cs ├── SSHMonitor.cs └── SSHMonitorDisconnectException.cs ├── RetroSpyStateHandlers ├── MisterHandler.cs ├── PlaystationHandler.cs └── RetroSpyControllerHandler.cs ├── SystemGamePadInfo.cs ├── SystemJoyStickInfo.cs ├── UI ├── GameUI.cs ├── InputSourceChangedEventArgs.cs └── Usb2SnesGameChangedEventArgs.cs ├── Usb2Snes ├── Usb2SnesButtonFlags.cs ├── Usb2SnesClient.cs ├── Usb2SnesRequest.cs ├── Usb2SnesResponse.cs └── Usb2SnesState.cs ├── VisualizationEngines ├── PressedVector.cs ├── RectangeOrientation.cs ├── RectangleContainer.cs ├── RectangleContainerState.cs ├── RectangleEngine.cs └── VisualizerEngine.cs ├── app.manifest ├── retrospy ├── ControllerStateBuilder.cs ├── ControllerStateEventArgs.cs ├── IControllerReader.cs ├── Sega.cs ├── SerialControllerReader.cs ├── SerialMonitor.cs ├── SignalTool.cs └── SuperNESandNES.cs └── usb2snesGameList.json /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-mgcb": { 6 | "version": "3.8.1.303", 7 | "commands": [ 8 | "mgcb" 9 | ] 10 | }, 11 | "dotnet-mgcb-editor": { 12 | "version": "3.8.1.303", 13 | "commands": [ 14 | "mgcb-editor" 15 | ] 16 | }, 17 | "dotnet-mgcb-editor-linux": { 18 | "version": "3.8.1.303", 19 | "commands": [ 20 | "mgcb-editor-linux" 21 | ] 22 | }, 23 | "dotnet-mgcb-editor-windows": { 24 | "version": "3.8.1.303", 25 | "commands": [ 26 | "mgcb-editor-windows" 27 | ] 28 | }, 29 | "dotnet-mgcb-editor-mac": { 30 | "version": "3.8.1.303", 31 | "commands": [ 32 | "mgcb-editor-mac" 33 | ] 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: kungfusedmike 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /ButtonStateHistory.cs: -------------------------------------------------------------------------------- 1 | using InputVisualizer.Config; 2 | using Microsoft.Xna.Framework.Input; 3 | using System; 4 | using System.Collections.Generic; 5 | using Color = Microsoft.Xna.Framework.Color; 6 | 7 | namespace InputVisualizer 8 | { 9 | public class ButtonStateHistory 10 | { 11 | private List StateChangeHistory { get; set; } = new List(100); 12 | public Color Color { get; set; } 13 | private readonly object _modifyLock = new(); 14 | public ButtonMappingType MappingType { get; set; } 15 | public ButtonType UnmappedButtonType { get; set; } 16 | public int JoystickHatIndex { get; set; } 17 | public int JoystickAxisIndex { get; set; } 18 | public bool JoystickAxisDirectionIsNegative { get; set; } 19 | public Keys MappedKey { get; set; } 20 | public MouseButtonType MappedMouseButton { get; set; } 21 | public int StateChangeCount { get; private set; } 22 | public int MaxFrameDisplay { get; set; } 23 | public DateTime LastActiveCompletedTime { get; set; } = DateTime.MinValue; 24 | public int LastActiveFrameCount { get; set; } = 0; 25 | private bool _isPressed { get; set; } 26 | private ButtonStateValue _lastState { get; set; } 27 | public bool IsViolationStateHistory { get; set; } 28 | 29 | public void AddStateChange(bool state, DateTime time, int currentFrame) 30 | { 31 | lock (_modifyLock) 32 | { 33 | if (_lastState != null) 34 | { 35 | _lastState.EndTime = time; 36 | _lastState.Completed = true; 37 | LastActiveCompletedTime = _lastState.IsPressed ? DateTime.Now : DateTime.MinValue; 38 | LastActiveFrameCount = _lastState.IsPressed ? Math.Abs(_lastState.StartFrame - currentFrame) : 0; 39 | } 40 | var newState = new ButtonStateValue { IsPressed = state, StartTime = time, StartFrame = currentFrame }; 41 | StateChangeHistory.Add(newState); 42 | StateChangeCount++; 43 | _lastState = newState; 44 | _isPressed = state; 45 | } 46 | } 47 | 48 | public void RemoveOldStateChanges(double ms) 49 | { 50 | lock (_modifyLock) 51 | { 52 | if (StateChangeCount < 1) 53 | { 54 | return; 55 | } 56 | var removeItems = new List(); 57 | foreach (var change in StateChangeHistory) 58 | { 59 | if (change.Completed && change.EndTime < DateTime.Now.AddMilliseconds(-ms)) 60 | { 61 | removeItems.Add(change); 62 | } 63 | else if (!change.IsPressed && change.StartTime < DateTime.Now.AddMilliseconds(-ms)) 64 | { 65 | removeItems.Add(change); 66 | } 67 | } 68 | foreach (var item in removeItems) 69 | { 70 | StateChangeHistory.Remove(item); 71 | StateChangeCount--; 72 | } 73 | if (StateChangeCount == 0) 74 | { 75 | _lastState = null; 76 | _isPressed = false; 77 | LastActiveFrameCount = 0; 78 | } 79 | } 80 | } 81 | 82 | public bool IsPressed() 83 | { 84 | lock (_modifyLock) 85 | { 86 | return _isPressed; 87 | } 88 | } 89 | 90 | public TimeSpan PressedElapsed() 91 | { 92 | lock (_modifyLock) 93 | { 94 | if (StateChangeCount < 1 || !_isPressed || _lastState == null) 95 | { 96 | return TimeSpan.Zero; 97 | } 98 | return DateTime.Now - _lastState.StartTime; 99 | } 100 | } 101 | 102 | public int GetPressedLastSecond(DateTime timeStamp) 103 | { 104 | lock (_modifyLock) 105 | { 106 | var frequency = 0; 107 | var oneSecondAgo = timeStamp.AddSeconds(-1); 108 | 109 | var numChanges = StateChangeHistory.Count; 110 | for (var i = numChanges - 1; i >= 0; i--) 111 | { 112 | var sc = StateChangeHistory[i]; 113 | if (sc.StartTime < oneSecondAgo) 114 | { 115 | break; 116 | } 117 | if (sc.IsPressed) 118 | { 119 | frequency++; 120 | } 121 | } 122 | return frequency; 123 | } 124 | } 125 | 126 | public ButtonStateValue[] GetCurrentStateHistory() 127 | { 128 | lock (_modifyLock) 129 | { 130 | return StateChangeHistory.ToArray(); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /ButtonStateValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace InputVisualizer 4 | { 5 | public class ButtonStateValue 6 | { 7 | public bool IsPressed { get; set; } 8 | public DateTime StartTime { get; set; } 9 | public DateTime EndTime { get; set; } 10 | public bool Completed { get; set; } 11 | public int StartFrame { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ButtonType.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer 3 | { 4 | public enum ButtonType 5 | { 6 | NONE, 7 | UP, 8 | DOWN, 9 | LEFT, 10 | RIGHT, 11 | A, 12 | B, 13 | X, 14 | Y, 15 | SELECT, 16 | START, 17 | L, 18 | R, 19 | C, 20 | Z, 21 | MODE, 22 | D, 23 | LT, 24 | RT, 25 | CROSS, 26 | CIRCLE, 27 | SQUARE, 28 | TRIANGLE, 29 | L1, 30 | L2, 31 | R1, 32 | R2, 33 | B0, 34 | B1, 35 | B2, 36 | B3, 37 | B4, 38 | B5, 39 | B6, 40 | B7, 41 | B8, 42 | B9, 43 | B10, 44 | B11, 45 | B12, 46 | B13, 47 | B14, 48 | B15, 49 | B16, 50 | B17, 51 | B18, 52 | B19, 53 | B20, 54 | B21, 55 | B22, 56 | B23, 57 | B24 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to InputVisualizer 2 | 3 | Thank you for your interest in contributing to my project, but I'm not accepting any pull requests at this time. 4 | 5 | I don't have time to properly manage reviewing contributions, and would like to maintain and grow this project at my own pace. 6 | 7 | Please feel free to submit issues and bug reports if you come across something that should be added or needs fixing. 8 | 9 | This is a fun personal project for me, so I initially added features for what I do most often. 10 | I will do my best to listen to feedback and continue to add new stuff, but it might take time. 11 | I appreciate your patience and support! 12 | -------------------------------------------------------------------------------- /CommonTextures.cs: -------------------------------------------------------------------------------- 1 | using FontStashSharp; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Content; 4 | using Microsoft.Xna.Framework.Graphics; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | 8 | namespace InputVisualizer 9 | { 10 | public class CommonTextures 11 | { 12 | public Texture2D Pixel { get; set; } 13 | public Texture2D IllegalInput { get; set; } 14 | 15 | public Dictionary ButtonImages = new Dictionary(); 16 | public SpriteFontBase Font18; 17 | public FontSystem FontSystem; 18 | 19 | public void Init( GraphicsDevice graphicsDevice, ContentManager content ) 20 | { 21 | Pixel = new Texture2D(graphicsDevice, 1, 1, false, SurfaceFormat.Color); 22 | Pixel.SetData(new Color[] { Color.White }); 23 | 24 | ButtonImages.Add(ButtonType.UP.ToString(), content.Load("up_button")); 25 | ButtonImages.Add(ButtonType.DOWN.ToString(), content.Load("down_button")); 26 | ButtonImages.Add(ButtonType.LEFT.ToString(), content.Load("left_button")); 27 | ButtonImages.Add(ButtonType.RIGHT.ToString(), content.Load("right_button")); 28 | ButtonImages.Add(ButtonType.A.ToString(), content.Load("a_button")); 29 | ButtonImages.Add(ButtonType.B.ToString(), content.Load("b_button")); 30 | ButtonImages.Add(ButtonType.C.ToString(), content.Load("c_button")); 31 | ButtonImages.Add(ButtonType.D.ToString(), content.Load("d_button")); 32 | ButtonImages.Add(ButtonType.X.ToString(), content.Load("x_button")); 33 | ButtonImages.Add(ButtonType.Y.ToString(), content.Load("y_button")); 34 | ButtonImages.Add(ButtonType.Z.ToString(), content.Load("z_button")); 35 | ButtonImages.Add(ButtonType.SELECT.ToString(), content.Load("select_button")); 36 | ButtonImages.Add(ButtonType.START.ToString(), content.Load("start_button")); 37 | ButtonImages.Add(ButtonType.L.ToString(), content.Load("left_shoulder_button")); 38 | ButtonImages.Add(ButtonType.R.ToString(), content.Load("right_shoulder_button")); 39 | ButtonImages.Add(ButtonType.LT.ToString(), content.Load("lt_button")); 40 | ButtonImages.Add(ButtonType.RT.ToString(), content.Load("rt_button")); 41 | ButtonImages.Add(ButtonType.MODE.ToString(), content.Load("mode_button")); 42 | ButtonImages.Add(ButtonType.CROSS.ToString(), content.Load("cross_button")); 43 | ButtonImages.Add(ButtonType.CIRCLE.ToString(), content.Load("circle_button")); 44 | ButtonImages.Add(ButtonType.TRIANGLE.ToString(), content.Load("triangle_button")); 45 | ButtonImages.Add(ButtonType.SQUARE.ToString(), content.Load("square_button")); 46 | ButtonImages.Add(ButtonType.L1.ToString(), content.Load("l1_button")); 47 | ButtonImages.Add(ButtonType.L2.ToString(), content.Load("l2_button")); 48 | ButtonImages.Add(ButtonType.R1.ToString(), content.Load("r1_button")); 49 | ButtonImages.Add(ButtonType.R2.ToString(), content.Load("r2_button")); 50 | ButtonImages.Add(ButtonType.NONE.ToString(), content.Load("empty_button")); 51 | 52 | IllegalInput = content.Load("illegal_input"); 53 | 54 | FontSystem = new FontSystem(); 55 | FontSystem.AddFont(File.ReadAllBytes(@"Fonts\DroidSans.ttf")); 56 | Font18 = FontSystem.GetFont(18); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Config/ButtonMapping.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Input; 3 | 4 | namespace InputVisualizer.Config 5 | { 6 | public class ButtonMapping 7 | { 8 | public ButtonType ButtonType { get; set; } 9 | public ButtonType MappedButtonType { get; set; } 10 | public Keys MappedKey { get; set; } 11 | public MouseButtonType MappedMouseButton { get; set; } 12 | public ButtonMappingType MappingType { get; set; } = ButtonMappingType.Button; 13 | public Color Color { get; set; } 14 | public bool IsVisible { get; set; } = true; 15 | public int Order { get; set; } 16 | public int JoystickHatIndex { get; set; } = -1; 17 | public int JoystickAxisIndex { get; set; } = -1; 18 | public bool JoystickAxisDirectionIsNegative { get; set; } 19 | public int MaxFrameDisplay { get; set; } = 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Config/ButtonMappingSet.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Input; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace InputVisualizer.Config 7 | { 8 | public class ButtonMappingSet 9 | { 10 | public List ButtonMappings { get; set; } = new List(); 11 | 12 | public void AddButton(ButtonType buttonType, ButtonType mappedButtonType, Color color) 13 | { 14 | ButtonMappings.Add(new ButtonMapping 15 | { 16 | ButtonType = buttonType, 17 | MappedButtonType = mappedButtonType, 18 | MappingType = ButtonMappingType.Button, 19 | MappedKey = Keys.None, 20 | MappedMouseButton = MouseButtonType.None, 21 | Color = color 22 | }); 23 | } 24 | 25 | public void MapToKey(ButtonType buttonType, Keys mappedKey) 26 | { 27 | var mapping = ButtonMappings.FirstOrDefault(m => m.ButtonType == buttonType); 28 | if (mapping == null) 29 | { 30 | return; 31 | } 32 | mapping.MappingType = ButtonMappingType.Key; 33 | mapping.MappedKey = mappedKey; 34 | mapping.MappedButtonType = ButtonType.NONE; 35 | } 36 | 37 | public void MapToMouse(ButtonType buttonType, MouseButtonType mappedMouseButton) 38 | { 39 | var mapping = ButtonMappings.FirstOrDefault(m => m.ButtonType == buttonType); 40 | if (mapping == null) 41 | { 42 | return; 43 | } 44 | mapping.MappingType = ButtonMappingType.Mouse; 45 | mapping.MappedMouseButton = mappedMouseButton; 46 | mapping.MappedKey = Keys.None; 47 | mapping.MappedButtonType = ButtonType.NONE; 48 | } 49 | 50 | public void InitOrder() 51 | { 52 | for (var i = 0; i < ButtonMappings.Count; i++) 53 | { 54 | ButtonMappings[i].Order = i; 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Config/ButtonMappingType.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer.Config 3 | { 4 | public enum ButtonMappingType 5 | { 6 | Button = 0, 7 | Key = 1, 8 | Mouse = 2 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Config/DisplayConfig.cs: -------------------------------------------------------------------------------- 1 |  2 | using Microsoft.Xna.Framework; 3 | 4 | namespace InputVisualizer.Config 5 | { 6 | public class DisplayConfig 7 | { 8 | public Color BackgroundColor { get; set; } = new Color(32, 32, 32, 0); 9 | public bool DisplayDuration { get; set; } = true; 10 | public int MinDisplayDuration { get; set; } = 2; 11 | public bool DisplayFrequency { get; set; } = true; 12 | public int MinDisplayFrequency { get; set; } = 5; 13 | public DisplayLayoutStyle Layout { get; set; } = DisplayLayoutStyle.Horizontal; 14 | public bool DrawIdleLines { get; set; } = true; 15 | public float Speed { get; set; } = 4; 16 | public int LineLength { get; set; } = 150; 17 | public float TurnOffLineSpeed { get; set; } = 200; 18 | public int MaxContainers { get; set; } = 0; 19 | public Color EmptyContainerColor { get; set; } = Color.PapayaWhip; 20 | public Color IllegalInputColor { get; set; } = Color.Red; 21 | public bool DisplayIllegalInputs { get; set; } = false; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Config/DisplayLayoutStyle.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.ComponentModel; 3 | 4 | namespace InputVisualizer.Config 5 | { 6 | public enum DisplayLayoutStyle 7 | { 8 | Horizontal = 0, 9 | [Description("Vertical Down")] 10 | VerticalDown = 1, 11 | [Description("Vertical Up")] 12 | VerticalUp = 2 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Config/GamepadConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using Microsoft.Xna.Framework.Input; 3 | using System.Linq; 4 | 5 | namespace InputVisualizer.Config 6 | { 7 | public class GamepadConfig 8 | { 9 | public string Id { get; set; } 10 | public GamepadStyle Style { get; set; } 11 | public bool UseLStickForDpad { get; set; } = false; 12 | public ButtonMappingSet ButtonMappingSet { get; set; } = new ButtonMappingSet(); 13 | public bool IsKeyboard => string.Equals(Id, "keyboard", System.StringComparison.InvariantCultureIgnoreCase); 14 | 15 | public void GenerateButtonMappings() 16 | { 17 | ButtonMappingSet = new ButtonMappingSet(); 18 | ButtonMappingSet.AddButton(ButtonType.UP, ButtonType.UP, Color.WhiteSmoke); 19 | ButtonMappingSet.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.WhiteSmoke); 20 | ButtonMappingSet.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.WhiteSmoke); 21 | ButtonMappingSet.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.WhiteSmoke); 22 | 23 | switch (Style) 24 | { 25 | case GamepadStyle.NES: 26 | { 27 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.A, Color.DeepSkyBlue); 28 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.B, Color.Gold); 29 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.SELECT, Color.PowderBlue); 30 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 31 | break; 32 | } 33 | case GamepadStyle.SNES: 34 | { 35 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.B, Color.Gold); 36 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.A, Color.DarkRed); 37 | ButtonMappingSet.AddButton(ButtonType.Y, ButtonType.Y, Color.DarkGreen); 38 | ButtonMappingSet.AddButton(ButtonType.X, ButtonType.X, Color.DeepSkyBlue); 39 | ButtonMappingSet.AddButton(ButtonType.L, ButtonType.L, Color.Silver); 40 | ButtonMappingSet.AddButton(ButtonType.R, ButtonType.R, Color.Silver); 41 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.SELECT, Color.PowderBlue); 42 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 43 | break; 44 | } 45 | case GamepadStyle.Genesis: 46 | { 47 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.A, Color.Silver); 48 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.B, Color.Silver); 49 | ButtonMappingSet.AddButton(ButtonType.C, ButtonType.L, Color.Silver); 50 | ButtonMappingSet.AddButton(ButtonType.X, ButtonType.X, Color.DarkSlateGray); 51 | ButtonMappingSet.AddButton(ButtonType.Y, ButtonType.Y, Color.DarkSlateGray); 52 | ButtonMappingSet.AddButton(ButtonType.Z, ButtonType.R, Color.DarkSlateGray); 53 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 54 | ButtonMappingSet.AddButton(ButtonType.MODE, ButtonType.SELECT, Color.PowderBlue); 55 | break; 56 | } 57 | case GamepadStyle.XBOX: 58 | { 59 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.A, Color.DarkGreen); 60 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.B, Color.DarkRed); 61 | ButtonMappingSet.AddButton(ButtonType.X, ButtonType.X, Color.DeepSkyBlue); 62 | ButtonMappingSet.AddButton(ButtonType.Y, ButtonType.Y, Color.Gold); 63 | ButtonMappingSet.AddButton(ButtonType.L, ButtonType.L, Color.Silver); 64 | ButtonMappingSet.AddButton(ButtonType.R, ButtonType.R, Color.Silver); 65 | ButtonMappingSet.AddButton(ButtonType.LT, ButtonType.LT, Color.Silver); 66 | ButtonMappingSet.AddButton(ButtonType.RT, ButtonType.RT, Color.Silver); 67 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.SELECT, Color.PowderBlue); 68 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 69 | break; 70 | } 71 | case GamepadStyle.Playstation: 72 | { 73 | ButtonMappingSet.AddButton(ButtonType.CROSS, ButtonType.CROSS, Color.CornflowerBlue); 74 | ButtonMappingSet.AddButton(ButtonType.CIRCLE, ButtonType.CIRCLE, Color.IndianRed); 75 | ButtonMappingSet.AddButton(ButtonType.SQUARE, ButtonType.SQUARE, Color.Pink); 76 | ButtonMappingSet.AddButton(ButtonType.TRIANGLE, ButtonType.TRIANGLE, Color.Teal); 77 | ButtonMappingSet.AddButton(ButtonType.L1, ButtonType.L1, Color.Silver); 78 | ButtonMappingSet.AddButton(ButtonType.R1, ButtonType.R1, Color.Silver); 79 | ButtonMappingSet.AddButton(ButtonType.L2, ButtonType.L2, Color.Silver); 80 | ButtonMappingSet.AddButton(ButtonType.R2, ButtonType.R2, Color.Silver); 81 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.SELECT, Color.PowderBlue); 82 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 83 | break; 84 | } 85 | case GamepadStyle.NeoGeo: 86 | { 87 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.A, Color.DarkRed); 88 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.X, Color.Gold); 89 | ButtonMappingSet.AddButton(ButtonType.C, ButtonType.Y, Color.DarkGreen); 90 | ButtonMappingSet.AddButton(ButtonType.D, ButtonType.R, Color.DeepSkyBlue); 91 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.SELECT, Color.PowderBlue); 92 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 93 | break; 94 | } 95 | } 96 | 97 | if (IsKeyboard) 98 | { 99 | ButtonMappingSet.MapToKey(ButtonType.UP, Keys.Up); 100 | ButtonMappingSet.MapToKey(ButtonType.DOWN, Keys.Down); 101 | ButtonMappingSet.MapToKey(ButtonType.LEFT, Keys.Left); 102 | ButtonMappingSet.MapToKey(ButtonType.RIGHT, Keys.Right); 103 | ButtonMappingSet.MapToKey(ButtonType.A, Keys.A); 104 | ButtonMappingSet.MapToKey(ButtonType.B, Keys.S); 105 | ButtonMappingSet.MapToKey(ButtonType.SELECT, Keys.OemOpenBrackets); 106 | ButtonMappingSet.MapToKey(ButtonType.START, Keys.OemCloseBrackets); 107 | 108 | foreach (var mapping in ButtonMappingSet.ButtonMappings.Where(m => m.MappingType != ButtonMappingType.Key)) 109 | { 110 | mapping.MappingType = ButtonMappingType.Key; 111 | mapping.MappedButtonType = ButtonType.NONE; 112 | } 113 | } 114 | ButtonMappingSet.InitOrder(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Config/GeneralSettings.cs: -------------------------------------------------------------------------------- 1 | using InputVisualizer.Usb2Snes; 2 | 3 | namespace InputVisualizer.Config 4 | { 5 | public class GeneralSettings 6 | { 7 | public string Usb2SnesServer { get; set; } = Usb2SnesClient.DEFAULT_SERVER; 8 | public string Usb2SnesPort { get; set; } = Usb2SnesClient.DEFAULT_PORT; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Config/JoystickConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace InputVisualizer.Config 4 | { 5 | public class JoystickConfig 6 | { 7 | public string Id { get; set; } 8 | public GamepadStyle Style { get; set; } = GamepadStyle.NES; 9 | 10 | public ButtonMappingSet ButtonMappingSet { get; set; } = new ButtonMappingSet(); 11 | 12 | public void GenerateButtonMappings() 13 | { 14 | ButtonMappingSet = new ButtonMappingSet(); 15 | ButtonMappingSet.AddButton(ButtonType.UP, ButtonType.NONE, Color.WhiteSmoke); 16 | ButtonMappingSet.AddButton(ButtonType.DOWN, ButtonType.NONE, Color.WhiteSmoke); 17 | ButtonMappingSet.AddButton(ButtonType.LEFT, ButtonType.NONE, Color.WhiteSmoke); 18 | ButtonMappingSet.AddButton(ButtonType.RIGHT, ButtonType.NONE, Color.WhiteSmoke); 19 | 20 | switch (Style) 21 | { 22 | case GamepadStyle.NES: 23 | { 24 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.NONE, Color.DeepSkyBlue); 25 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.NONE, Color.Gold); 26 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.NONE, Color.PowderBlue); 27 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.NONE, Color.PowderBlue); 28 | break; 29 | } 30 | case GamepadStyle.SNES: 31 | { 32 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.NONE, Color.Gold); 33 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.NONE, Color.DarkRed); 34 | ButtonMappingSet.AddButton(ButtonType.Y, ButtonType.NONE, Color.DarkGreen); 35 | ButtonMappingSet.AddButton(ButtonType.X, ButtonType.NONE, Color.DeepSkyBlue); 36 | ButtonMappingSet.AddButton(ButtonType.L, ButtonType.NONE, Color.Silver); 37 | ButtonMappingSet.AddButton(ButtonType.R, ButtonType.NONE, Color.Silver); 38 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.NONE, Color.PowderBlue); 39 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.NONE, Color.PowderBlue); 40 | break; 41 | } 42 | case GamepadStyle.Genesis: 43 | { 44 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.NONE, Color.Silver); 45 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.NONE, Color.Silver); 46 | ButtonMappingSet.AddButton(ButtonType.C, ButtonType.NONE, Color.Silver); 47 | ButtonMappingSet.AddButton(ButtonType.X, ButtonType.NONE, Color.DarkSlateGray); 48 | ButtonMappingSet.AddButton(ButtonType.Y, ButtonType.NONE, Color.DarkSlateGray); 49 | ButtonMappingSet.AddButton(ButtonType.Z, ButtonType.NONE, Color.DarkSlateGray); 50 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.NONE, Color.PowderBlue); 51 | ButtonMappingSet.AddButton(ButtonType.MODE, ButtonType.NONE, Color.PowderBlue); 52 | break; 53 | } 54 | case GamepadStyle.XBOX: 55 | { 56 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.NONE, Color.DarkGreen); 57 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.NONE, Color.DarkRed); 58 | ButtonMappingSet.AddButton(ButtonType.X, ButtonType.NONE, Color.DeepSkyBlue); 59 | ButtonMappingSet.AddButton(ButtonType.Y, ButtonType.NONE, Color.Gold); 60 | ButtonMappingSet.AddButton(ButtonType.L, ButtonType.NONE, Color.Silver); 61 | ButtonMappingSet.AddButton(ButtonType.R, ButtonType.NONE, Color.Silver); 62 | ButtonMappingSet.AddButton(ButtonType.LT, ButtonType.NONE, Color.Silver); 63 | ButtonMappingSet.AddButton(ButtonType.RT, ButtonType.NONE, Color.Silver); 64 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.NONE, Color.PowderBlue); 65 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.NONE, Color.PowderBlue); 66 | break; 67 | } 68 | case GamepadStyle.Playstation: 69 | { 70 | ButtonMappingSet.AddButton(ButtonType.CROSS, ButtonType.NONE, Color.CornflowerBlue); 71 | ButtonMappingSet.AddButton(ButtonType.CIRCLE, ButtonType.NONE, Color.IndianRed); 72 | ButtonMappingSet.AddButton(ButtonType.SQUARE, ButtonType.NONE, Color.Pink); 73 | ButtonMappingSet.AddButton(ButtonType.TRIANGLE, ButtonType.NONE, Color.Teal); 74 | ButtonMappingSet.AddButton(ButtonType.L1, ButtonType.NONE, Color.Silver); 75 | ButtonMappingSet.AddButton(ButtonType.R1, ButtonType.NONE, Color.Silver); 76 | ButtonMappingSet.AddButton(ButtonType.L2, ButtonType.NONE, Color.Silver); 77 | ButtonMappingSet.AddButton(ButtonType.R2, ButtonType.NONE, Color.Silver); 78 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.NONE, Color.PowderBlue); 79 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.NONE, Color.PowderBlue); 80 | break; 81 | } 82 | case GamepadStyle.NeoGeo: 83 | { 84 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.NONE, Color.DarkRed); 85 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.NONE, Color.Gold); 86 | ButtonMappingSet.AddButton(ButtonType.C, ButtonType.NONE, Color.DarkGreen); 87 | ButtonMappingSet.AddButton(ButtonType.D, ButtonType.NONE, Color.DeepSkyBlue); 88 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.NONE, Color.PowderBlue); 89 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.NONE, Color.PowderBlue); 90 | break; 91 | } 92 | } 93 | ButtonMappingSet.InitOrder(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Config/MisterConfig.cs: -------------------------------------------------------------------------------- 1 |  2 | using Microsoft.Xna.Framework; 3 | using System.Collections.Generic; 4 | 5 | namespace InputVisualizer.Config 6 | { 7 | public class MisterConfig 8 | { 9 | public string Hostname { get; set; } 10 | public string Username { get; set; } = "root"; 11 | public string Password { get; set; } = "1"; 12 | public int Controller { get; set; } = 0; 13 | public GamepadStyle Style { get; set; } = GamepadStyle.NES; 14 | public bool UseLStickForDpad { get; set; } = false; 15 | 16 | public Dictionary ButtonMappingSets = new Dictionary(); 17 | 18 | public ButtonMappingSet GetCurrentMappingSet() 19 | { 20 | return ButtonMappingSets.ContainsKey( Style ) ? ButtonMappingSets[ Style ] : ButtonMappingSets[GamepadStyle.NES]; 21 | } 22 | 23 | public void GenerateButtonMappings() 24 | { 25 | if( !ButtonMappingSets.ContainsKey( GamepadStyle.NES ) ) 26 | { 27 | var nes = new ButtonMappingSet(); 28 | nes.AddButton(ButtonType.UP, ButtonType.UP, Color.WhiteSmoke); 29 | nes.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.WhiteSmoke); 30 | nes.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.WhiteSmoke); 31 | nes.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.WhiteSmoke); 32 | nes.AddButton(ButtonType.B, ButtonType.B0, Color.Gold); 33 | nes.AddButton(ButtonType.A, ButtonType.B1, Color.DeepSkyBlue); 34 | nes.AddButton(ButtonType.SELECT, ButtonType.B8, Color.PowderBlue); 35 | nes.AddButton(ButtonType.START, ButtonType.B9, Color.PowderBlue); 36 | nes.InitOrder(); 37 | ButtonMappingSets.Add(GamepadStyle.NES, nes); 38 | } 39 | 40 | if (!ButtonMappingSets.ContainsKey(GamepadStyle.SNES)) 41 | { 42 | var snes = new ButtonMappingSet(); 43 | snes.AddButton(ButtonType.UP, ButtonType.UP, Color.WhiteSmoke); 44 | snes.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.WhiteSmoke); 45 | snes.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.WhiteSmoke); 46 | snes.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.WhiteSmoke); 47 | snes.AddButton(ButtonType.B, ButtonType.B1, Color.Gold); 48 | snes.AddButton(ButtonType.A, ButtonType.B2, Color.DarkRed); 49 | snes.AddButton(ButtonType.Y, ButtonType.B0, Color.DarkGreen); 50 | snes.AddButton(ButtonType.X, ButtonType.B3, Color.DeepSkyBlue); 51 | snes.AddButton(ButtonType.L, ButtonType.B4, Color.Silver); 52 | snes.AddButton(ButtonType.R, ButtonType.B5, Color.Silver); 53 | snes.AddButton(ButtonType.SELECT, ButtonType.B8, Color.PowderBlue); 54 | snes.AddButton(ButtonType.START, ButtonType.B9, Color.PowderBlue); 55 | snes.InitOrder(); 56 | ButtonMappingSets.Add(GamepadStyle.SNES, snes); 57 | } 58 | 59 | if (!ButtonMappingSets.ContainsKey(GamepadStyle.Genesis)) 60 | { 61 | var genesis = new ButtonMappingSet(); 62 | genesis.AddButton(ButtonType.UP, ButtonType.UP, Color.WhiteSmoke); 63 | genesis.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.WhiteSmoke); 64 | genesis.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.WhiteSmoke); 65 | genesis.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.WhiteSmoke); 66 | genesis.AddButton(ButtonType.A, ButtonType.B1, Color.Silver); 67 | genesis.AddButton(ButtonType.B, ButtonType.B2, Color.Silver); 68 | genesis.AddButton(ButtonType.C, ButtonType.B4, Color.Silver); 69 | genesis.AddButton(ButtonType.X, ButtonType.B0, Color.DarkSlateGray); 70 | genesis.AddButton(ButtonType.Y, ButtonType.B3, Color.DarkSlateGray); 71 | genesis.AddButton(ButtonType.Z, ButtonType.B5, Color.DarkSlateGray); 72 | genesis.AddButton(ButtonType.START, ButtonType.B9, Color.PowderBlue); 73 | genesis.AddButton(ButtonType.MODE, ButtonType.NONE, Color.PowderBlue); 74 | genesis.InitOrder(); 75 | ButtonMappingSets.Add(GamepadStyle.Genesis, genesis); 76 | } 77 | 78 | if (!ButtonMappingSets.ContainsKey(GamepadStyle.NeoGeo)) 79 | { 80 | var neogeo = new ButtonMappingSet(); 81 | neogeo.AddButton(ButtonType.UP, ButtonType.UP, Color.WhiteSmoke); 82 | neogeo.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.WhiteSmoke); 83 | neogeo.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.WhiteSmoke); 84 | neogeo.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.WhiteSmoke); 85 | neogeo.AddButton(ButtonType.A, ButtonType.B1, Color.DarkRed); 86 | neogeo.AddButton(ButtonType.B, ButtonType.B0, Color.Gold); 87 | neogeo.AddButton(ButtonType.C, ButtonType.B3, Color.DarkGreen); 88 | neogeo.AddButton(ButtonType.D, ButtonType.B5, Color.DeepSkyBlue); 89 | neogeo.AddButton(ButtonType.SELECT, ButtonType.B8, Color.PowderBlue); 90 | neogeo.AddButton(ButtonType.START, ButtonType.B9, Color.PowderBlue); 91 | neogeo.InitOrder(); 92 | ButtonMappingSets.Add(GamepadStyle.NeoGeo, neogeo); 93 | } 94 | 95 | if (!ButtonMappingSets.ContainsKey(GamepadStyle.Playstation)) 96 | { 97 | var playstation = new ButtonMappingSet(); 98 | playstation.AddButton(ButtonType.UP, ButtonType.UP, Color.WhiteSmoke); 99 | playstation.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.WhiteSmoke); 100 | playstation.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.WhiteSmoke); 101 | playstation.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.WhiteSmoke); 102 | playstation.AddButton(ButtonType.CROSS, ButtonType.B1, Color.CornflowerBlue); 103 | playstation.AddButton(ButtonType.CIRCLE, ButtonType.B2, Color.IndianRed); 104 | playstation.AddButton(ButtonType.SQUARE, ButtonType.B0, Color.Pink); 105 | playstation.AddButton(ButtonType.TRIANGLE, ButtonType.B3, Color.Teal); 106 | playstation.AddButton(ButtonType.L1, ButtonType.B4, Color.Silver); 107 | playstation.AddButton(ButtonType.R1, ButtonType.B5, Color.Silver); 108 | playstation.AddButton(ButtonType.L2, ButtonType.B6, Color.Silver); 109 | playstation.AddButton(ButtonType.R2, ButtonType.B7, Color.Silver); 110 | playstation.AddButton(ButtonType.SELECT, ButtonType.B8, Color.PowderBlue); 111 | playstation.AddButton(ButtonType.START, ButtonType.B9, Color.PowderBlue); 112 | playstation.InitOrder(); 113 | ButtonMappingSets.Add(GamepadStyle.Playstation, playstation); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Config/RetroSpyConfig.cs: -------------------------------------------------------------------------------- 1 |  2 | using Microsoft.Xna.Framework; 3 | using System.Linq; 4 | 5 | namespace InputVisualizer.Config 6 | { 7 | public class RetroSpyConfig 8 | { 9 | public RetroSpyControllerType ControllerType { get; set; } 10 | public string ComPortName { get; set; } 11 | public RetrySpyButtonMappingSet NES { get; set; } = new RetrySpyButtonMappingSet(); 12 | public RetrySpyButtonMappingSet SNES { get; set; } = new RetrySpyButtonMappingSet(); 13 | public RetrySpyButtonMappingSet Genesis { get; set; } = new RetrySpyButtonMappingSet(); 14 | public RetrySpyButtonMappingSet Playstation { get; set; } = new RetrySpyButtonMappingSet(); 15 | 16 | public RetrySpyButtonMappingSet GetMappingSet(RetroSpyControllerType controllerType) 17 | { 18 | switch (controllerType) 19 | { 20 | case RetroSpyControllerType.NES: 21 | return NES; 22 | case RetroSpyControllerType.SNES: 23 | return SNES; 24 | case RetroSpyControllerType.Genesis: 25 | return Genesis; 26 | case RetroSpyControllerType.Playstation: 27 | return Playstation; 28 | default: 29 | return null; 30 | } 31 | } 32 | 33 | public void GenerateButtonMappings() 34 | { 35 | if (!NES.ButtonMappings.Any()) 36 | { 37 | NES = new RetrySpyButtonMappingSet() { ControllerType = RetroSpyControllerType.NES }; 38 | 39 | NES.AddButton(ButtonType.UP, ButtonType.UP, Color.WhiteSmoke); 40 | NES.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.WhiteSmoke); 41 | NES.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.WhiteSmoke); 42 | NES.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.WhiteSmoke); 43 | NES.AddButton(ButtonType.B, ButtonType.B, Color.Gold); 44 | NES.AddButton(ButtonType.A, ButtonType.A, Color.DeepSkyBlue); 45 | NES.AddButton(ButtonType.SELECT, ButtonType.SELECT, Color.PowderBlue); 46 | NES.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 47 | NES.InitOrder(); 48 | } 49 | if (!SNES.ButtonMappings.Any()) 50 | { 51 | SNES = new RetrySpyButtonMappingSet() { ControllerType = RetroSpyControllerType.SNES }; 52 | 53 | SNES.AddButton(ButtonType.UP, ButtonType.UP, Color.WhiteSmoke); 54 | SNES.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.WhiteSmoke); 55 | SNES.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.WhiteSmoke); 56 | SNES.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.WhiteSmoke); 57 | SNES.AddButton(ButtonType.B, ButtonType.B, Color.Gold); 58 | SNES.AddButton(ButtonType.A, ButtonType.A, Color.DarkRed); 59 | SNES.AddButton(ButtonType.Y, ButtonType.Y, Color.DarkGreen); 60 | SNES.AddButton(ButtonType.X, ButtonType.X, Color.DeepSkyBlue); 61 | SNES.AddButton(ButtonType.L, ButtonType.L, Color.Silver); 62 | SNES.AddButton(ButtonType.R, ButtonType.R, Color.Silver); 63 | SNES.AddButton(ButtonType.SELECT, ButtonType.SELECT, Color.PowderBlue); 64 | SNES.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 65 | SNES.InitOrder(); 66 | } 67 | if (!Genesis.ButtonMappings.Any()) 68 | { 69 | Genesis = new RetrySpyButtonMappingSet() { ControllerType = RetroSpyControllerType.Genesis }; 70 | 71 | Genesis.AddButton(ButtonType.UP, ButtonType.UP, Color.WhiteSmoke); 72 | Genesis.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.WhiteSmoke); 73 | Genesis.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.WhiteSmoke); 74 | Genesis.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.WhiteSmoke); 75 | Genesis.AddButton(ButtonType.A, ButtonType.A, Color.Silver); 76 | Genesis.AddButton(ButtonType.B, ButtonType.B, Color.Silver); 77 | Genesis.AddButton(ButtonType.C, ButtonType.C, Color.Silver); 78 | Genesis.AddButton(ButtonType.X, ButtonType.X, Color.DarkSlateGray); 79 | Genesis.AddButton(ButtonType.Y, ButtonType.Y, Color.DarkSlateGray); 80 | Genesis.AddButton(ButtonType.Z, ButtonType.Z, Color.DarkSlateGray); 81 | Genesis.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 82 | Genesis.AddButton(ButtonType.MODE, ButtonType.MODE, Color.PowderBlue); 83 | Genesis.InitOrder(); 84 | } 85 | if( !Playstation.ButtonMappings.Any()) 86 | { 87 | Playstation = new RetrySpyButtonMappingSet() { ControllerType = RetroSpyControllerType.Playstation }; 88 | Playstation.AddButton(ButtonType.UP, ButtonType.UP, Color.WhiteSmoke); 89 | Playstation.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.WhiteSmoke); 90 | Playstation.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.WhiteSmoke); 91 | Playstation.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.WhiteSmoke); 92 | Playstation.AddButton(ButtonType.CROSS, ButtonType.CROSS, Color.CornflowerBlue); 93 | Playstation.AddButton(ButtonType.CIRCLE, ButtonType.CIRCLE, Color.IndianRed); 94 | Playstation.AddButton(ButtonType.SQUARE, ButtonType.SQUARE, Color.Pink); 95 | Playstation.AddButton(ButtonType.TRIANGLE, ButtonType.TRIANGLE, Color.Teal); 96 | Playstation.AddButton(ButtonType.L1, ButtonType.L1, Color.Silver); 97 | Playstation.AddButton(ButtonType.R1, ButtonType.R1, Color.Silver); 98 | Playstation.AddButton(ButtonType.L2, ButtonType.L2, Color.Silver); 99 | Playstation.AddButton(ButtonType.R2, ButtonType.R2, Color.Silver); 100 | Playstation.AddButton(ButtonType.SELECT, ButtonType.SELECT, Color.PowderBlue); 101 | Playstation.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 102 | Playstation.InitOrder(); 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Config/RetroSpyControllerType.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.ComponentModel; 3 | 4 | namespace InputVisualizer.Config 5 | { 6 | public enum RetroSpyControllerType 7 | { 8 | NES, 9 | SNES, 10 | [Description("Genesis")] 11 | Genesis, 12 | [Description("Playstation 1/2")] 13 | Playstation 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Config/RetrySpyButtonMappingSet.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer.Config 3 | { 4 | public class RetrySpyButtonMappingSet : ButtonMappingSet 5 | { 6 | public RetroSpyControllerType ControllerType; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Config/Usb2SnesConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using System.Linq; 3 | 4 | namespace InputVisualizer.Config 5 | { 6 | public class Usb2SnesConfig 7 | { 8 | public ButtonMappingSet ButtonMappingSet { get; set; } = new ButtonMappingSet(); 9 | 10 | public void GenerateButtonMappings() 11 | { 12 | if (!ButtonMappingSet.ButtonMappings.Any()) 13 | { 14 | ButtonMappingSet.AddButton(ButtonType.UP, ButtonType.UP, Color.LightGreen); 15 | ButtonMappingSet.AddButton(ButtonType.DOWN, ButtonType.DOWN, Color.LightGreen); 16 | ButtonMappingSet.AddButton(ButtonType.LEFT, ButtonType.LEFT, Color.LightGreen); 17 | ButtonMappingSet.AddButton(ButtonType.RIGHT, ButtonType.RIGHT, Color.LightGreen); 18 | ButtonMappingSet.AddButton(ButtonType.B, ButtonType.B, Color.Gold); 19 | ButtonMappingSet.AddButton(ButtonType.A, ButtonType.A, Color.DarkRed); 20 | ButtonMappingSet.AddButton(ButtonType.Y, ButtonType.Y, Color.DarkGreen); 21 | ButtonMappingSet.AddButton(ButtonType.X, ButtonType.X, Color.DeepSkyBlue); 22 | ButtonMappingSet.AddButton(ButtonType.L, ButtonType.L, Color.Silver); 23 | ButtonMappingSet.AddButton(ButtonType.R, ButtonType.R, Color.Silver); 24 | ButtonMappingSet.AddButton(ButtonType.SELECT, ButtonType.SELECT, Color.PowderBlue); 25 | ButtonMappingSet.AddButton(ButtonType.START, ButtonType.START, Color.PowderBlue); 26 | ButtonMappingSet.InitOrder(); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Config/Usb2SnesGame.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer.Config 3 | { 4 | public class Usb2SnesGame 5 | { 6 | public string Name { get; set; } 7 | public string[] Address { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Config/Usb2SnesGameList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace InputVisualizer.Config 4 | { 5 | public class Usb2SnesGameList 6 | { 7 | public List Games = new List(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Config/ViewerConfig.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | 6 | namespace InputVisualizer.Config 7 | { 8 | public class ViewerConfig 9 | { 10 | public static string CONFIG_PATH = @"config.json"; 11 | 12 | public GeneralSettings GeneralSettings { get; set; } = new GeneralSettings(); 13 | public RetroSpyConfig RetroSpyConfig { get; set; } = new RetroSpyConfig(); 14 | public MisterConfig MisterConfig { get; set; } = new MisterConfig(); 15 | public Usb2SnesConfig Usb2SnesConfig { get; set; } = new Usb2SnesConfig(); 16 | public DisplayConfig DisplayConfig { get; set; } = new DisplayConfig(); 17 | public string CurrentInputSource { get; set; } 18 | public List GamepadConfigs { get; set; } = new List(); 19 | public List JoystickConfigs { get; set; } = new List(); 20 | 21 | public GamepadConfig CreateGamepadConfig(string id, GamepadStyle gamepadStyle) 22 | { 23 | var config = new GamepadConfig(); 24 | 25 | config.Id = id; 26 | config.Style = gamepadStyle; 27 | config.GenerateButtonMappings(); 28 | GamepadConfigs.Add(config); 29 | return config; 30 | } 31 | 32 | public JoystickConfig CreateJoystickConfig(string id, GamepadStyle gamepadStyle) 33 | { 34 | var config = new JoystickConfig(); 35 | 36 | config.Id = id; 37 | config.Style = gamepadStyle; 38 | config.GenerateButtonMappings(); 39 | JoystickConfigs.Add(config); 40 | return config; 41 | } 42 | 43 | public void Save() 44 | { 45 | string path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), CONFIG_PATH); 46 | File.WriteAllText(path, JsonConvert.SerializeObject(this, Formatting.Indented)); 47 | } 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Content/Content.mgcb: -------------------------------------------------------------------------------- 1 | 2 | #----------------------------- Global Properties ----------------------------# 3 | 4 | /outputDir:bin/$(Platform) 5 | /intermediateDir:obj/$(Platform) 6 | /platform:DesktopGL 7 | /config: 8 | /profile:Reach 9 | /compress:False 10 | 11 | #-------------------------------- References --------------------------------# 12 | 13 | 14 | #---------------------------------- Content ---------------------------------# 15 | 16 | #begin a_button.png 17 | /importer:TextureImporter 18 | /processor:TextureProcessor 19 | /processorParam:ColorKeyColor=255,0,255,255 20 | /processorParam:ColorKeyEnabled=True 21 | /processorParam:GenerateMipmaps=False 22 | /processorParam:PremultiplyAlpha=True 23 | /processorParam:ResizeToPowerOfTwo=False 24 | /processorParam:MakeSquare=False 25 | /processorParam:TextureFormat=Color 26 | /build:a_button.png;a_button 27 | 28 | #begin b_button.png 29 | /importer:TextureImporter 30 | /processor:TextureProcessor 31 | /processorParam:ColorKeyColor=255,0,255,255 32 | /processorParam:ColorKeyEnabled=True 33 | /processorParam:GenerateMipmaps=False 34 | /processorParam:PremultiplyAlpha=True 35 | /processorParam:ResizeToPowerOfTwo=False 36 | /processorParam:MakeSquare=False 37 | /processorParam:TextureFormat=Color 38 | /build:b_button.png;b_button 39 | 40 | #begin c_button.png 41 | /importer:TextureImporter 42 | /processor:TextureProcessor 43 | /processorParam:ColorKeyColor=255,0,255,255 44 | /processorParam:ColorKeyEnabled=True 45 | /processorParam:GenerateMipmaps=False 46 | /processorParam:PremultiplyAlpha=True 47 | /processorParam:ResizeToPowerOfTwo=False 48 | /processorParam:MakeSquare=False 49 | /processorParam:TextureFormat=Color 50 | /build:c_button.png;c_button 51 | 52 | #begin circle_button.png 53 | /importer:TextureImporter 54 | /processor:TextureProcessor 55 | /processorParam:ColorKeyColor=255,0,255,255 56 | /processorParam:ColorKeyEnabled=True 57 | /processorParam:GenerateMipmaps=False 58 | /processorParam:PremultiplyAlpha=True 59 | /processorParam:ResizeToPowerOfTwo=False 60 | /processorParam:MakeSquare=False 61 | /processorParam:TextureFormat=Color 62 | /build:circle_button.png;circle_button 63 | 64 | #begin cross_button.png 65 | /importer:TextureImporter 66 | /processor:TextureProcessor 67 | /processorParam:ColorKeyColor=255,0,255,255 68 | /processorParam:ColorKeyEnabled=True 69 | /processorParam:GenerateMipmaps=False 70 | /processorParam:PremultiplyAlpha=True 71 | /processorParam:ResizeToPowerOfTwo=False 72 | /processorParam:MakeSquare=False 73 | /processorParam:TextureFormat=Color 74 | /build:cross_button.png;cross_button 75 | 76 | #begin d_button.png 77 | /importer:TextureImporter 78 | /processor:TextureProcessor 79 | /processorParam:ColorKeyColor=255,0,255,255 80 | /processorParam:ColorKeyEnabled=True 81 | /processorParam:GenerateMipmaps=False 82 | /processorParam:PremultiplyAlpha=True 83 | /processorParam:ResizeToPowerOfTwo=False 84 | /processorParam:MakeSquare=False 85 | /processorParam:TextureFormat=Color 86 | /build:d_button.png;d_button 87 | 88 | #begin down_button.png 89 | /importer:TextureImporter 90 | /processor:TextureProcessor 91 | /processorParam:ColorKeyColor=255,0,255,255 92 | /processorParam:ColorKeyEnabled=True 93 | /processorParam:GenerateMipmaps=False 94 | /processorParam:PremultiplyAlpha=True 95 | /processorParam:ResizeToPowerOfTwo=False 96 | /processorParam:MakeSquare=False 97 | /processorParam:TextureFormat=Color 98 | /build:down_button.png;down_button 99 | 100 | #begin empty_button.png 101 | /importer:TextureImporter 102 | /processor:TextureProcessor 103 | /processorParam:ColorKeyColor=255,0,255,255 104 | /processorParam:ColorKeyEnabled=True 105 | /processorParam:GenerateMipmaps=False 106 | /processorParam:PremultiplyAlpha=True 107 | /processorParam:ResizeToPowerOfTwo=False 108 | /processorParam:MakeSquare=False 109 | /processorParam:TextureFormat=Color 110 | /build:empty_button.png;empty_button 111 | 112 | #begin illegal_input.png 113 | /importer:TextureImporter 114 | /processor:TextureProcessor 115 | /processorParam:ColorKeyColor=255,0,255,255 116 | /processorParam:ColorKeyEnabled=True 117 | /processorParam:GenerateMipmaps=False 118 | /processorParam:PremultiplyAlpha=True 119 | /processorParam:ResizeToPowerOfTwo=False 120 | /processorParam:MakeSquare=False 121 | /processorParam:TextureFormat=Color 122 | /build:illegal_input.png;illegal_input 123 | 124 | #begin l1_button.png 125 | /importer:TextureImporter 126 | /processor:TextureProcessor 127 | /processorParam:ColorKeyColor=255,0,255,255 128 | /processorParam:ColorKeyEnabled=True 129 | /processorParam:GenerateMipmaps=False 130 | /processorParam:PremultiplyAlpha=True 131 | /processorParam:ResizeToPowerOfTwo=False 132 | /processorParam:MakeSquare=False 133 | /processorParam:TextureFormat=Color 134 | /build:l1_button.png;l1_button 135 | 136 | #begin l2_button.png 137 | /importer:TextureImporter 138 | /processor:TextureProcessor 139 | /processorParam:ColorKeyColor=255,0,255,255 140 | /processorParam:ColorKeyEnabled=True 141 | /processorParam:GenerateMipmaps=False 142 | /processorParam:PremultiplyAlpha=True 143 | /processorParam:ResizeToPowerOfTwo=False 144 | /processorParam:MakeSquare=False 145 | /processorParam:TextureFormat=Color 146 | /build:l2_button.png;l2_button 147 | 148 | #begin left_button.png 149 | /importer:TextureImporter 150 | /processor:TextureProcessor 151 | /processorParam:ColorKeyColor=255,0,255,255 152 | /processorParam:ColorKeyEnabled=True 153 | /processorParam:GenerateMipmaps=False 154 | /processorParam:PremultiplyAlpha=True 155 | /processorParam:ResizeToPowerOfTwo=False 156 | /processorParam:MakeSquare=False 157 | /processorParam:TextureFormat=Color 158 | /build:left_button.png;left_button 159 | 160 | #begin left_shoulder_button.png 161 | /importer:TextureImporter 162 | /processor:TextureProcessor 163 | /processorParam:ColorKeyColor=255,0,255,255 164 | /processorParam:ColorKeyEnabled=True 165 | /processorParam:GenerateMipmaps=False 166 | /processorParam:PremultiplyAlpha=True 167 | /processorParam:ResizeToPowerOfTwo=False 168 | /processorParam:MakeSquare=False 169 | /processorParam:TextureFormat=Color 170 | /build:left_shoulder_button.png;left_shoulder_button 171 | 172 | #begin lt_button.png 173 | /importer:TextureImporter 174 | /processor:TextureProcessor 175 | /processorParam:ColorKeyColor=255,0,255,255 176 | /processorParam:ColorKeyEnabled=True 177 | /processorParam:GenerateMipmaps=False 178 | /processorParam:PremultiplyAlpha=True 179 | /processorParam:ResizeToPowerOfTwo=False 180 | /processorParam:MakeSquare=False 181 | /processorParam:TextureFormat=Color 182 | /build:lt_button.png;lt_button 183 | 184 | #begin mode_button.png 185 | /importer:TextureImporter 186 | /processor:TextureProcessor 187 | /processorParam:ColorKeyColor=255,0,255,255 188 | /processorParam:ColorKeyEnabled=True 189 | /processorParam:GenerateMipmaps=False 190 | /processorParam:PremultiplyAlpha=True 191 | /processorParam:ResizeToPowerOfTwo=False 192 | /processorParam:MakeSquare=False 193 | /processorParam:TextureFormat=Color 194 | /build:mode_button.png;mode_button 195 | 196 | #begin r1_button.png 197 | /importer:TextureImporter 198 | /processor:TextureProcessor 199 | /processorParam:ColorKeyColor=255,0,255,255 200 | /processorParam:ColorKeyEnabled=True 201 | /processorParam:GenerateMipmaps=False 202 | /processorParam:PremultiplyAlpha=True 203 | /processorParam:ResizeToPowerOfTwo=False 204 | /processorParam:MakeSquare=False 205 | /processorParam:TextureFormat=Color 206 | /build:r1_button.png;r1_button 207 | 208 | #begin r2_button.png 209 | /importer:TextureImporter 210 | /processor:TextureProcessor 211 | /processorParam:ColorKeyColor=255,0,255,255 212 | /processorParam:ColorKeyEnabled=True 213 | /processorParam:GenerateMipmaps=False 214 | /processorParam:PremultiplyAlpha=True 215 | /processorParam:ResizeToPowerOfTwo=False 216 | /processorParam:MakeSquare=False 217 | /processorParam:TextureFormat=Color 218 | /build:r2_button.png;r2_button 219 | 220 | #begin right_button.png 221 | /importer:TextureImporter 222 | /processor:TextureProcessor 223 | /processorParam:ColorKeyColor=255,0,255,255 224 | /processorParam:ColorKeyEnabled=True 225 | /processorParam:GenerateMipmaps=False 226 | /processorParam:PremultiplyAlpha=True 227 | /processorParam:ResizeToPowerOfTwo=False 228 | /processorParam:MakeSquare=False 229 | /processorParam:TextureFormat=Color 230 | /build:right_button.png;right_button 231 | 232 | #begin right_shoulder_button.png 233 | /importer:TextureImporter 234 | /processor:TextureProcessor 235 | /processorParam:ColorKeyColor=255,0,255,255 236 | /processorParam:ColorKeyEnabled=True 237 | /processorParam:GenerateMipmaps=False 238 | /processorParam:PremultiplyAlpha=True 239 | /processorParam:ResizeToPowerOfTwo=False 240 | /processorParam:MakeSquare=False 241 | /processorParam:TextureFormat=Color 242 | /build:right_shoulder_button.png;right_shoulder_button 243 | 244 | #begin rt_button.png 245 | /importer:TextureImporter 246 | /processor:TextureProcessor 247 | /processorParam:ColorKeyColor=255,0,255,255 248 | /processorParam:ColorKeyEnabled=True 249 | /processorParam:GenerateMipmaps=False 250 | /processorParam:PremultiplyAlpha=True 251 | /processorParam:ResizeToPowerOfTwo=False 252 | /processorParam:MakeSquare=False 253 | /processorParam:TextureFormat=Color 254 | /build:rt_button.png;rt_button 255 | 256 | #begin select_button.png 257 | /importer:TextureImporter 258 | /processor:TextureProcessor 259 | /processorParam:ColorKeyColor=255,0,255,255 260 | /processorParam:ColorKeyEnabled=True 261 | /processorParam:GenerateMipmaps=False 262 | /processorParam:PremultiplyAlpha=True 263 | /processorParam:ResizeToPowerOfTwo=False 264 | /processorParam:MakeSquare=False 265 | /processorParam:TextureFormat=Color 266 | /build:select_button.png;select_button 267 | 268 | #begin square_button.png 269 | /importer:TextureImporter 270 | /processor:TextureProcessor 271 | /processorParam:ColorKeyColor=255,0,255,255 272 | /processorParam:ColorKeyEnabled=True 273 | /processorParam:GenerateMipmaps=False 274 | /processorParam:PremultiplyAlpha=True 275 | /processorParam:ResizeToPowerOfTwo=False 276 | /processorParam:MakeSquare=False 277 | /processorParam:TextureFormat=Color 278 | /build:square_button.png;square_button 279 | 280 | #begin start_button.png 281 | /importer:TextureImporter 282 | /processor:TextureProcessor 283 | /processorParam:ColorKeyColor=255,0,255,255 284 | /processorParam:ColorKeyEnabled=True 285 | /processorParam:GenerateMipmaps=False 286 | /processorParam:PremultiplyAlpha=True 287 | /processorParam:ResizeToPowerOfTwo=False 288 | /processorParam:MakeSquare=False 289 | /processorParam:TextureFormat=Color 290 | /build:start_button.png;start_button 291 | 292 | #begin triangle_button.png 293 | /importer:TextureImporter 294 | /processor:TextureProcessor 295 | /processorParam:ColorKeyColor=255,0,255,255 296 | /processorParam:ColorKeyEnabled=True 297 | /processorParam:GenerateMipmaps=False 298 | /processorParam:PremultiplyAlpha=True 299 | /processorParam:ResizeToPowerOfTwo=False 300 | /processorParam:MakeSquare=False 301 | /processorParam:TextureFormat=Color 302 | /build:triangle_button.png;triangle_button 303 | 304 | #begin up_button.png 305 | /importer:TextureImporter 306 | /processor:TextureProcessor 307 | /processorParam:ColorKeyColor=255,0,255,255 308 | /processorParam:ColorKeyEnabled=True 309 | /processorParam:GenerateMipmaps=False 310 | /processorParam:PremultiplyAlpha=True 311 | /processorParam:ResizeToPowerOfTwo=False 312 | /processorParam:MakeSquare=False 313 | /processorParam:TextureFormat=Color 314 | /build:up_button.png;up_button 315 | 316 | #begin x_button.png 317 | /importer:TextureImporter 318 | /processor:TextureProcessor 319 | /processorParam:ColorKeyColor=255,0,255,255 320 | /processorParam:ColorKeyEnabled=True 321 | /processorParam:GenerateMipmaps=False 322 | /processorParam:PremultiplyAlpha=True 323 | /processorParam:ResizeToPowerOfTwo=False 324 | /processorParam:MakeSquare=False 325 | /processorParam:TextureFormat=Color 326 | /build:x_button.png;x_button 327 | 328 | #begin y_button.png 329 | /importer:TextureImporter 330 | /processor:TextureProcessor 331 | /processorParam:ColorKeyColor=255,0,255,255 332 | /processorParam:ColorKeyEnabled=True 333 | /processorParam:GenerateMipmaps=False 334 | /processorParam:PremultiplyAlpha=True 335 | /processorParam:ResizeToPowerOfTwo=False 336 | /processorParam:MakeSquare=False 337 | /processorParam:TextureFormat=Color 338 | /build:y_button.png;y_button 339 | 340 | #begin z_button.png 341 | /importer:TextureImporter 342 | /processor:TextureProcessor 343 | /processorParam:ColorKeyColor=255,0,255,255 344 | /processorParam:ColorKeyEnabled=True 345 | /processorParam:GenerateMipmaps=False 346 | /processorParam:PremultiplyAlpha=True 347 | /processorParam:ResizeToPowerOfTwo=False 348 | /processorParam:MakeSquare=False 349 | /processorParam:TextureFormat=Color 350 | /build:z_button.png;z_button 351 | 352 | -------------------------------------------------------------------------------- /Content/a_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/a_button.png -------------------------------------------------------------------------------- /Content/b_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/b_button.png -------------------------------------------------------------------------------- /Content/c_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/c_button.png -------------------------------------------------------------------------------- /Content/circle_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/circle_button.png -------------------------------------------------------------------------------- /Content/cross_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/cross_button.png -------------------------------------------------------------------------------- /Content/d_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/d_button.png -------------------------------------------------------------------------------- /Content/down_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/down_button.png -------------------------------------------------------------------------------- /Content/empty_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/empty_button.png -------------------------------------------------------------------------------- /Content/illegal_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/illegal_input.png -------------------------------------------------------------------------------- /Content/l1_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/l1_button.png -------------------------------------------------------------------------------- /Content/l2_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/l2_button.png -------------------------------------------------------------------------------- /Content/left_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/left_button.png -------------------------------------------------------------------------------- /Content/left_shoulder_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/left_shoulder_button.png -------------------------------------------------------------------------------- /Content/lt_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/lt_button.png -------------------------------------------------------------------------------- /Content/mode_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/mode_button.png -------------------------------------------------------------------------------- /Content/r1_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/r1_button.png -------------------------------------------------------------------------------- /Content/r2_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/r2_button.png -------------------------------------------------------------------------------- /Content/right_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/right_button.png -------------------------------------------------------------------------------- /Content/right_shoulder_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/right_shoulder_button.png -------------------------------------------------------------------------------- /Content/rt_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/rt_button.png -------------------------------------------------------------------------------- /Content/select_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/select_button.png -------------------------------------------------------------------------------- /Content/square_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/square_button.png -------------------------------------------------------------------------------- /Content/start_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/start_button.png -------------------------------------------------------------------------------- /Content/triangle_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/triangle_button.png -------------------------------------------------------------------------------- /Content/up_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/up_button.png -------------------------------------------------------------------------------- /Content/x_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/x_button.png -------------------------------------------------------------------------------- /Content/y_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/y_button.png -------------------------------------------------------------------------------- /Content/z_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Content/z_button.png -------------------------------------------------------------------------------- /DPadState.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer 3 | { 4 | public struct DPadState 5 | { 6 | public bool Up; 7 | public bool Down; 8 | public bool Left; 9 | public bool Right; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Linq; 4 | 5 | namespace InputVisualizer 6 | { 7 | public static class EnumExtensions 8 | { 9 | public static string GetDescription(this Enum en) 10 | { 11 | var mi = en.GetType().GetMember(en.ToString()); 12 | if ((mi != null && mi.Length > 0)) 13 | { 14 | var attrs = mi[0].GetCustomAttributes(typeof(DescriptionAttribute), false); 15 | if (attrs?.Length > 0) 16 | { 17 | return ((DescriptionAttribute)attrs.ElementAt(0)).Description; 18 | } 19 | } 20 | return en.ToString(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Fonts/DroidSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Fonts/DroidSans.ttf -------------------------------------------------------------------------------- /GameState.cs: -------------------------------------------------------------------------------- 1 | using InputVisualizer.Config; 2 | using InputVisualizer.Layouts; 3 | using Microsoft.Xna.Framework; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Timers; 7 | 8 | namespace InputVisualizer 9 | { 10 | public class GameState 11 | { 12 | public GamepadConfig ActiveGamepadConfig { get; set; } = null; 13 | public JoystickConfig ActiveJoystickConfig { get; set; } = null; 14 | public InputMode CurrentInputMode { get; set; } = InputMode.XInputOrKeyboard; 15 | public PlayerIndex CurrentPlayerIndex { get; set; } = PlayerIndex.One; 16 | public int CurrentJoystickIndex { get; set; } 17 | public DateTime MinAge { get; set; } 18 | public bool ConnectedToMister { get; set; } = false; 19 | public VisualizerEngine CurrentLayout { get; set; } 20 | public Dictionary ButtonStates = new Dictionary(); 21 | public Dictionary FrequencyDict = new Dictionary(); 22 | public float PixelsPerMs { get; set; } = 0.05f; 23 | public float LineMilliseconds { get; set; } = 0f; 24 | private Timer _purgeTimer = null; 25 | private float _currentPurgeDelay = 0f; 26 | public float AnalogStickDeadZoneTolerance = 0.2f; 27 | public float DirectInputDeadZoneTolerance = 15500; 28 | public bool DisplayIllegalInputs { get; set; } 29 | public DateTime CurrentTimeStamp { get; set; } = DateTime.Now; 30 | public int CurrentFrame { get; set; } = 0; 31 | 32 | public GameState() 33 | { 34 | } 35 | 36 | public void ResetPurgeTimer(float turnOffLineSpeed) 37 | { 38 | _currentPurgeDelay = turnOffLineSpeed; 39 | 40 | if (_purgeTimer != null) 41 | { 42 | _purgeTimer.Stop(); 43 | _purgeTimer.Dispose(); 44 | } 45 | _purgeTimer = new Timer(500); 46 | _purgeTimer.Elapsed += OnPurgeTimerElapsed; 47 | _purgeTimer.AutoReset = false; 48 | _purgeTimer.Start(); 49 | } 50 | 51 | private void OnPurgeTimerElapsed(object sender, ElapsedEventArgs e) 52 | { 53 | foreach (var button in ButtonStates.Values) 54 | { 55 | button.RemoveOldStateChanges(LineMilliseconds + _currentPurgeDelay + 500); 56 | } 57 | _purgeTimer.Start(); 58 | } 59 | 60 | public void UpdateMinAge(int currentLineLength) 61 | { 62 | LineMilliseconds = currentLineLength / PixelsPerMs; 63 | MinAge = CurrentTimeStamp.AddMilliseconds(-LineMilliseconds); 64 | } 65 | 66 | public void UpdateSpeed(float currentSpeed) 67 | { 68 | PixelsPerMs = 0.05f * currentSpeed; 69 | } 70 | 71 | public void ProcessIllegalDpadStates( DPadState dPadState, DateTime timeStamp, int currentFrame ) 72 | { 73 | if( !DisplayIllegalInputs ) 74 | { 75 | return; 76 | } 77 | 78 | var upDownPressed = ButtonStates["updown_violation"].IsPressed(); 79 | var leftRightPressed = ButtonStates["leftright_violation"].IsPressed(); 80 | 81 | if( upDownPressed != (dPadState.Up && dPadState.Down ) ) 82 | { 83 | ButtonStates["updown_violation"].AddStateChange(dPadState.Up && dPadState.Down, timeStamp, currentFrame); 84 | } 85 | if (leftRightPressed != (dPadState.Left && dPadState.Right)) 86 | { 87 | ButtonStates["leftright_violation"].AddStateChange(dPadState.Left && dPadState.Right, timeStamp, currentFrame); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /GamepadStyle.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.ComponentModel; 3 | 4 | namespace InputVisualizer 5 | { 6 | public enum GamepadStyle 7 | { 8 | XBOX = 0, 9 | NES = 1, 10 | [Description("Neo Geo")] 11 | NeoGeo = 2, 12 | SNES = 3, 13 | Genesis = 4, 14 | Playstation = 5 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Hooks/KeyboardHook.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Runtime.InteropServices; 5 | using Microsoft.Xna.Framework.Input; 6 | 7 | namespace InputVisualizer.Hooks 8 | { 9 | public class KeyboardHook 10 | { 11 | private const int WH_KEYBOARD_LL = 13; 12 | private const int WM_KEYDOWN = 0x0100; 13 | private const int WM_KEYUP = 0x101; 14 | private static LowLevelKeyboardProc _proc = HookCallback; 15 | private static IntPtr _hookID = IntPtr.Zero; 16 | public static Dictionary KeyboardState { get; private set; } 17 | 18 | public KeyboardHook() 19 | { 20 | KeyboardState = new Dictionary(); 21 | foreach (var value in Enum.GetValues(typeof(Keys))) 22 | { 23 | KeyboardState.Add((Keys)value, false); 24 | } 25 | } 26 | 27 | public void InstallHook() 28 | { 29 | try 30 | { 31 | _hookID = SetHook(_proc); 32 | } 33 | catch 34 | { 35 | _hookID = IntPtr.Zero; 36 | } 37 | } 38 | 39 | public void UninstallHook() 40 | { 41 | if (_hookID != IntPtr.Zero) 42 | { 43 | UnhookWindowsHookEx(_hookID); 44 | } 45 | } 46 | 47 | private static IntPtr SetHook(LowLevelKeyboardProc proc) 48 | { 49 | using (Process curProcess = Process.GetCurrentProcess()) 50 | using (ProcessModule curModule = curProcess.MainModule) 51 | { 52 | return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0); 53 | } 54 | } 55 | 56 | private delegate IntPtr LowLevelKeyboardProc( int nCode, IntPtr wParam, IntPtr lParam); 57 | 58 | private static IntPtr HookCallback( int nCode, IntPtr wParam, IntPtr lParam) 59 | { 60 | if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN) 61 | { 62 | int vkCode = Marshal.ReadInt32(lParam); 63 | var key = (Keys)vkCode; 64 | KeyboardState[key] = true; 65 | } 66 | else if (nCode >= 0 && wParam == (IntPtr)WM_KEYUP) 67 | { 68 | int vkCode = Marshal.ReadInt32(lParam); 69 | var key = (Keys)vkCode; 70 | KeyboardState[key] = false; 71 | } 72 | return CallNextHookEx(_hookID, nCode, wParam, lParam); 73 | } 74 | 75 | [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] 76 | private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId); 77 | 78 | [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] 79 | [return: MarshalAs(UnmanagedType.Bool)] 80 | private static extern bool UnhookWindowsHookEx(IntPtr hhk); 81 | 82 | [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] 83 | private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); 84 | 85 | [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] 86 | private static extern IntPtr GetModuleHandle(string lpModuleName); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Icon.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Icon.bmp -------------------------------------------------------------------------------- /Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kfmike/InputVisualizer/98c5811fe3a21545f241ed3e5238d03569e642b0/Icon.ico -------------------------------------------------------------------------------- /InputHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework.Input; 2 | using System; 3 | 4 | namespace InputVisualizer 5 | { 6 | public static class InputHelper 7 | { 8 | public struct AnalogDpadState 9 | { 10 | public AnalogDpadState() 11 | { 12 | UpDown = ButtonType.NONE; 13 | LeftRight = ButtonType.NONE; 14 | } 15 | public ButtonType UpDown { get; set; } 16 | public ButtonType LeftRight { get; set; } 17 | } 18 | 19 | public static AnalogDpadState GetAnalogDpadMovement( GamePadState state, float tolerance ) 20 | { 21 | var x = Math.Abs(state.ThumbSticks.Left.X); 22 | var y = Math.Abs(state.ThumbSticks.Left.Y); 23 | 24 | var result = new AnalogDpadState(); 25 | 26 | if( x > tolerance ) 27 | { 28 | result.LeftRight = state.ThumbSticks.Left.X > 0 ? ButtonType.RIGHT : ButtonType.LEFT; 29 | } 30 | if( y > tolerance ) 31 | { 32 | result.UpDown = state.ThumbSticks.Left.Y > 0 ? ButtonType.UP : ButtonType.DOWN; 33 | } 34 | return result; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /InputMode.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer 3 | { 4 | public enum InputMode 5 | { 6 | None = 0, 7 | XInputOrKeyboard, 8 | DirectInput, 9 | RetroSpy, 10 | Usb2Snes, 11 | MiSTer 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /InputVisualizer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net6.0 5 | Major 6 | false 7 | false 8 | AnyCPU;x64 9 | 10 | 11 | app.manifest 12 | Icon.ico 13 | 14 | 15 | 16 | Icon.bmp 17 | 18 | 19 | Icon.ico 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Always 36 | 37 | 38 | Always 39 | 40 | 41 | Always 42 | 43 | 44 | Always 45 | 46 | 47 | Always 48 | 49 | 50 | Always 51 | 52 | 53 | Always 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /InputVisualizer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.34024.191 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InputVisualizer", "InputVisualizer.csproj", "{CC5AF0C3-0DDC-4C4D-B941-00369E2B2F10}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Release|Any CPU = Release|Any CPU 13 | Release|x64 = Release|x64 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {CC5AF0C3-0DDC-4C4D-B941-00369E2B2F10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {CC5AF0C3-0DDC-4C4D-B941-00369E2B2F10}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {CC5AF0C3-0DDC-4C4D-B941-00369E2B2F10}.Debug|x64.ActiveCfg = Debug|x64 19 | {CC5AF0C3-0DDC-4C4D-B941-00369E2B2F10}.Debug|x64.Build.0 = Debug|x64 20 | {CC5AF0C3-0DDC-4C4D-B941-00369E2B2F10}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {CC5AF0C3-0DDC-4C4D-B941-00369E2B2F10}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {CC5AF0C3-0DDC-4C4D-B941-00369E2B2F10}.Release|x64.ActiveCfg = Release|x64 23 | {CC5AF0C3-0DDC-4C4D-B941-00369E2B2F10}.Release|x64.Build.0 = Release|x64 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {9F137D71-4809-4A52-931A-1269AF8323FE} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /LICENSE.MonoGame.md: -------------------------------------------------------------------------------- 1 | Microsoft Public License (Ms-PL) 2 | MonoGame - Copyright © 2009-2023 The MonoGame Team 3 | 4 | All rights reserved. 5 | 6 | This license governs use of the accompanying software. If you use the software, 7 | you accept this license. If you do not accept the license, do not use the 8 | software. 9 | 10 | 1. Definitions 11 | 12 | The terms "reproduce," "reproduction," "derivative works," and "distribution" 13 | have the same meaning here as under U.S. copyright law. 14 | 15 | A "contribution" is the original software, or any additions or changes to the 16 | software. 17 | 18 | A "contributor" is any person that distributes its contribution under this 19 | license. 20 | 21 | "Licensed patents" are a contributor's patent claims that read directly on its 22 | contribution. 23 | 24 | 2. Grant of Rights 25 | 26 | (A) Copyright Grant- Subject to the terms of this license, including the 27 | license conditions and limitations in section 3, each contributor grants you a 28 | non-exclusive, worldwide, royalty-free copyright license to reproduce its 29 | contribution, prepare derivative works of its contribution, and distribute its 30 | contribution or any derivative works that you create. 31 | 32 | (B) Patent Grant- Subject to the terms of this license, including the license 33 | conditions and limitations in section 3, each contributor grants you a 34 | non-exclusive, worldwide, royalty-free license under its licensed patents to 35 | make, have made, use, sell, offer for sale, import, and/or otherwise dispose of 36 | its contribution in the software or derivative works of the contribution in the 37 | software. 38 | 39 | 3. Conditions and Limitations 40 | 41 | (A) No Trademark License- This license does not grant you rights to use any 42 | contributors' name, logo, or trademarks. 43 | 44 | (B) If you bring a patent claim against any contributor over patents that you 45 | claim are infringed by the software, your patent license from such contributor 46 | to the software ends automatically. 47 | 48 | (C) If you distribute any portion of the software, you must retain all 49 | copyright, patent, trademark, and attribution notices that are present in the 50 | software. 51 | 52 | (D) If you distribute any portion of the software in source code form, you may 53 | do so only under this license by including a complete copy of this license with 54 | your distribution. If you distribute any portion of the software in compiled or 55 | object code form, you may only do so under a license that complies with this 56 | license. 57 | 58 | (E) The software is licensed "as-is." You bear the risk of using it. The 59 | contributors give no express warranties, guarantees or conditions. You may have 60 | additional consumer rights under your local laws which this license cannot 61 | change. To the extent permitted under your local laws, the contributors exclude 62 | the implied warranties of merchantability, fitness for a particular purpose and 63 | non-infringement. 64 | 65 | ------------------------------------------------------------------------------- 66 | 67 | The MIT License (MIT) 68 | Portions Copyright © The Mono.Xna Team 69 | 70 | Permission is hereby granted, free of charge, to any person obtaining a copy 71 | of this software and associated documentation files (the "Software"), to deal 72 | in the Software without restriction, including without limitation the rights 73 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 74 | copies of the Software, and to permit persons to whom the Software is 75 | furnished to do so, subject to the following conditions: 76 | 77 | The above copyright notice and this permission notice shall be included in 78 | all copies or substantial portions of the Software. 79 | 80 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 81 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 82 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 83 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 84 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 85 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 86 | THE SOFTWARE. -------------------------------------------------------------------------------- /LICENSE.Myra.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2020 The Myra Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /LICENSE.Renci.SSH.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Renci, Oleg Kapeljushnik, Gert Driesen and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MouseButtonType.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer 3 | { 4 | public enum MouseButtonType 5 | { 6 | None = 0, 7 | LeftButton = 1, 8 | RightButton = 2, 9 | MiddleButton = 3, 10 | XButton1 = 4, 11 | XButton2 = 5 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using var game = new InputVisualizer.InputVisualizer(); 3 | game.Run(); 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InputVisualizer 2 | 3 | View your PC gamepad and classic retro console controller inputs in an entirely new way. 4 | InputVisualizer allows you to see your controller input graphically over time, including press duration and button mash frequency. 5 | Useful for speedrunners and streamers alike. 6 | 7 | **Features** 8 | 9 | - PC XInput/DirectInput gamepad, keyboard, and mouse support with customizable input mapping 10 | - RetroSpy/NintendoSpy support for NES, SNES, Sega Genesis, and Playstation 1/2 consoles 11 | - RetroSpy MiSTer script support 12 | - SD2SNES/FXPAK PRO support for SNES hardware using Usb2Snes/QUsb2Snes/SNI 13 | - Customizable display with background transparency for easy integration into streaming layouts 14 | - Displays button press frequency and duration metrics 15 | - Display frame duration metrics for the last button press, configurable per button up to a max of 20 frames 16 | - Compact display view option to only show active buttons 17 | - Option to display illegal d-pad input combinations such as up + down at the same time 18 | 19 | ![visualizer_sample](https://github.com/kfmike/InputVisualizer/assets/57804306/fda458a9-939a-4f20-98e3-3fccf51d01db) 20 | 21 | # OBS Settings 22 | 23 | Add InputVisualizer as a "Game Capture" source. 24 | 25 | **Transparent Background** 26 | - Check "Allow Transparency" in the game input source properties. 27 | - Set the InputVisualizer background color alpha value to zero (default) 28 | 29 | # Frame Counts 30 | When configuring input mappings, there is a column named "Max Frames". If you select a value greater than zero for a specific button, the visualizer will show you how many frames the last button press was, 60fps, up to the maximum frame count specified. 31 | So, if for example you only care about viewing frame duration for the A button up to 5 frames, you would set that button's "Max Frames" to 5, and whenever you do a press between 1f-5f, it will show up in the visualizer display next to the line. 32 | 33 | # Compact View 34 | By default, the visualizer will be in compact view, and only show a maximum # of lines. Buttons that are pressed but cannot appear on a line will be displayed below or next to the lines and flash to indicate activity. 35 | If a line opens up, they will move up into the lines and the entire line history will be available for that button. 36 | 37 | By selecting "All" in the display configuration for Max Lines, you will have the original functionality of prior versions. 38 | 39 | # RetroSpy MiSTer Script Support 40 | You can use this software to view inputs from a MiSTer FPGA by installing the [Retro-Spy MiSTer Script](https://retro-spy.com/wiki/setting-up-retrospy-for-the-mister/) 41 | 42 | # SD2SNES/FXPAK PRO Game Support 43 | Please check the following document to see an incomplete list of games that are currently supported: 44 | 45 | https://docs.google.com/spreadsheets/d/1nq40DwiOmKDQm1oxOPezcIoIM7wi8jIx71q46V8Fz0k/edit?usp=sharing 46 | 47 | You can update InputVisualizer with the current list by replacing the usb2snesGameList.json file with the most recent version here: 48 | 49 | https://github.com/kfmike/InputVisualizer/blob/master/usb2snesGameList.json 50 | 51 | # Troubleshooting Arduino Serial Port Error / Visualizer freezing issues: 52 | There is a known issue with the latest Windows driver for the usb serial com with some arduino devices. 53 | In order to fix this issue, it is recommended to downgrade to an older driver: 54 | 55 | In device manager go to the usb serial CHXXX in the Ports (COM & LPT) section. 56 | Click on update driver and then select the "Let me pick from a list of available drivers on my computer" option. 57 | Select the one from 2013 or 2014. 58 | 59 | # Contributing to InputVisualizer 60 | 61 | I'm not accepting any pull requests at this time. 62 | 63 | I don't have time to properly manage reviewing contributions, and would like to maintain and grow this project at my own pace. 64 | 65 | Please feel free to submit issues and bug reports if you come across something that should be added or needs fixing. 66 | 67 | This is a fun personal project for me, so I initially added features for what I do most often. 68 | I will do my best to listen to feedback and continue to add new stuff, but it might take time. 69 | I appreciate your patience and support! 70 | 71 | # Credits 72 | 73 | Game framework and UI libraries: 74 | - MonoGame (https://www.monogame.net/) 75 | - Myra (https://github.com/rds1983/Myra) 76 | 77 | RetroSpy signal reader code: 78 | - https://retro-spy.com/ 79 | - https://github.com/retrospy/RetroSpy 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /RetroSpy/ControllerConnectionFailedArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace InputVisualizer.RetroSpy 4 | { 5 | public class ControllerConnectionFailedArgs : EventArgs 6 | { 7 | public Exception Exception { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /RetroSpy/ISSHControllerReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace InputVisualizer.RetroSpy 4 | { 5 | public interface ISSHControllerReader : IControllerReader 6 | { 7 | public event EventHandler ControllerConnected; 8 | public event EventHandler ControllerConnectionFailed; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /RetroSpy/MiSTerReader.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | using System.Globalization; 7 | 8 | namespace InputVisualizer.RetroSpy 9 | { 10 | public static class MiSTerReader 11 | { 12 | private static readonly string[] AXES_NAMES = { 13 | "x", "y", "z", "rx", "ry", "rz", "s0", "s1" 14 | }; 15 | 16 | public static ControllerStateEventArgs? ReadFromPacket(byte[]? packet) 17 | { 18 | if (packet == null) 19 | { 20 | throw new ArgumentNullException(nameof(packet)); 21 | } 22 | 23 | if (packet.Length < 16) 24 | { 25 | return null; 26 | } 27 | 28 | int axes = 0; 29 | for (byte j = 0; j < 8; ++j) 30 | { 31 | axes |= (packet[j] == 0x30 ? 0 : 1) << j; 32 | } 33 | 34 | int buttons = 0; 35 | for (byte j = 0; j < 8; ++j) 36 | { 37 | buttons |= (packet[8 + j] == 0x30 ? 0 : 1) << j; 38 | } 39 | 40 | int packetSize = 16 + (axes * 32) + buttons + 1; 41 | 42 | if (packet.Length != packetSize) 43 | { 44 | return null; 45 | } 46 | 47 | byte[] buttonValues = new byte[buttons]; 48 | int[] axesValues = new int[axes]; 49 | 50 | for (int i = 0; i < buttons; ++i) 51 | { 52 | buttonValues[i] = (byte)((packet[16 + i] == 0x31) ? 1 : 0); 53 | } 54 | 55 | for (int i = 0; i < axes; ++i) 56 | { 57 | axesValues[i] = 0; 58 | for (byte j = 0; j < 32; ++j) 59 | { 60 | axesValues[i] |= (packet[16 + buttons + (i * 32) + j] == 0x30 ? 0 : 1) << j; 61 | } 62 | } 63 | 64 | ControllerStateBuilder outState = new(); 65 | 66 | for (int i = 0; i < buttonValues.Length; ++i) 67 | { 68 | outState.SetButton("b" + i.ToString(CultureInfo.CurrentCulture), buttonValues[i] != 0x00); 69 | } 70 | 71 | for (int i = 0; i < axesValues.Length; ++i) 72 | { 73 | if (i < AXES_NAMES.Length) 74 | { 75 | outState.SetAnalog(AXES_NAMES[i], axesValues[i] / (float)short.MaxValue, axesValues[i]); 76 | } 77 | 78 | outState.SetAnalog("a" + i.ToString(CultureInfo.CurrentCulture), axesValues[i] / (float)short.MaxValue, axesValues[i]); 79 | } 80 | 81 | if (axes >= 2) 82 | { 83 | if (axesValues[axes - 2] < 0) 84 | { 85 | outState.SetButton("left", true); 86 | outState.SetButton("right", false); 87 | } 88 | else if (axesValues[axes - 2] > 0) 89 | { 90 | outState.SetButton("right", true); 91 | outState.SetButton("left", false); 92 | } 93 | else 94 | { 95 | outState.SetButton("left", false); 96 | outState.SetButton("right", false); 97 | } 98 | 99 | if (axesValues[axes - 1] < 0) 100 | { 101 | outState.SetButton("up", true); 102 | outState.SetButton("down", false); 103 | } 104 | else if (axesValues[axes - 1] > 0) 105 | { 106 | outState.SetButton("down", true); 107 | outState.SetButton("up", false); 108 | } 109 | else 110 | { 111 | outState.SetButton("up", false); 112 | outState.SetButton("down", false); 113 | } 114 | } 115 | else 116 | { 117 | outState.SetButton("up", false); 118 | outState.SetButton("down", false); 119 | outState.SetButton("left", false); 120 | outState.SetButton("right", false); 121 | } 122 | 123 | return outState.Build(); 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /RetroSpy/Playstation2.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | 7 | namespace InputVisualizer.RetroSpy 8 | { 9 | public static class Playstation2 10 | { 11 | private const int PACKET_SIZE = 168; 12 | private const int PACKET_SIZE_WO_RUMBLE = 152; 13 | private const int POLISHED_PACKET_SIZE = 35; 14 | 15 | private static readonly string?[] BUTTONS = { 16 | null, "select", "lstick", "rstick", "start", "up", "right", "down", "left", "l2", "r2", "l1", "r1", "triangle", "circle", "cross", "square" 17 | }; 18 | 19 | private static readonly string?[] ANALOG_BUTTONS = { 20 | null, null, null, null, "start", "up", "right", "down", "left", "l2", "l1", "square", "triangle", "r1", "circle", "cross", "r2" 21 | }; 22 | 23 | private static readonly string?[] NEGCON_BUTTONS = { 24 | null, null, null, null, "start", "up", "right", "down", "left", null, null, null, "r1", "a", "b", null, null 25 | }; 26 | 27 | private static float ReadAnalogButton(byte input) 28 | { 29 | return (float)input / 256; 30 | } 31 | 32 | private static float ReadStick(byte input) 33 | { 34 | return (float)(input - 128) / 128; 35 | } 36 | private static float ReadNegConSteering(byte input) 37 | { 38 | return (input - 128.0f) / -128.0f; 39 | } 40 | 41 | private static float ReadMouse(bool over, byte data) 42 | { 43 | float val = over ? data >= 128 ? -1.0f : 1.0f : data >= 128 ? -1.0f * (255 - data) / 127.0f : data / 127.0f; 44 | return val; 45 | } 46 | 47 | public static ControllerStateEventArgs? ReadFromPacket(byte[]? packet) 48 | { 49 | if (packet == null) 50 | { 51 | throw new ArgumentNullException(nameof(packet)); 52 | } 53 | 54 | if (packet.Length < PACKET_SIZE_WO_RUMBLE) 55 | { 56 | return null; 57 | } 58 | 59 | bool hasRumble = packet.Length == PACKET_SIZE; 60 | 61 | byte[] polishedPacket = new byte[POLISHED_PACKET_SIZE]; 62 | 63 | polishedPacket[0] = 0; 64 | for (byte i = 0; i < 8; ++i) 65 | { 66 | polishedPacket[0] |= (byte)((packet[i] == 0 ? 0 : 1) << i); 67 | } 68 | 69 | for (byte i = 0; i < 16; ++i) 70 | { 71 | polishedPacket[i + 1] = packet[i + 8]; 72 | } 73 | 74 | int nextNumBytes = 0; 75 | if (polishedPacket[0] == 0x73 || polishedPacket[0] == 0x79 76 | || polishedPacket[0] == 0x53 || polishedPacket[0] == 0x23) 77 | { 78 | nextNumBytes = 4; 79 | } 80 | else if (polishedPacket[0] == 0x12) 81 | { 82 | nextNumBytes = 2; 83 | } 84 | 85 | for (int i = 0; i < nextNumBytes; ++i) 86 | { 87 | polishedPacket[17 + i] = 0; 88 | for (byte j = 0; j < 8; ++j) 89 | { 90 | polishedPacket[17 + i] |= (byte)((packet[24 + (i * 8) + j] == 0 ? 0 : 1) << j); 91 | } 92 | } 93 | 94 | if (polishedPacket[0] == 0x79) 95 | { 96 | for (int i = 0; i < 12; ++i) 97 | { 98 | polishedPacket[21 + i] = 0; 99 | for (byte j = 0; j < 8; ++j) 100 | { 101 | polishedPacket[21 + i] |= (byte)((packet[56 + (i * 8) + j] == 0 ? 0 : 1) << j); 102 | } 103 | } 104 | } 105 | 106 | if (hasRumble) 107 | { 108 | for (int i = 0; i < 2; ++i) 109 | { 110 | polishedPacket[33 + i] = 0; 111 | for (byte j = 0; j < 8; ++j) 112 | { 113 | polishedPacket[33 + i] |= (byte)((packet[152 + (i * 8) + j] == 0 ? 0 : 1) << j); 114 | } 115 | } 116 | } 117 | 118 | if (polishedPacket.Length < (hasRumble ? POLISHED_PACKET_SIZE : POLISHED_PACKET_SIZE - 2)) 119 | { 120 | return null; 121 | } 122 | 123 | if (polishedPacket[0] != 0x41 && polishedPacket[0] != 0x73 && polishedPacket[0] != 0x79 124 | && polishedPacket[0] != 0x12 && polishedPacket[0] != 0x23 && polishedPacket[0] != 0x53) 125 | { 126 | return null; 127 | } 128 | 129 | ControllerStateBuilder state = new(); 130 | 131 | if (polishedPacket[0] == 0x53) 132 | { 133 | SetButtons(ANALOG_BUTTONS, polishedPacket, state); 134 | } 135 | else if (polishedPacket[0] == 0x23) 136 | { 137 | SetButtons(NEGCON_BUTTONS, polishedPacket, state); 138 | } 139 | else 140 | { 141 | SetButtons(BUTTONS, polishedPacket, state); 142 | } 143 | 144 | if (hasRumble) 145 | { 146 | state.SetAnalog("motor1", ReadAnalogButton(polishedPacket[33]), polishedPacket[33]); 147 | state.SetAnalog("motor2", ReadAnalogButton(polishedPacket[34]), polishedPacket[34]); 148 | } 149 | else 150 | { 151 | state.SetAnalog("motor1", 0, 0); 152 | state.SetAnalog("motor2", 0, 0); 153 | } 154 | 155 | if (polishedPacket[0] == 0x73 || polishedPacket[0] == 0x79 || polishedPacket[0] == 0x53) 156 | { 157 | state.SetAnalog("rstick_x", ReadStick(polishedPacket[17]), polishedPacket[17]); 158 | state.SetAnalog("rstick_y", ReadStick(polishedPacket[18]), polishedPacket[18]); 159 | state.SetAnalog("lstick_x", ReadStick(polishedPacket[19]), polishedPacket[19]); 160 | state.SetAnalog("lstick_y", ReadStick(polishedPacket[20]), polishedPacket[20]); 161 | } 162 | else if (polishedPacket[0] == 0x23) 163 | { 164 | float steering = ReadNegConSteering(polishedPacket[17]); 165 | if (steering < 0) 166 | { 167 | state.SetAnalog("l_steering", steering * -1, polishedPacket[17]); 168 | state.SetAnalog("r_steering", 0.0f, 0); 169 | } 170 | else if (steering > 0) 171 | { 172 | state.SetAnalog("l_steering", 0.0f, 0); 173 | state.SetAnalog("r_steering", steering, polishedPacket[17]); 174 | } 175 | else 176 | { 177 | state.SetAnalog("l_steering", 0.0f, 0); 178 | state.SetAnalog("r_steering", 0.0f, 0); 179 | } 180 | 181 | state.SetAnalog("I", ReadAnalogButton(polishedPacket[18]), polishedPacket[18]); 182 | state.SetAnalog("II", ReadAnalogButton(polishedPacket[19]), polishedPacket[19]); 183 | state.SetAnalog("l1", ReadAnalogButton(polishedPacket[20]), polishedPacket[20]); 184 | } 185 | else 186 | { 187 | state.SetAnalog("rstick_x", 0, 0); 188 | state.SetAnalog("rstick_y", 0, 0); 189 | state.SetAnalog("lstick_x", 0, 0); 190 | state.SetAnalog("lstick_y", 0, 0); 191 | } 192 | 193 | if (polishedPacket[0] == 0x79) 194 | { 195 | state.SetAnalog("analog_right", polishedPacket[6] != 0x00 ? ReadAnalogButton(polishedPacket[21]) : 0.0f, polishedPacket[6] != 0x00 ? polishedPacket[21] : 0); 196 | state.SetAnalog("analog_left", polishedPacket[8] != 0x00 ? ReadAnalogButton(polishedPacket[22]) : 0.0f, polishedPacket[8] != 0x00 ? polishedPacket[22] : 0); 197 | state.SetAnalog("analog_up", polishedPacket[5] != 0x00 ? ReadAnalogButton(polishedPacket[23]) : 0.0f, polishedPacket[5] != 0x00 ? polishedPacket[23] : 0); 198 | state.SetAnalog("analog_down", polishedPacket[7] != 0x00 ? ReadAnalogButton(polishedPacket[24]) : 0.0f, polishedPacket[7] != 0x00 ? polishedPacket[24] : 0); 199 | 200 | state.SetAnalog("analog_triangle", polishedPacket[13] != 0x00 ? ReadAnalogButton(polishedPacket[25]) : 0.0f, polishedPacket[13] != 0x00 ? polishedPacket[25] : 0); 201 | state.SetAnalog("analog_circle", polishedPacket[14] != 0x00 ? ReadAnalogButton(polishedPacket[26]) : 0.0f, polishedPacket[14] != 0x00 ? polishedPacket[26] : 0); 202 | state.SetAnalog("analog_x", polishedPacket[15] != 0x00 ? ReadAnalogButton(polishedPacket[27]) : 0.0f, polishedPacket[15] != 0x00 ? polishedPacket[27] : 0); 203 | state.SetAnalog("analog_square", polishedPacket[16] != 0x00 ? ReadAnalogButton(polishedPacket[28]) : 0.0f, polishedPacket[16] != 0x00 ? polishedPacket[28] : 0); 204 | 205 | state.SetAnalog("analog_l1", polishedPacket[11] != 0x00 ? ReadAnalogButton(polishedPacket[29]) : 0.0f, polishedPacket[11] != 0x00 ? polishedPacket[29] : 0); 206 | state.SetAnalog("analog_r1", polishedPacket[12] != 0x00 ? ReadAnalogButton(polishedPacket[30]) : 0.0f, polishedPacket[12] != 0x00 ? polishedPacket[30] : 0); 207 | state.SetAnalog("analog_l2", polishedPacket[9] != 0x00 ? ReadAnalogButton(polishedPacket[31]) : 0.0f, polishedPacket[9] != 0x00 ? polishedPacket[31] : 0); 208 | state.SetAnalog("analog_r2", polishedPacket[10] != 0x00 ? ReadAnalogButton(polishedPacket[32]) : 0.0f, polishedPacket[10] != 0x00 ? polishedPacket[32] : 0); 209 | } 210 | else 211 | { 212 | state.SetAnalog("analog_right", (float)(polishedPacket[6] != 0x00 ? 1.0 : 0.0), 0); 213 | state.SetAnalog("analog_left", (float)(polishedPacket[8] != 0x00 ? 1.0 : 0.0), 0); 214 | state.SetAnalog("analog_up", (float)(polishedPacket[5] != 0x00 ? 1.0 : 0.0), 0); 215 | state.SetAnalog("analog_down", (float)(polishedPacket[7] != 0x00 ? 1.0 : 0.0), 0); 216 | 217 | state.SetAnalog("analog_triangle", (float)(polishedPacket[13] != 0x00 ? 1.0 : 0.0), 0); 218 | state.SetAnalog("analog_circle", (float)(polishedPacket[14] != 0x00 ? 1.0 : 0.0), 0); 219 | state.SetAnalog("analog_x", (float)(polishedPacket[15] != 0x00 ? 1.0 : 0.0), 0); 220 | state.SetAnalog("analog_square", (float)(polishedPacket[16] != 0x00 ? 1.0 : 0.0), 0); 221 | 222 | state.SetAnalog("analog_l1", (float)(polishedPacket[11] != 0x00 ? 1.0 : 0.0), 0); 223 | state.SetAnalog("analog_r1", (float)(polishedPacket[12] != 0x00 ? 1.0 : 0.0), 0); 224 | state.SetAnalog("analog_l2", (float)(polishedPacket[9] != 0x00 ? 1.0 : 0.0), 0); 225 | state.SetAnalog("analog_r2", (float)(polishedPacket[10] != 0x00 ? 1.0 : 0.0), 0); 226 | } 227 | 228 | if (polishedPacket[0] == 0x12) 229 | { 230 | float x = ReadMouse(polishedPacket[10] == 0x00, polishedPacket[17]); 231 | float y = ReadMouse(polishedPacket[9] == 0x00, polishedPacket[18]); 232 | SignalTool.SetMouseProperties(x, y, polishedPacket[17], polishedPacket[18], state); 233 | } 234 | else 235 | { 236 | SignalTool.SetMouseProperties(0, 0, 0, 0, state); 237 | } 238 | 239 | return state.Build(); 240 | } 241 | 242 | private static void SetButtons(string?[] buttons, byte[] polishedPacket, ControllerStateBuilder state) 243 | { 244 | for (int i = 0; i < buttons.Length; ++i) 245 | { 246 | if (string.IsNullOrEmpty(buttons[i])) 247 | { 248 | continue; 249 | } 250 | 251 | state.SetButton(BUTTONS[i], polishedPacket[i] != 0x00); 252 | } 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /RetroSpy/SSHControllerReader.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | 7 | namespace InputVisualizer.RetroSpy 8 | { 9 | public sealed class SSHControllerReader : ISSHControllerReader, IDisposable 10 | { 11 | public event EventHandler? ControllerStateChanged; 12 | 13 | public event EventHandler? ControllerDisconnected; 14 | public event EventHandler ControllerConnected; 15 | public event EventHandler ControllerConnectionFailed; 16 | 17 | private readonly Func? _packetParser; 18 | private SSHMonitor? _serialMonitor; 19 | 20 | public SSHControllerReader(string hostname, string arguments, Func? packetParser, 21 | string username, string password, string? commandSub, int delayInMilliseconds = 0, bool useQuickDisconnect = false) 22 | { 23 | _packetParser = packetParser; 24 | 25 | _serialMonitor = new SSHMonitor(hostname, arguments, username, password, commandSub, delayInMilliseconds, useQuickDisconnect); 26 | _serialMonitor.PacketReceived += SerialMonitor_PacketReceived; 27 | _serialMonitor.Connected += SerialMonitor_Connected; 28 | _serialMonitor.Disconnected += SerialMonitor_Disconnected; 29 | _serialMonitor.ConnectionFailed += SerialMonitor_ConnectionFailed; 30 | } 31 | 32 | public void Start() 33 | { 34 | _serialMonitor.Start(); 35 | } 36 | 37 | private void SerialMonitor_Connected(object sender, EventArgs e) 38 | { 39 | ControllerConnected?.Invoke(this, e); 40 | } 41 | 42 | private void SerialMonitor_ConnectionFailed(object sender, ControllerConnectionFailedArgs e) 43 | { 44 | ControllerConnectionFailed?.Invoke(this, e); 45 | } 46 | 47 | private void SerialMonitor_Disconnected(object? sender, EventArgs e) 48 | { 49 | Finish(); 50 | ControllerDisconnected?.Invoke(this, EventArgs.Empty); 51 | } 52 | 53 | private void SerialMonitor_PacketReceived(object? sender, PacketDataEventArgs packet) 54 | { 55 | if (ControllerStateChanged != null) 56 | { 57 | ControllerStateEventArgs? state = _packetParser != null ? _packetParser(packet.GetPacket()) : null; 58 | if (state != null) 59 | { 60 | ControllerStateChanged(this, state); 61 | } 62 | } 63 | } 64 | 65 | public void Finish() 66 | { 67 | if (_serialMonitor != null) 68 | { 69 | _serialMonitor.Stop(); 70 | _serialMonitor.Dispose(); 71 | _serialMonitor = null; 72 | } 73 | } 74 | 75 | private void Dispose(bool disposing) 76 | { 77 | if (disposing) 78 | { 79 | Finish(); 80 | } 81 | } 82 | 83 | public void Dispose() 84 | { 85 | Dispose(true); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /RetroSpy/SSHMonitor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using Renci.SshNet; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Globalization; 9 | using System.IO; 10 | using System.Net; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | namespace InputVisualizer.RetroSpy 15 | { 16 | public class SSHMonitor : IDisposable 17 | { 18 | private const int TIMER_MS = 1; 19 | 20 | public event EventHandler? PacketReceived; 21 | 22 | public event EventHandler Connected; 23 | public event EventHandler? Disconnected; 24 | public event EventHandler ConnectionFailed; 25 | 26 | private SshClient? _client; 27 | private ShellStream? _data; // Disposing this on a disconnect locks up the UI. It seems to cleanup itself when the connection is terminated. 28 | private readonly List _localBuffer; 29 | private readonly string _command; 30 | private readonly int _delayInMilliseconds; 31 | private System.Timers.Timer? _timer; 32 | private readonly bool quickDisconnect; 33 | 34 | public SSHMonitor(string hostname, string command, string username, string password, string? commandSub, int delayInMilliseconds, bool useQuickDisconnect) 35 | { 36 | string strIP = hostname; 37 | if (!IPAddress.TryParse(hostname, out _)) 38 | { 39 | var ips = Dns.GetHostEntry(hostname); 40 | 41 | foreach (var ip in ips.AddressList) 42 | { 43 | if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) 44 | { 45 | strIP = ip.ToString(); 46 | break; 47 | } 48 | } 49 | } 50 | 51 | _localBuffer = new List(); 52 | _client = new SshClient(strIP, username, password); 53 | 54 | _command = !string.IsNullOrEmpty(commandSub) ? string.Format(CultureInfo.CurrentCulture, command, commandSub) : command; 55 | _delayInMilliseconds = delayInMilliseconds; 56 | quickDisconnect = useQuickDisconnect; 57 | } 58 | 59 | public async void Start() 60 | { 61 | if (_timer != null) 62 | { 63 | return; 64 | } 65 | 66 | _localBuffer.Clear(); 67 | if (_client != null) 68 | { 69 | await ConnectToClient(); 70 | } 71 | 72 | _timer = new System.Timers.Timer(TIMER_MS); 73 | _timer.Elapsed += Tick; 74 | _timer.AutoReset = true; 75 | _timer.Start(); 76 | } 77 | 78 | private async Task ConnectToClient() 79 | { 80 | await Task.Run(() => 81 | { 82 | if (_client != null) 83 | { 84 | try 85 | { 86 | _client.Connect(); 87 | _data = _client.CreateShellStream("", 0, 0, 0, 0, 0); 88 | if (_delayInMilliseconds > 0) 89 | { 90 | Thread.Sleep(_delayInMilliseconds); 91 | } 92 | 93 | Connected?.Invoke(this, new EventArgs()); 94 | _data.WriteLine(_command); 95 | } 96 | catch (Exception ex) 97 | { 98 | ConnectionFailed?.Invoke(this, new ControllerConnectionFailedArgs { Exception = ex }); 99 | } 100 | } 101 | }); 102 | } 103 | 104 | public void Stop() 105 | { 106 | if (_data != null) 107 | { 108 | // This should be fine, but on disconnect it locks up everything. No closing it doesn't seem to have any adverse effects. 109 | //try 110 | //{ // If the device has been unplugged, Close will throw an IOException. This is fine, we'll just keep cleaning up. 111 | // //_data.Close(); 112 | //} 113 | //catch (IOException) { } 114 | _data = null; 115 | if (_client != null) 116 | { 117 | _client.Disconnect(); 118 | _client.Dispose(); 119 | _client = null; 120 | } 121 | } 122 | if (_timer != null) 123 | { 124 | _timer.Stop(); 125 | _timer = null; 126 | } 127 | } 128 | 129 | private int numNoReads; 130 | 131 | private void Tick(object? sender, EventArgs e) 132 | { 133 | if (_data == null || !_data.CanRead || PacketReceived == null) 134 | { 135 | return; 136 | } 137 | 138 | // Try to read some data from the COM port and append it to our localBuffer. 139 | // If there's an IOException then the device has been disconnected. 140 | try 141 | { 142 | int readCount = (int)_data.Length; 143 | if (quickDisconnect && readCount < 1) 144 | { 145 | numNoReads++; 146 | if (numNoReads == 100) 147 | { 148 | throw new SSHMonitorDisconnectException(); 149 | } 150 | return; 151 | } 152 | numNoReads = 0; 153 | byte[] readBuffer = new byte[readCount]; 154 | _ = _data.Read(readBuffer, 0, readCount); 155 | _localBuffer.AddRange(readBuffer); 156 | } 157 | catch (IOException) 158 | { 159 | Stop(); 160 | Disconnected?.Invoke(this, EventArgs.Empty); 161 | return; 162 | } 163 | 164 | // Try and find 2 splitting characters in our buffer. 165 | int lastSplitIndex = _localBuffer.LastIndexOf(0x0A); 166 | if (lastSplitIndex <= 1) 167 | { 168 | return; 169 | } 170 | 171 | int sndLastSplitIndex = _localBuffer.LastIndexOf(0x0A, lastSplitIndex - 1); 172 | if (lastSplitIndex == -1) 173 | { 174 | return; 175 | } 176 | 177 | // Grab the latest packet out of the buffer and fire it off to the receive event listeners. 178 | int packetStart = sndLastSplitIndex + 1; 179 | int packetSize = lastSplitIndex - packetStart; 180 | PacketReceived(this, new PacketDataEventArgs(_localBuffer.GetRange(packetStart, packetSize).ToArray())); 181 | 182 | // Clear our buffer up until the last split character. 183 | _localBuffer.RemoveRange(0, lastSplitIndex); 184 | } 185 | 186 | protected virtual void Dispose(bool disposing) 187 | { 188 | if (disposing) 189 | { 190 | Stop(); 191 | } 192 | } 193 | 194 | public void Dispose() 195 | { 196 | Dispose(true); 197 | GC.SuppressFinalize(this); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /RetroSpy/SSHMonitorDisconnectException.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | 7 | namespace InputVisualizer.RetroSpy 8 | { 9 | [Serializable] 10 | public class SSHMonitorDisconnectException : Exception 11 | { 12 | public SSHMonitorDisconnectException(string message) : base(message) 13 | { 14 | } 15 | 16 | public SSHMonitorDisconnectException(string message, Exception innerException) : base(message, innerException) 17 | { 18 | } 19 | 20 | public SSHMonitorDisconnectException() 21 | { 22 | } 23 | 24 | protected SSHMonitorDisconnectException(System.Runtime.Serialization.SerializationInfo serializationInfo, System.Runtime.Serialization.StreamingContext streamingContext) : base(serializationInfo, streamingContext) 25 | { 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /RetroSpyStateHandlers/MisterHandler.cs: -------------------------------------------------------------------------------- 1 | using InputVisualizer.RetroSpy; 2 | 3 | namespace InputVisualizer.RetroSpyStateHandlers 4 | { 5 | public class MisterHandler : RetroSpyControllerHandler 6 | { 7 | public MisterHandler(GameState gameState) : base(gameState) { } 8 | 9 | public override void ProcessControllerState(ControllerStateEventArgs e, int currentFrame) 10 | { 11 | base.ProcessControllerState(e, currentFrame); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RetroSpyStateHandlers/PlaystationHandler.cs: -------------------------------------------------------------------------------- 1 | using InputVisualizer.RetroSpy; 2 | 3 | namespace InputVisualizer.RetroSpyStateHandlers 4 | { 5 | public class PlaystationHandler : RetroSpyControllerHandler 6 | { 7 | public PlaystationHandler(GameState gameState) : base(gameState) { } 8 | 9 | public override void ProcessControllerState(ControllerStateEventArgs e, int currentFrame) 10 | { 11 | base.ProcessControllerState(e, currentFrame); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /RetroSpyStateHandlers/RetroSpyControllerHandler.cs: -------------------------------------------------------------------------------- 1 | using InputVisualizer.RetroSpy; 2 | using System; 3 | 4 | namespace InputVisualizer.RetroSpyStateHandlers 5 | { 6 | public class RetroSpyControllerHandler 7 | { 8 | protected GameState _gameState; 9 | 10 | public RetroSpyControllerHandler(GameState gameState) 11 | { 12 | _gameState = gameState; 13 | } 14 | 15 | public virtual void ProcessControllerState(ControllerStateEventArgs e, int currentFrame) 16 | { 17 | var dpadState = new DPadState(); 18 | var timeStamp = _gameState.CurrentTimeStamp; 19 | 20 | foreach (var button in e.Buttons) 21 | { 22 | if (!_gameState.ButtonStates.ContainsKey(button.Key)) 23 | { 24 | continue; 25 | } 26 | 27 | if (_gameState.ButtonStates[button.Key].IsPressed() != button.Value) 28 | { 29 | _gameState.ButtonStates[button.Key].AddStateChange(button.Value, timeStamp, currentFrame); 30 | } 31 | 32 | switch (button.Key) 33 | { 34 | case "UP": 35 | { 36 | dpadState.Up = button.Value; 37 | break; 38 | } 39 | case "DOWN": 40 | { 41 | dpadState.Down = button.Value; 42 | break; 43 | } 44 | case "LEFT": 45 | { 46 | dpadState.Left = button.Value; 47 | break; 48 | } 49 | case "RIGHT": 50 | { 51 | dpadState.Right = button.Value; 52 | break; 53 | } 54 | } 55 | } 56 | _gameState.ProcessIllegalDpadStates(dpadState, timeStamp, _gameState.CurrentFrame); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SystemGamePadInfo.cs: -------------------------------------------------------------------------------- 1 |  2 | using Microsoft.Xna.Framework; 3 | 4 | namespace InputVisualizer 5 | { 6 | public class SystemGamePadInfo 7 | { 8 | public string Id { get; set; } 9 | public string Name { get; set; } 10 | public PlayerIndex PlayerIndex { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SystemJoyStickInfo.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer 3 | { 4 | public class SystemJoyStickInfo 5 | { 6 | public string Id { get; set; } 7 | public string Name { get; set; } 8 | public int Index { get; set; } 9 | public int NumButtons { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /UI/InputSourceChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace InputVisualizer.UI 4 | { 5 | public class InputSourceChangedEventArgs : EventArgs 6 | { 7 | public string InputSourceId { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /UI/Usb2SnesGameChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace InputVisualizer.UI 4 | { 5 | public class Usb2SnesGameChangedEventArgs : EventArgs 6 | { 7 | public string Game { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Usb2Snes/Usb2SnesButtonFlags.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer.Usb2Snes 3 | { 4 | public enum Usb2SnesButtonFlags1 5 | { 6 | None = 0, 7 | A = 128, 8 | X = 64, 9 | L = 32, 10 | R = 16 11 | } 12 | 13 | public enum Usb2SnesButtonFlags2 14 | { 15 | None = 0, 16 | Up = 8, 17 | Down = 4, 18 | Left = 2, 19 | Right = 1, 20 | Start = 16, 21 | Select = 32, 22 | B = 128, 23 | Y = 64, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Usb2Snes/Usb2SnesClient.cs: -------------------------------------------------------------------------------- 1 | using InputVisualizer.Config; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.WebSockets; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using System.Timers; 10 | 11 | namespace InputVisualizer.Usb2Snes 12 | { 13 | public class Usb2SnesClient 14 | { 15 | public const string DEFAULT_SERVER = "ws://localhost"; 16 | public const string DEFAULT_PORT = "8080"; 17 | 18 | private const string SNES_SPACE = "SNES"; 19 | 20 | private const string OPCODE_DEVICE_LIST = "DeviceList"; 21 | private const string OPCODE_ATTACH = "Attach"; 22 | private const string OPCODE_INFO = "Info"; 23 | private const string OPCODE_GETADDRESS = "GetAddress"; 24 | private const string DEFAULT_ADDRESS = "F90718"; 25 | private const int RESPONSE_BUFFER_CHUNK = 1024; 26 | private const int INPUT_TIMER_MS = 1; 27 | private const int ROM_TIMER_MS = 2000; 28 | 29 | ClientWebSocket _socket = null; 30 | private string _server = null; 31 | private List _deviceList = new List(); 32 | private Usb2SnesGame _selectedGame; 33 | public List Devices => _deviceList; 34 | private System.Timers.Timer _inputTimer; 35 | private Usb2SnesState _state = Usb2SnesState.Idle; 36 | private string _currentDevice = null; 37 | private System.Timers.Timer _restartListenTimer; 38 | 39 | public Dictionary ButtonStates1 = new Dictionary(); 40 | public Dictionary ButtonStates2 = new Dictionary(); 41 | 42 | public Usb2SnesClient() 43 | { 44 | _server = $"{DEFAULT_SERVER}:{DEFAULT_PORT}"; 45 | _selectedGame = CreateDefaultGame(); 46 | _inputTimer = new System.Timers.Timer(); 47 | _inputTimer.Elapsed += InputTimerElapsed; 48 | _inputTimer.Interval = INPUT_TIMER_MS; 49 | _inputTimer.AutoReset = false; 50 | _inputTimer.Enabled = false; 51 | 52 | _restartListenTimer = new System.Timers.Timer(ROM_TIMER_MS); 53 | _restartListenTimer.Elapsed += RestartListenElapsed; 54 | _restartListenTimer.AutoReset = false; 55 | _restartListenTimer.Enabled = false; 56 | 57 | foreach (var flag in Enum.GetValues(typeof(Usb2SnesButtonFlags2))) 58 | { 59 | ButtonStates2.Add((Usb2SnesButtonFlags2)flag, false); 60 | } 61 | foreach (var flag in Enum.GetValues(typeof(Usb2SnesButtonFlags1))) 62 | { 63 | ButtonStates1.Add((Usb2SnesButtonFlags1)flag, false); 64 | } 65 | } 66 | 67 | private Usb2SnesGame CreateDefaultGame() 68 | { 69 | return new Usb2SnesGame { Name = "Default Game", Address = new string[] { DEFAULT_ADDRESS } }; 70 | } 71 | 72 | private async void RestartListenElapsed(object sender, ElapsedEventArgs e) 73 | { 74 | if (_state != Usb2SnesState.ListeningError) 75 | { 76 | return; 77 | } 78 | 79 | if (await StartListening(_currentDevice)) 80 | { 81 | return; 82 | } 83 | _restartListenTimer.Start(); 84 | } 85 | 86 | private void RestartListener() 87 | { 88 | _state = Usb2SnesState.ListeningError; 89 | _restartListenTimer.Start(); 90 | } 91 | 92 | public void SetCurrentGame(Usb2SnesGame game) 93 | { 94 | _selectedGame = game ?? CreateDefaultGame(); 95 | } 96 | 97 | public void SetServer(string server, string port) 98 | { 99 | if (string.IsNullOrEmpty(server) || string.IsNullOrEmpty(port)) 100 | { 101 | return; 102 | } 103 | _server = $"{server}:{port}"; 104 | } 105 | 106 | public async Task StopUsb2SnesClient() 107 | { 108 | try 109 | { 110 | StopListening(); 111 | await Disconnect(); 112 | } 113 | catch { } 114 | } 115 | 116 | public async Task> GetDeviceList() 117 | { 118 | _deviceList.Clear(); 119 | try 120 | { 121 | if (!await Connect()) 122 | { 123 | return _deviceList; 124 | } 125 | 126 | await SendRequest(OPCODE_DEVICE_LIST, SNES_SPACE, CreateCancellationToken()); 127 | var response = await GetResponse(); 128 | 129 | if (response != null) 130 | { 131 | _deviceList.AddRange(response.Results); 132 | } 133 | } 134 | catch 135 | { } 136 | return _deviceList; 137 | } 138 | 139 | public async Task StartListening(string deviceName) 140 | { 141 | _currentDevice = deviceName; 142 | 143 | if (!await Connect()) 144 | { 145 | return false; 146 | } 147 | 148 | if (!await SendRequest(OPCODE_ATTACH, SNES_SPACE, CancellationToken.None, new string[] { _currentDevice })) 149 | { 150 | return false; 151 | } 152 | 153 | if (!await SendRequest(OPCODE_INFO, SNES_SPACE, CancellationToken.None)) 154 | { 155 | return false; 156 | } 157 | var infoResponse = await GetResponse(); 158 | 159 | if (infoResponse == null) 160 | { 161 | return false; 162 | } 163 | 164 | _state = Usb2SnesState.Listening; 165 | _inputTimer.Start(); 166 | 167 | return true; 168 | } 169 | 170 | public void StopListening() 171 | { 172 | _state = Usb2SnesState.Idle; 173 | _inputTimer.Stop(); 174 | _restartListenTimer.Stop(); 175 | } 176 | 177 | private async void InputTimerElapsed(object sender, ElapsedEventArgs e) 178 | { 179 | try 180 | { 181 | if (_state != Usb2SnesState.Listening) 182 | { 183 | return; 184 | } 185 | 186 | if (_socket.State != WebSocketState.Open && !string.IsNullOrEmpty(_currentDevice)) 187 | { 188 | if (!await StartListening(_currentDevice)) 189 | { 190 | return; 191 | } 192 | } 193 | byte[] inputData = new byte[2]; 194 | 195 | if (_selectedGame.Address.Length == 1) 196 | { 197 | if (!await SendRequest(OPCODE_GETADDRESS, SNES_SPACE, CancellationToken.None, new string[] { _selectedGame.Address[0], "2" })) 198 | { 199 | return; 200 | } 201 | await GetBinaryResponse(inputData); 202 | } 203 | else 204 | { 205 | var oneByteBuffer = new byte[1]; 206 | if (!await SendRequest(OPCODE_GETADDRESS, SNES_SPACE, CancellationToken.None, new string[] { _selectedGame.Address[0], "1" })) 207 | { 208 | return; 209 | } 210 | await GetBinaryResponse(oneByteBuffer); 211 | inputData[0] = oneByteBuffer[0]; 212 | 213 | if (!await SendRequest(OPCODE_GETADDRESS, SNES_SPACE, CancellationToken.None, new string[] { _selectedGame.Address[1], "1" })) 214 | { 215 | return; 216 | } 217 | 218 | await GetBinaryResponse(oneByteBuffer); 219 | inputData[1] = oneByteBuffer[0]; 220 | } 221 | 222 | var flags1 = (Usb2SnesButtonFlags1)inputData[0]; 223 | var flags2 = (Usb2SnesButtonFlags2)inputData[1]; 224 | 225 | foreach (var state in ButtonStates1) 226 | { 227 | ButtonStates1[state.Key] = (flags1 & state.Key) != Usb2SnesButtonFlags1.None; 228 | } 229 | foreach (var state in ButtonStates2) 230 | { 231 | ButtonStates2[state.Key] = (flags2 & state.Key) != Usb2SnesButtonFlags2.None; 232 | } 233 | } 234 | catch 235 | { 236 | RestartListener(); 237 | } 238 | finally 239 | { 240 | _inputTimer.Start(); 241 | } 242 | } 243 | 244 | private async Task Connect() 245 | { 246 | if (_socket?.State == WebSocketState.Connecting) 247 | { 248 | return false; 249 | } 250 | if (_socket?.State == WebSocketState.Open) 251 | { 252 | return true; 253 | } 254 | 255 | _socket?.Dispose(); 256 | _socket = new ClientWebSocket(); 257 | 258 | try 259 | { 260 | await _socket.ConnectAsync(new Uri(_server), CreateCancellationToken()); 261 | return _socket.State == WebSocketState.Open; 262 | } 263 | catch 264 | { 265 | if (_state == Usb2SnesState.Listening) 266 | { 267 | RestartListener(); 268 | } 269 | return false; 270 | } 271 | } 272 | 273 | private async Task Disconnect() 274 | { 275 | if (_socket?.State == WebSocketState.Open) 276 | { 277 | await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed by client", CreateCancellationToken()); 278 | } 279 | _socket?.Dispose(); 280 | _socket = null; 281 | } 282 | 283 | private async Task SendRequest(string opCode, string space, CancellationToken cancellationToken, string[] operands = null) 284 | { 285 | try 286 | { 287 | if (!await Connect()) 288 | { 289 | return false; 290 | } 291 | var command = new Usb2SnesRequest { Opcode = opCode, Space = space, Operands = operands ?? Array.Empty() }; 292 | var request = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(command)); 293 | await _socket.SendAsync(new ArraySegment(request), WebSocketMessageType.Text, true, CreateCancellationToken()); 294 | return _socket?.State == WebSocketState.Open; 295 | } 296 | catch 297 | { 298 | if (_state == Usb2SnesState.Listening) 299 | { 300 | RestartListener(); 301 | } 302 | return false; 303 | } 304 | } 305 | 306 | private async Task GetResponse() 307 | { 308 | try 309 | { 310 | if (_socket?.State != WebSocketState.Open) 311 | { 312 | return null; 313 | } 314 | var buffer = new byte[RESPONSE_BUFFER_CHUNK]; 315 | var result = await _socket.ReceiveAsync(new ArraySegment(buffer), CreateCancellationToken()); 316 | return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(buffer, 0, result.Count)); 317 | } 318 | catch 319 | { 320 | if (_state == Usb2SnesState.Listening) 321 | { 322 | RestartListener(); 323 | } 324 | return null; 325 | } 326 | } 327 | 328 | private async Task GetBinaryResponse(byte[] buffer) 329 | { 330 | if (_socket?.State == WebSocketState.Open) 331 | { 332 | await _socket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); 333 | } 334 | } 335 | 336 | private CancellationToken CreateCancellationToken(double seconds = 1.0) 337 | { 338 | var source = new CancellationTokenSource(); 339 | source.CancelAfter(TimeSpan.FromSeconds(seconds)); 340 | return source.Token; 341 | } 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /Usb2Snes/Usb2SnesRequest.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer.Usb2Snes 3 | { 4 | public class Usb2SnesRequest 5 | { 6 | public string Opcode { get; set; } 7 | public string Space { get; set; } 8 | public string[] Flags { get; set; } = new string[0]; 9 | public string[] Operands { get; set; } = new string[0]; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Usb2Snes/Usb2SnesResponse.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer.Usb2Snes 3 | { 4 | public class Usb2SnesResponse 5 | { 6 | public string[] Results { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Usb2Snes/Usb2SnesState.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer.Usb2Snes 3 | { 4 | public enum Usb2SnesState 5 | { 6 | Idle, 7 | Listening, 8 | ListeningError 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /VisualizationEngines/PressedVector.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework.Graphics; 2 | 3 | namespace InputVisualizer.VisualizationEngines 4 | { 5 | public class PressedVector 6 | { 7 | public float StartPoint { get; set; } 8 | public float Length { get; set; } 9 | public bool LengthNormalized { get; set; } 10 | public int RectStartPoint { get; set; } 11 | public int RectLength { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /VisualizationEngines/RectangeOrientation.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer.VisualizationEngines 3 | { 4 | public enum RectangeOrientation 5 | { 6 | Right, 7 | Down, 8 | Up 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /VisualizationEngines/RectangleContainer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace InputVisualizer.VisualizationEngines 7 | { 8 | public class RectangleContainer 9 | { 10 | protected const int MIN_DIM_DELAY = 0; 11 | protected const int MAX_DIM_DELAY = 5000; 12 | 13 | public string ButtonName { get; set; } 14 | public string UnmappedButtonName { get; set; } 15 | public bool ButtonIsCurrentlyPressed { get; set; } 16 | public int MaxFrameCount { get; set; } = 0; 17 | public int LastPressFrameCount { get; set; } 18 | public TimeSpan ButtonPressedElapsedTime { get; set; } = TimeSpan.Zero; 19 | public Color Color { get; set; } = Color.PapayaWhip; 20 | public Dictionary PressedVectors { get; set; } = new Dictionary(); 21 | public RectangleContainerState State { get; set; } = RectangleContainerState.None; 22 | public int LastPosition { get; set; } = -1; 23 | public int CurrentPosition { get; set; } = -1; 24 | public bool IsEmptyContainer { get; set; } = false; 25 | public float RepositionRemainingPixels { get; set; } = 0.0f; 26 | public float FadeInAmount { get; set; } = 0.0f; 27 | public float FadeOutAmount { get; set; } = 0.0f; 28 | 29 | public Vector2 ButtonVector = new Vector2(); 30 | public Rectangle OffLineRect = new Rectangle(0, 0, 1, 1); 31 | public Rectangle SquareOuterRect = new Rectangle(0, 0, 13, 13); 32 | public Rectangle SquareInnerRect = new Rectangle(0, 0, 11, 11); 33 | public List DrawRects = new List(); 34 | public List IllegalInputDrawRects = new List(); 35 | public Vector2 InfoVector = new Vector2(); 36 | public Vector2 IllegalInputVector = new Vector2(); 37 | 38 | public bool IsContainerActive(ButtonStateHistory stateHistory, float dimSpeed) 39 | { 40 | var inactive = false; 41 | if (dimSpeed != MAX_DIM_DELAY && !PressedVectors.Any()) 42 | { 43 | inactive = dimSpeed == MIN_DIM_DELAY || (DateTime.Now - stateHistory.LastActiveCompletedTime).TotalMilliseconds > dimSpeed; 44 | } 45 | return !inactive; 46 | } 47 | 48 | public void IncrementRepositioningPixels( int amount ) 49 | { 50 | RepositionRemainingPixels += amount; 51 | State = RepositionRemainingPixels > 0.0f ? RectangleContainerState.Repositioning : RectangleContainerState.Active; 52 | } 53 | 54 | public void DecrementRepositioningPixels() 55 | { 56 | RepositionRemainingPixels--; 57 | State = RepositionRemainingPixels <= 0 ? RectangleContainerState.Active : RectangleContainerState.Repositioning; 58 | } 59 | 60 | public void IncrementFadeIn() 61 | { 62 | FadeInAmount += 0.01f; 63 | if (FadeInAmount >= 1.0f) 64 | { 65 | FadeInAmount = 1.0f; 66 | State = RectangleContainerState.Dim; 67 | } 68 | } 69 | 70 | public float GetDimFactor() 71 | { 72 | return State == RectangleContainerState.Active ? 1.0f : 0.3f; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /VisualizationEngines/RectangleContainerState.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace InputVisualizer.VisualizationEngines 3 | { 4 | public enum RectangleContainerState 5 | { 6 | None, 7 | Active, 8 | Dim, 9 | Repositioning, 10 | FadingIn 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /VisualizationEngines/VisualizerEngine.cs: -------------------------------------------------------------------------------- 1 | using InputVisualizer.Config; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | 5 | namespace InputVisualizer.Layouts 6 | { 7 | public class VisualizerEngine 8 | { 9 | public virtual void Clear(GameState gameState) { } 10 | public virtual void Update(ViewerConfig config, GameState gameState, GameTime gameTime) { } 11 | public virtual void Draw(SpriteBatch spriteBatch, ViewerConfig config, GameState gameState, GameTime gameTime, CommonTextures commonTextures) { } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | true/pm 39 | permonitorv2,permonitor 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /retrospy/ControllerStateBuilder.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Globalization; 8 | 9 | namespace InputVisualizer.RetroSpy 10 | { 11 | public sealed class ControllerStateBuilder 12 | { 13 | private readonly Dictionary _buttons = new(); 14 | private readonly Dictionary _analogs = new(); 15 | private readonly Dictionary _raw_analogs = new(); 16 | private string? _gameboyPrinterData; 17 | public void SetButton(string? name, bool value) 18 | { 19 | if (name == null) 20 | { 21 | throw new ArgumentNullException(nameof(name)); 22 | } 23 | 24 | _buttons[name] = value; 25 | _buttons[name.ToLower(CultureInfo.CurrentUICulture)] = value; 26 | _buttons[name.ToUpper(CultureInfo.CurrentUICulture)] = value; 27 | } 28 | 29 | public void SetAnalog(string? name, float value, int rawValue) 30 | { 31 | if (name != null) 32 | { 33 | _analogs[name] = value; 34 | _raw_analogs[name + "_raw"] = rawValue; 35 | } 36 | } 37 | 38 | public void SetPrinterData(string data) 39 | { 40 | _gameboyPrinterData = data; 41 | } 42 | 43 | public ControllerStateEventArgs Build() 44 | { 45 | return new ControllerStateEventArgs(_buttons, _analogs, _raw_analogs, _gameboyPrinterData); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /retrospy/ControllerStateEventArgs.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace InputVisualizer.RetroSpy 9 | { 10 | public class ControllerStateEventArgs : EventArgs 11 | { 12 | public static readonly ControllerStateEventArgs Zero = new 13 | (new Dictionary(), new Dictionary(), new Dictionary()); 14 | 15 | public string? RawPrinterData { get; } 16 | 17 | public IReadOnlyDictionary Buttons { get; private set; } 18 | public IReadOnlyDictionary Analogs { get; private set; } 19 | public IReadOnlyDictionary RawAnalogs { get; private set; } 20 | 21 | public ControllerStateEventArgs(IReadOnlyDictionary buttons, IReadOnlyDictionary analogs, IReadOnlyDictionary rawAnalogs, string? rawPrinterData = null) 22 | { 23 | RawPrinterData = rawPrinterData; 24 | Buttons = buttons; 25 | Analogs = analogs; 26 | RawAnalogs = rawAnalogs; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /retrospy/IControllerReader.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | 7 | namespace InputVisualizer.RetroSpy 8 | { 9 | public interface IControllerReader 10 | { 11 | event EventHandler ControllerStateChanged; 12 | event EventHandler ControllerDisconnected; 13 | 14 | void Finish(); 15 | void Start(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /retrospy/Sega.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | 7 | namespace InputVisualizer.RetroSpy 8 | { 9 | public static class Sega 10 | { 11 | private const int PACKET_SIZE = 13; 12 | private const int MOUSE_PACKET_SIZE = 24; 13 | 14 | private static readonly string[] BUTTONS = { 15 | "ctrl", "up", "down", "left", "right", "b", "c", "a", "start", "z", "y", "x", "mode" 16 | }; 17 | 18 | private static readonly string[] MOUSE_BUTTONS = { 19 | "left", "right", "middle", "start" 20 | }; 21 | 22 | private static float ReadMouse(bool sign, bool over, byte data) 23 | { 24 | float val = over ? 1.0f : sign ? 0xFF - data : data; 25 | return val * (sign ? -1 : 1) / 255; 26 | } 27 | 28 | public static ControllerStateEventArgs? ReadFromPacket(byte[]? packet) 29 | { 30 | if (packet == null) 31 | { 32 | throw new ArgumentNullException(nameof(packet)); 33 | } 34 | 35 | if (packet.Length != PACKET_SIZE && packet.Length != MOUSE_PACKET_SIZE) 36 | { 37 | return null; 38 | } 39 | 40 | ControllerStateBuilder state = new(); 41 | if (packet.Length == PACKET_SIZE) 42 | { 43 | for (int i = 0; i < BUTTONS.Length; ++i) 44 | { 45 | if (string.IsNullOrEmpty(BUTTONS[i])) 46 | { 47 | continue; 48 | } 49 | 50 | state.SetButton(BUTTONS[i], packet[i] != 0x00); 51 | } 52 | state.SetButton("1", packet[5] != 0); 53 | state.SetButton("2", packet[6] != 0); 54 | } 55 | else if (packet.Length == MOUSE_PACKET_SIZE) 56 | { 57 | for (int i = 0; i < MOUSE_BUTTONS.Length; ++i) 58 | { 59 | if (string.IsNullOrEmpty(MOUSE_BUTTONS[i])) 60 | { 61 | continue; 62 | } 63 | 64 | state.SetButton(MOUSE_BUTTONS[i], packet[i] != 0x00); 65 | } 66 | 67 | bool xSign = packet[4] != 0; 68 | bool ySign = packet[5] != 0; 69 | bool xOver = packet[6] != 0; 70 | bool yOver = packet[7] != 0; 71 | 72 | byte xVal = SignalTool.ReadByteBackwards(packet, 8); 73 | byte yVal = SignalTool.ReadByteBackwards(packet, 16); 74 | 75 | float x = ReadMouse(xSign, xOver, xVal); 76 | float y = ReadMouse(ySign, yOver, yVal); 77 | 78 | SignalTool.SetMouseProperties(x, y, xVal, yVal, state); 79 | } 80 | 81 | return state.Build(); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /retrospy/SerialControllerReader.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | 7 | namespace InputVisualizer.RetroSpy 8 | { 9 | public sealed class SerialControllerReader : IControllerReader, IDisposable 10 | { 11 | public event EventHandler? ControllerStateChanged; 12 | public event EventHandler? ControllerDisconnected; 13 | 14 | private readonly Func _packetParser; 15 | private SerialMonitor? _serialMonitor; 16 | 17 | public SerialControllerReader(string? portName, bool useLagFix, Func packetParser) 18 | { 19 | _packetParser = packetParser; 20 | 21 | _serialMonitor = new SerialMonitor(portName, useLagFix); 22 | _serialMonitor.PacketReceived += SerialMonitor_PacketReceived; 23 | _serialMonitor.Disconnected += SerialMonitor_Disconnected; 24 | } 25 | 26 | public void Start() 27 | { 28 | _serialMonitor.Start(); 29 | } 30 | 31 | private void SerialMonitor_Disconnected(object? sender, EventArgs e) 32 | { 33 | Finish(); 34 | ControllerDisconnected?.Invoke(this, EventArgs.Empty); 35 | } 36 | 37 | private void SerialMonitor_PacketReceived(object? sender, PacketDataEventArgs packet) 38 | { 39 | if (ControllerStateChanged != null) 40 | { 41 | ControllerStateEventArgs? state = _packetParser(packet.GetPacket()); 42 | if (state != null) 43 | { 44 | ControllerStateChanged(this, state); 45 | } 46 | } 47 | } 48 | 49 | public void Finish() 50 | { 51 | if (_serialMonitor != null) 52 | { 53 | _serialMonitor.Stop(); 54 | _serialMonitor.Dispose(); 55 | _serialMonitor = null; 56 | } 57 | } 58 | 59 | private void Dispose(bool disposing) 60 | { 61 | if (disposing) 62 | { 63 | Finish(); 64 | } 65 | } 66 | 67 | public void Dispose() 68 | { 69 | Dispose(true); 70 | GC.SuppressFinalize(this); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /retrospy/SerialMonitor.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.IO.Ports; 9 | using System.Timers; 10 | 11 | namespace InputVisualizer.RetroSpy 12 | { 13 | public class PacketDataEventArgs : EventArgs 14 | { 15 | public PacketDataEventArgs(byte[] packet) 16 | { 17 | Packet = packet; 18 | } 19 | public byte[]? GetPacket() { return Packet; } 20 | 21 | private readonly byte[] Packet; 22 | } 23 | 24 | public class SerialMonitor : IDisposable 25 | { 26 | private const int BAUD_RATE = 115200; 27 | private const int TIMER_MS = 1; 28 | 29 | public event EventHandler? PacketReceived; 30 | public event EventHandler? Disconnected; 31 | 32 | private SerialPort? _datPort; 33 | private readonly List _localBuffer; 34 | private Timer? _timer; 35 | 36 | public SerialMonitor(string? portName, bool useLagFix, bool printerMode = false) 37 | { 38 | _localBuffer = new List(); 39 | _datPort = new SerialPort(portName != null ? portName.Split(' ')[0] : "", useLagFix ? 57600 : BAUD_RATE) 40 | { 41 | Handshake = Handshake.RequestToSend, 42 | DtrEnable = true 43 | }; 44 | } 45 | 46 | public void Start() 47 | { 48 | if (_timer != null) 49 | { 50 | return; 51 | } 52 | 53 | _localBuffer.Clear(); 54 | _datPort?.Open(); 55 | 56 | _timer = new Timer(TIMER_MS); 57 | _timer.Elapsed += Tick; 58 | _timer.AutoReset = true; 59 | _timer.Start(); 60 | } 61 | 62 | public void Stop() 63 | { 64 | if (_datPort != null) 65 | { 66 | try 67 | { 68 | _datPort.Close(); 69 | } 70 | catch (IOException) { } 71 | _datPort.Dispose(); 72 | _datPort = null; 73 | } 74 | if (_timer != null) 75 | { 76 | _timer.Stop(); 77 | _timer = null; 78 | } 79 | } 80 | 81 | private void Tick(object sender, ElapsedEventArgs e) 82 | { 83 | if (_datPort == null || !_datPort.IsOpen || PacketReceived == null) 84 | { 85 | return; 86 | } 87 | 88 | try 89 | { 90 | int readCount = _datPort.BytesToRead; 91 | byte[] readBuffer = new byte[readCount]; 92 | _ = _datPort.Read(readBuffer, 0, readCount); 93 | _localBuffer.AddRange(readBuffer); 94 | } 95 | catch (IOException) 96 | { 97 | Stop(); 98 | Disconnected?.Invoke(this, EventArgs.Empty); 99 | return; 100 | } 101 | catch (OverflowException) 102 | { 103 | Stop(); 104 | Disconnected?.Invoke(this, EventArgs.Empty); 105 | return; 106 | } 107 | 108 | int lastSplitIndex = _localBuffer.LastIndexOf(0x0A); 109 | if (lastSplitIndex <= 1) 110 | { 111 | return; 112 | } 113 | 114 | int sndLastSplitIndex = _localBuffer.LastIndexOf(0x0A, lastSplitIndex - 1); 115 | if (lastSplitIndex == -1) 116 | { 117 | return; 118 | } 119 | 120 | int packetStart = sndLastSplitIndex + 1; 121 | int packetSize = lastSplitIndex - packetStart; 122 | 123 | PacketReceived(this, new PacketDataEventArgs(_localBuffer.GetRange(packetStart, packetSize).ToArray())); 124 | 125 | _localBuffer.RemoveRange(0, lastSplitIndex); 126 | } 127 | 128 | protected virtual void Dispose(bool disposing) 129 | { 130 | if (disposing) 131 | { 132 | Stop(); 133 | } 134 | } 135 | 136 | public void Dispose() 137 | { 138 | Dispose(true); 139 | GC.SuppressFinalize(this); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /retrospy/SignalTool.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace InputVisualizer.RetroSpy 9 | { 10 | internal static class SignalTool 11 | { 12 | /// 13 | /// Reads a byte of data from a string of 8 bits in a controller data packet. 14 | /// 15 | public static byte ReadByte(byte[] packet, int offset, byte numBits = 8, byte mask = 0x0F) 16 | { 17 | byte val = 0; 18 | for (int i = 0; i < numBits; ++i) 19 | { 20 | if ((packet[i + offset] & mask) != 0) 21 | { 22 | val |= (byte)(1 << (numBits - 1 - i)); 23 | } 24 | } 25 | return val; 26 | } 27 | 28 | public static byte ReadByteBackwards(byte[] packet, int offset, byte numBits = 8, byte mask = 0x0F) 29 | { 30 | byte val = 0; 31 | for (int i = 0; i < numBits; ++i) 32 | { 33 | if ((packet[i + offset] & mask) != 0) 34 | { 35 | val |= (byte)(1 << i); 36 | } 37 | } 38 | return val; 39 | } 40 | 41 | private static float MiddleOfThree(float a, float b, float c) 42 | { 43 | // Compare each three number to find middle 44 | // number. Enter only if a > b 45 | if (a > b) 46 | { 47 | return b > c ? b : a > c ? c : a; 48 | } 49 | else 50 | { 51 | // Decided a is not greater than b. 52 | return a > c ? a : b > c ? c : b; 53 | } 54 | } 55 | 56 | private class SlidingWindow 57 | { 58 | public SlidingWindow() 59 | { 60 | windowX = new float[3]; 61 | windowPositionX = 0; 62 | windowY = new float[3]; 63 | windowPositionY = 0; 64 | } 65 | 66 | public float[] windowX; 67 | public int windowPositionX; 68 | public float[] windowY; 69 | public int windowPositionY; 70 | } 71 | 72 | private static readonly Dictionary windows = new(); 73 | 74 | public static void SetMouseProperties(float x, float y, int xRaw, int yRaw, ControllerStateBuilder state, float maxCircleSize = 1.0f) 75 | { 76 | if (!windows.ContainsKey("")) 77 | { 78 | windows[""] = new SlidingWindow(); 79 | } 80 | 81 | SetMouseProperties(x, y, xRaw, yRaw, state, maxCircleSize, windows[""], ""); 82 | } 83 | 84 | public static void SetPCMouseProperties(float x, float y, int xRaw, int yRaw, ControllerStateBuilder state, float maxCircleSize = 1.0f) 85 | { 86 | if (!windows.ContainsKey("PC_")) 87 | { 88 | windows["PC_"] = new SlidingWindow(); 89 | } 90 | 91 | SetMouseProperties(x, y, xRaw, yRaw, state, maxCircleSize, windows["PC_"], "PC_"); 92 | } 93 | 94 | private static void SetMouseProperties(float x, float y, int xRaw, int yRaw, ControllerStateBuilder state, 95 | float maxCircleSize, SlidingWindow window, string prefix) 96 | { 97 | window.windowX[window.windowPositionX] = x; 98 | window.windowPositionX += 1; 99 | window.windowPositionX %= 3; 100 | 101 | window.windowY[window.windowPositionY] = y; 102 | window.windowPositionY += 1; 103 | window.windowPositionY %= 3; 104 | 105 | y = MiddleOfThree(window.windowY[0], window.windowY[1], window.windowY[2]); 106 | x = MiddleOfThree(window.windowX[0], window.windowX[1], window.windowX[2]); 107 | 108 | float y1 = y; 109 | float x1 = x; 110 | 111 | if (y != 0 || x != 0) 112 | { 113 | // Direction shows around the unit circle 114 | double radian = Math.Atan2(y, x); 115 | x1 = maxCircleSize * (float)Math.Cos(radian); 116 | y1 = maxCircleSize * (float)Math.Sin(radian); 117 | 118 | // Don't let magnitude exceed the unit circle 119 | if (Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2)) > maxCircleSize) 120 | { 121 | x = x1; 122 | y = y1; 123 | } 124 | } 125 | 126 | state.SetAnalog(prefix + "mouse_center_x", 0, 0); 127 | state.SetAnalog(prefix + "mouse_center_y", 0, 0); 128 | state.SetAnalog(prefix + "mouse_direction_x", x1, xRaw); 129 | state.SetAnalog(prefix + "mouse_direction_y", y1, yRaw); 130 | state.SetAnalog(prefix + "mouse_magnitude_x", x, xRaw); 131 | state.SetAnalog(prefix + "mouse_magnitude_y", y, yRaw); 132 | } 133 | 134 | public static void FakeAnalogStick(byte up, byte down, byte left, byte right, ControllerStateBuilder state, string xName, string yName) 135 | { 136 | float x = 0; 137 | float y = 0; 138 | 139 | if (right != 0x00) 140 | { 141 | x = 1; 142 | } 143 | else if (left != 0x00) 144 | { 145 | x = -1; 146 | } 147 | 148 | if (up != 0x00) 149 | { 150 | y = 1; 151 | } 152 | else if (down != 0x00) 153 | { 154 | y = -1; 155 | } 156 | 157 | if (y != 0 || x != 0) 158 | { 159 | // point on the unit circle at the same angle 160 | double radian = Math.Atan2(y, x); 161 | float x1 = (float)Math.Cos(radian); 162 | float y1 = (float)Math.Sin(radian); 163 | 164 | // Don't let magnitude exceed the unit circle 165 | if (Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2)) > 1.0) 166 | { 167 | x = x1; 168 | y = y1; 169 | } 170 | } 171 | 172 | state.SetAnalog(xName, x, 0); 173 | state.SetAnalog(yName, y, 0); 174 | } 175 | 176 | public static void GenerateFakeStick(ControllerStateBuilder state, string xname, string yname, bool up, bool down, bool left, bool right) 177 | { 178 | float x = 0; 179 | float y = 0; 180 | 181 | if (right) 182 | { 183 | x = 1; 184 | } 185 | else if (left) 186 | { 187 | x = -1; 188 | } 189 | 190 | if (up) 191 | { 192 | y = 1; 193 | } 194 | else if (down) 195 | { 196 | y = -1; 197 | } 198 | 199 | if (y != 0 || x != 0) 200 | { 201 | // point on the unit circle at the same angle 202 | double radian = Math.Atan2(y, x); 203 | float x1 = (float)Math.Cos(radian); 204 | float y1 = (float)Math.Sin(radian); 205 | 206 | // Don't let magnitude exceed the unit circle 207 | if (Math.Sqrt(Math.Pow(x, 2) + Math.Pow(y, 2)) > 1.0) 208 | { 209 | x = x1; 210 | y = y1; 211 | } 212 | } 213 | 214 | state.SetAnalog(xname, x, 0); 215 | state.SetAnalog(yname, y, 0); 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /retrospy/SuperNESandNES.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) RetroSpy Technologies 3 | */ 4 | 5 | using System; 6 | 7 | namespace InputVisualizer.RetroSpy 8 | { 9 | public static class SuperNESandNES 10 | { 11 | private static ControllerStateEventArgs? ReadPacketButtons(byte[] packet, string?[] buttons, byte mask = 0xFF) 12 | { 13 | if (packet == null || packet.Length < buttons.Length) 14 | { 15 | return null; 16 | } 17 | 18 | ControllerStateBuilder state = new(); 19 | 20 | for (int i = 0; i < buttons.Length; ++i) 21 | { 22 | if (string.IsNullOrEmpty(buttons[i])) 23 | { 24 | continue; 25 | } 26 | 27 | state.SetButton(buttons[i], (packet[i] & mask) != 0x00); 28 | } 29 | 30 | return state.Build(); 31 | } 32 | 33 | private static ControllerStateEventArgs? ReadPacketButtons_ascii(byte[]? packet, string?[] buttons) 34 | { 35 | if (packet == null || packet.Length < buttons.Length) 36 | { 37 | return null; 38 | } 39 | 40 | ControllerStateBuilder state = new(); 41 | 42 | for (int i = 0; i < buttons.Length; ++i) 43 | { 44 | if (string.IsNullOrEmpty(buttons[i])) 45 | { 46 | continue; 47 | } 48 | 49 | state.SetButton(buttons[i], packet[i] != '0'); 50 | } 51 | 52 | return state.Build(); 53 | } 54 | 55 | private static readonly string?[] BUTTONS_NES = { 56 | "a", "b", "select", "start", "up", "down", "left", "right", "2", "1", "5", "9", "6", "10", "11", "7", "4", "3", "12", "8", null, null, null, null 57 | }; 58 | 59 | private static readonly string?[] BUTTONS_NES_BACKCOMPAT = { 60 | "a", "b", "select", "start", "up", "down", "left", "right" 61 | }; 62 | 63 | private static readonly string?[] BUTTONS_NES_POWERGLOVE = { 64 | "a", "b", "select", "start", "up", "down", "left", "right", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0" ,"enter", "center" 65 | }; 66 | 67 | private static readonly string?[] BUTTONS_NES_POWERGLOVE_FINGERS = { 68 | "thumb", "index", "middle", "ring" 69 | }; 70 | 71 | private static readonly string?[] BUTTONS_SNES = { 72 | "b", "y", "select", "start", "up", "down", "left", "right", "a", "x", "l", "r", null, null, null, null 73 | }; 74 | 75 | private static readonly string?[] BUTTONS_SNES_NTTDATA = { 76 | "b", "y", "select", "start", "up", "down", "left", "right", "a", "x", "l", "r", null, null, null, null, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "star", "pound", "dot", "c", null, "end" 77 | }; 78 | 79 | private static readonly string?[] BUTTONS_INTELLIVISION = { 80 | "n", "nne", "ne", "ene", "e", "ese", "se", "sse", "s", "ssw", "sw", "wsw", "w", "wnw", "nw", "nnw", "1", "2", "3", "4", "5", "6", "7", "8", "9", "clear", "0", "enter", "topleft", "topright", "bottomleft", "bottomright" 81 | }; 82 | 83 | private static readonly string?[] BUTTONS_PSCLASSIC = 84 | { 85 | "r1", "l1", "r2", "l2", "square", "x", "circle", "triangle", null, null, "down", "up", "right", "left", "start", "select" 86 | }; 87 | 88 | private static readonly string?[] BUTTONS_ATARI5200 = 89 | { 90 | "start", "pause", "reset", "1", "2", "3", "4", "5", "6", "7", "8", "9", "star", "0", "pound", "trigger", "fire", null, null 91 | }; 92 | 93 | private static readonly string?[] BUTTONS_FMTOWNS = 94 | { 95 | "up", "down", "left", "right", null, "a", "b", null, null, "select", "run" 96 | }; 97 | 98 | private static readonly string?[] BUTTONS_PCFX = 99 | { 100 | null, "1", "2", "3", "4", "5", "6", "select", "run", "up", "right", "down", "left", "mode1", null, "mode2" 101 | }; 102 | 103 | private static readonly string?[] BUTTONS_VIRTUALBOY = 104 | { 105 | "r_down", "r_left", "select", "start", "up", "down", "left", "right", "r_right", "r_up", "lt", "rt", "b", "a", null, null 106 | }; 107 | 108 | private static readonly string?[] BUTTONS_NUON = 109 | { 110 | "CD", "A", "start", "nuon", "down", "left", "up", "right", null, null, "LT", "RT", "B", "CL", "CU", "CR" 111 | }; 112 | 113 | public static ControllerStateEventArgs? ReadFromPacketIntellivision(byte[]? packet) 114 | { 115 | return packet == null ? throw new ArgumentNullException("ReadFromPacketIntellivision(packet)") : ReadPacketButtons(packet, BUTTONS_INTELLIVISION); 116 | } 117 | 118 | public static ControllerStateEventArgs? ReadFromPacketNES(byte[]? packet) 119 | { 120 | return packet == null 121 | ? throw new ArgumentNullException(nameof(packet)) 122 | : packet.Length == 80 123 | ? ProcessPowerGlove(packet) 124 | : ReadPacketButtons(packet, packet.Length == 8 ? BUTTONS_NES_BACKCOMPAT : BUTTONS_NES); 125 | } 126 | 127 | public static ControllerStateEventArgs ProcessPowerGlove(byte[] packet) 128 | { 129 | if (packet == null) 130 | { 131 | throw new ArgumentNullException(nameof(packet)); 132 | } 133 | 134 | ControllerStateBuilder state = new(); 135 | 136 | sbyte x = (sbyte)SignalTool.ReadByte(packet, 8, 8); 137 | state.SetAnalog("x", x / 128.0f, x); 138 | 139 | sbyte y = (sbyte)SignalTool.ReadByte(packet, 16, 8); 140 | state.SetAnalog("y", y / 128.0f, y); 141 | 142 | sbyte z = (sbyte)SignalTool.ReadByte(packet, 24, 8); 143 | state.SetAnalog("z", z / 128.0f, z); 144 | 145 | byte rotation = SignalTool.ReadByte(packet, 32, 8); 146 | state.SetAnalog("rotation", rotation / 11.0f, rotation); 147 | 148 | byte[] fingers = new byte[4]; 149 | for (int i = 0; i < 4; ++i) 150 | { 151 | fingers[i] = 0; 152 | for (int j = 0; j < 2; ++j) 153 | { 154 | fingers[i] |= (byte)(packet[40 + (i * 2) + j] == 0 ? 0 : (1 << j)); 155 | } 156 | } 157 | for (int i = 0; i < 4; ++i) 158 | { 159 | state.SetAnalog(BUTTONS_NES_POWERGLOVE_FINGERS[i], fingers[i] / 4.0f, fingers[i]); 160 | } 161 | 162 | byte buttons = SignalTool.ReadByte(packet, 48, 8); 163 | for (int i = 0; i < BUTTONS_NES_POWERGLOVE.Length; ++i) 164 | { 165 | state.SetButton(BUTTONS_NES_POWERGLOVE[i], false); 166 | } 167 | switch (buttons) 168 | { 169 | case 0x00: 170 | state.SetButton("0", true); 171 | state.SetButton("center", true); 172 | break; 173 | case 0x01: 174 | state.SetButton("1", true); 175 | break; 176 | case 0x02: 177 | state.SetButton("2", true); 178 | break; 179 | case 0x03: 180 | state.SetButton("3", true); 181 | break; 182 | case 0x04: 183 | state.SetButton("4", true); 184 | break; 185 | case 0x05: 186 | state.SetButton("5", true); 187 | break; 188 | case 0x06: 189 | state.SetButton("6", true); 190 | break; 191 | case 0x07: 192 | state.SetButton("7", true); 193 | break; 194 | case 0x08: 195 | state.SetButton("8", true); 196 | break; 197 | case 0x09: 198 | state.SetButton("9", true); 199 | break; 200 | case 0x0A: 201 | state.SetButton("a", true); 202 | break; 203 | case 0x0B: 204 | state.SetButton("b", true); 205 | break; 206 | case 0x0C: 207 | state.SetButton("left", true); 208 | break; 209 | case 0x0D: 210 | state.SetButton("up", true); 211 | break; 212 | case 0x0E: 213 | state.SetButton("down", true); 214 | break; 215 | case 0x0F: 216 | state.SetButton("right", true); 217 | break; 218 | case 0x80: 219 | state.SetButton("enter", true); 220 | break; 221 | case 0x82: 222 | state.SetButton("start", true); 223 | break; 224 | case 0x83: 225 | state.SetButton("select", true); 226 | break; 227 | default: 228 | break; 229 | } 230 | 231 | return state.Build(); 232 | } 233 | 234 | public static ControllerStateEventArgs? ReadFromPacketVB(byte[]? packet) 235 | { 236 | return packet == null ? throw new ArgumentNullException(nameof(packet)) : ReadPacketButtons(packet, BUTTONS_VIRTUALBOY); 237 | } 238 | 239 | public static ControllerStateEventArgs? ReadFromPacketNuon(byte[]? packet) 240 | { 241 | return packet == null ? throw new ArgumentNullException(nameof(packet)) : ReadPacketButtons(packet, BUTTONS_NUON, 0x04); 242 | } 243 | 244 | public static ControllerStateEventArgs? ReadFromPacketPCFX(byte[]? packet) 245 | { 246 | return packet == null 247 | ? throw new ArgumentNullException(nameof(packet)) 248 | : packet.Length != BUTTONS_PCFX.Length ? null : ReadPacketButtons(packet, BUTTONS_PCFX); 249 | } 250 | 251 | public static ControllerStateEventArgs? ReadFromPacketPSClassic(byte[]? packet) 252 | { 253 | return packet == null ? throw new ArgumentNullException(nameof(packet)) : ReadPacketButtons_ascii(packet, BUTTONS_PSCLASSIC); 254 | } 255 | 256 | public static ControllerStateEventArgs? ReadFromPacketSNES(byte[]? packet) 257 | { 258 | if (packet == null) 259 | { 260 | throw new ArgumentNullException(nameof(packet)); 261 | } 262 | 263 | if (packet.Length < BUTTONS_SNES.Length) 264 | { 265 | return null; 266 | } 267 | 268 | ControllerStateBuilder state = new(); 269 | 270 | if (packet.Length == 32 && packet[15] == 0x00 && packet[13] != 0x00) 271 | { 272 | for (int i = 0; i < BUTTONS_SNES_NTTDATA.Length; ++i) 273 | { 274 | if (string.IsNullOrEmpty(BUTTONS_SNES_NTTDATA[i])) 275 | { 276 | continue; 277 | } 278 | 279 | state.SetButton(BUTTONS_SNES_NTTDATA[i], packet[i] != 0x00); 280 | } 281 | } 282 | else 283 | { 284 | for (int i = 0; i < BUTTONS_SNES.Length; ++i) 285 | { 286 | if (string.IsNullOrEmpty(BUTTONS_SNES[i])) 287 | { 288 | continue; 289 | } 290 | 291 | state.SetButton(BUTTONS_SNES[i], packet[i] != 0x00); 292 | } 293 | 294 | if (packet.Length == 32 && packet[15] != 0x00) 295 | { 296 | float y = (float)(SignalTool.ReadByte(packet, 17, 7, 0x1) * ((packet[16] & 0x1) != 0 ? 1 : -1)) / 127; 297 | float x = (float)(SignalTool.ReadByte(packet, 25, 7, 0x1) * ((packet[24] & 0x1) != 0 ? -1 : 1)) / 127; 298 | SignalTool.SetMouseProperties(x, y, SignalTool.ReadByte(packet, 25, 7, 0x1) * ((packet[24] & 0x1) != 0 ? -1 : 1), SignalTool.ReadByte(packet, 17, 7, 0x1) * ((packet[16] & 0x1) != 0 ? 1 : -1), state); 299 | } 300 | } 301 | 302 | return state.Build(); 303 | } 304 | 305 | public static ControllerStateEventArgs? ReadFromPacketJaguar(byte[]? packet) 306 | { 307 | if (packet == null) 308 | { 309 | throw new ArgumentNullException(nameof(packet)); 310 | } 311 | 312 | if (packet.Length < 4) 313 | { 314 | return null; 315 | } 316 | 317 | ControllerStateBuilder state = new(); 318 | 319 | state.SetButton("pause", (packet[0] & 0b00000100) == 0x00); 320 | state.SetButton("a", (packet[0] & 0b00001000) == 0x00); 321 | state.SetButton("right", (packet[0] & 0b00010000) == 0x00); 322 | state.SetButton("left", (packet[0] & 0b00100000) == 0x00); 323 | state.SetButton("down", (packet[0] & 0b01000000) == 0x00); 324 | state.SetButton("up", (packet[0] & 0b10000000) == 0x00); 325 | 326 | state.SetButton("b", (packet[1] & 0b00001000) == 0x00); 327 | state.SetButton("1", (packet[1] & 0b00010000) == 0x00); 328 | state.SetButton("4", (packet[1] & 0b00100000) == 0x00); 329 | state.SetButton("l", (packet[1] & 0b00100000) == 0x00); 330 | state.SetButton("7", (packet[1] & 0b01000000) == 0x00); 331 | state.SetButton("x", (packet[1] & 0b01000000) == 0x00); 332 | state.SetButton("star", (packet[1] & 0b10000000) == 0x00); 333 | 334 | state.SetButton("c", (packet[2] & 0b00001000) == 0x00); 335 | state.SetButton("2", (packet[2] & 0b00010000) == 0x00); 336 | state.SetButton("5", (packet[2] & 0b00100000) == 0x00); 337 | state.SetButton("8", (packet[2] & 0b01000000) == 0x00); 338 | state.SetButton("y", (packet[2] & 0b01000000) == 0x00); 339 | state.SetButton("0", (packet[2] & 0b10000000) == 0x00); 340 | 341 | state.SetButton("option", (packet[3] & 0b00001000) == 0x00); 342 | state.SetButton("3", (packet[3] & 0b00010000) == 0x00); 343 | state.SetButton("6", (packet[3] & 0b00100000) == 0x00); 344 | state.SetButton("r", (packet[3] & 0b00100000) == 0x00); 345 | state.SetButton("9", (packet[3] & 0b01000000) == 0x00); 346 | state.SetButton("z", (packet[3] & 0b01000000) == 0x00); 347 | state.SetButton("pound", (packet[3] & 0b10000000) == 0x00); 348 | 349 | return state.Build(); 350 | } 351 | 352 | private static readonly string?[] SCANCODES_FMTOWNS = 353 | { 354 | null, "ESC", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "^", "yen", "Backspace", 355 | "Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "@", "[", "Enter", "A", "S", 356 | "D", "F", "G", "H", "J", "K", "L", ";", ":", "]", "Z", "X", "C", "V", "B", "N", 357 | "M", ",", ".", "/", "quote", "Spacebar", "*", "divide", "+", "subtract", "Num7", "Num8", "Num9", "=", "Num4", "Num5", 358 | "Num6", null, "Num1", "Num2", "Num3", "NumEnter", "Num0", "Num.", "DUP", null, "000", "EL", null, "Up", "Home", "Left", 359 | "Down", "Right", "CTRL", "SHIFT", null, "CAP", "BottomLeft", "BelowSpace1", "BelowSpace2", "RightOfSpacebar1", 360 | "RightOfSpacebar2", "PF12", "ALT", "PF1", "PF2", "PF3", 361 | "PF4", "PF5", "PF6", "PF7", "PF8", "PF9", "PF10", null, null, "PF11", null, "UpperLeftOfHome", "AboveHome", "UpperRightOfHome", "LeftOfHome", null, 362 | "RightOfHome", "LowerLeftOfHome", "RightOfSpacebar3", "BelowArrows", "PF13", "PF14", "PF15", "PF16", "PF17", "PF18", "PF19", "PF20", "Pause", 363 | "Copy", null, null, 364 | null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 365 | null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 366 | null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 367 | null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 368 | null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, 369 | null, null, null, null, null, null, null, null, null, null, null, null, null, "SYSREQ", null, null, 370 | "ScrollLock", "SysHome", "End", null, null, null, null, null, null, null, null, null, null, null, null, null, 371 | null, null, null, null, "EXT1", null, null, null, "EXT2", null, null, null, null, null, null, null 372 | }; 373 | 374 | public static ControllerStateEventArgs? ReadFromPacketFMTowns(byte[]? packet) 375 | { 376 | if (packet == null) 377 | { 378 | throw new ArgumentNullException(nameof(packet)); 379 | } 380 | 381 | if (packet.Length != 9 && packet.Length != 70) 382 | { 383 | return null; 384 | } 385 | 386 | if (packet.Length == 9) 387 | { 388 | byte[] polishedPacket = new byte[BUTTONS_FMTOWNS.Length]; 389 | 390 | if (packet[0] != 0 && packet[1] != 0) 391 | { 392 | packet[0] = packet[1] = 0; 393 | polishedPacket[9] = 1; 394 | } 395 | 396 | if (packet[2] != 0 && packet[3] != 0) 397 | { 398 | packet[2] = packet[3] = 0; 399 | polishedPacket[10] = 1; 400 | } 401 | 402 | for (int i = 0; i < packet.Length; ++i) 403 | { 404 | polishedPacket[i] = packet[i]; 405 | } 406 | 407 | return ReadPacketButtons(polishedPacket, BUTTONS_FMTOWNS); 408 | } 409 | else 410 | { 411 | int j = 0; 412 | byte[] reconstructedPacket = new byte[34]; 413 | for (int i = 0; i < 34; ++i) 414 | { 415 | reconstructedPacket[i] = (byte)((packet[j] >> 4) | packet[j + 1]); 416 | j += 2; 417 | } 418 | 419 | byte[] polishedPacket = new byte[256]; 420 | 421 | for (int i = 0; i < 32; ++i) 422 | { 423 | for (int k = 0; k < 8; ++k) 424 | { 425 | polishedPacket[(i * 8) + k] = (byte)((reconstructedPacket[i] & (1 << k)) != 0 ? 1 : 0); 426 | } 427 | } 428 | 429 | ControllerStateBuilder state = new(); 430 | 431 | for (int i = 0; i < SCANCODES_FMTOWNS.Length; ++i) 432 | { 433 | if (string.IsNullOrEmpty(SCANCODES_FMTOWNS[i])) 434 | { 435 | continue; 436 | } 437 | 438 | state.SetButton(SCANCODES_FMTOWNS[i], polishedPacket[i] != 0x00); 439 | } 440 | 441 | SignalTool.SetMouseProperties(((sbyte)reconstructedPacket[33]) / -128.0f, ((sbyte)reconstructedPacket[32]) / 128.0f, reconstructedPacket[33], reconstructedPacket[32], state); 442 | 443 | state.SetButton("left", packet[68] == 1); 444 | state.SetButton("right", packet[69] == 1); 445 | 446 | return state.Build(); 447 | } 448 | } 449 | 450 | private static float atari5200_y; 451 | private static int atari5200_yRaw; 452 | 453 | public static ControllerStateEventArgs? ReadFromPacketAtari52002(byte[]? packet) 454 | { 455 | if (packet == null) 456 | { 457 | throw new ArgumentNullException(nameof(packet)); 458 | } 459 | 460 | if (packet.Length != BUTTONS_ATARI5200.Length) 461 | { 462 | return null; 463 | } 464 | 465 | atari5200_y = (((packet[17] >> 4) | (packet[18])) - 128.0f) / 128.0f; 466 | atari5200_yRaw = (packet[17] >> 4) | (packet[18]); 467 | return null; 468 | } 469 | 470 | public static ControllerStateEventArgs? ReadFromPacketAtari52001(byte[]? packet) 471 | { 472 | if (packet == null) 473 | { 474 | throw new ArgumentNullException(nameof(packet)); 475 | } 476 | 477 | if (packet.Length != BUTTONS_ATARI5200.Length) 478 | { 479 | return null; 480 | } 481 | 482 | ControllerStateBuilder state = new(); 483 | for (int i = 0; i < BUTTONS_ATARI5200.Length; ++i) 484 | { 485 | if (string.IsNullOrEmpty(BUTTONS_ATARI5200[i])) 486 | { 487 | continue; 488 | } 489 | 490 | state.SetButton(BUTTONS_ATARI5200[i], packet[i] == 0x00); 491 | } 492 | 493 | state.SetAnalog("x", (((packet[17] >> 4) | (packet[18])) - 128.0f) / -128.0f, (packet[17] >> 4) | (packet[18])); 494 | state.SetAnalog("y", atari5200_y, atari5200_yRaw); 495 | 496 | return state.Build(); 497 | } 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /usb2snesGameList.json: -------------------------------------------------------------------------------- 1 | { 2 | "games": [ 3 | { 4 | "name": "Default Game", 5 | "address": [ "F90718" ] 6 | }, 7 | { 8 | "name": "CV 4/Dracula X", 9 | "address": [ "F50020" ] 10 | }, 11 | { 12 | "name": "Demons Blazon", 13 | "address": [ "F50090", "F50093" ] 14 | }, 15 | { 16 | "name": "Donkey Kong Country", 17 | "address": [ "F50500" ] 18 | }, 19 | { 20 | "name": "Secret of Evermore", 21 | "address": [ "F50EC6" ] 22 | }, 23 | { 24 | "name": "SOS", 25 | "address": [ "F501C1", "F501C0" ] 26 | }, 27 | { 28 | "name": "Soul Blazer", 29 | "address": [ "F50322" ] 30 | }, 31 | { 32 | "name": "Super Metroid", 33 | "address": [ "F5008B" ] 34 | }, 35 | { 36 | "name": "Super Mario World", 37 | "address": [ "F50DA4", "F50DA2" ] 38 | }, 39 | { 40 | "name": "The Legend of Zelda", 41 | "address": [ "F500F2", "F500F0" ] 42 | }, 43 | { 44 | "name": "Yoshi's Island", 45 | "address": [ "F50035" ] 46 | } 47 | ] 48 | } --------------------------------------------------------------------------------