#region License // // The Open Toolkit Library License // // Copyright (c) 2006 - 2008 the Open Toolkit library, except where noted. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; using System.Security; using System.Text; using Microsoft.Win32; using OpenTK.Input; namespace OpenTK.Platform.Windows { sealed class WinMMJoystick : IJoystickDriver, IJoystickDriver2 { #region Fields readonly object sync = new object(); List sticks = new List(); IList sticks_readonly; // Matches a WinMM device index to a specific stick above readonly Dictionary index_to_stick = new Dictionary(); // Matches a player index to a WinMM device index readonly Dictionary player_to_index = new Dictionary(); // Todo: Read the joystick name from the registry. //static readonly string RegistryJoyConfig = @"Joystick%dConfiguration"; //static readonly string RegistryJoyName = @"Joystick%dOEMName"; //static readonly string RegstryJoyCurrent = @"CurrentJoystickSettings"; bool disposed; #endregion #region Constructors public WinMMJoystick() { sticks_readonly = sticks.AsReadOnly(); RefreshDevices(); } #endregion #region Internal Members internal void RefreshDevices() { for (int i = 0; i < sticks.Count; i++) { CloseJoystick(i); } // WinMM supports up to 16 joysticks. for (int i = 0; i < UnsafeNativeMethods.joyGetNumDevs(); i++) { OpenJoystick(i); } } #endregion #region Private Members JoystickDevice OpenJoystick(int number) { lock (sync) { JoystickDevice stick = null; JoyCaps caps; JoystickError result = UnsafeNativeMethods.joyGetDevCaps(number, out caps, JoyCaps.SizeInBytes); if (result == JoystickError.NoError) { if (caps.NumAxes > JoystickState.MaxAxes) { Debug.Print("[Input] Device has {0} axes, which is higher than OpenTK maximum {1}. Please report a bug at http://www.opentk.com", caps.NumAxes, JoystickState.MaxAxes); caps.NumAxes = JoystickState.MaxAxes; } if (caps.NumAxes > JoystickState.MaxButtons) { Debug.Print("[Input] Device has {0} buttons, which is higher than OpenTK maximum {1}. Please report a bug at http://www.opentk.com", caps.NumButtons, JoystickState.MaxButtons); caps.NumButtons = JoystickState.MaxButtons; } JoystickCapabilities joycaps = new JoystickCapabilities( caps.NumAxes, caps.NumButtons, (caps.Capabilities & JoystCapsFlags.HasPov) != 0 ? 1 : 0, true); int num_axes = caps.NumAxes; if ((caps.Capabilities & JoystCapsFlags.HasPov) != 0) num_axes += 2; stick = new JoystickDevice(number, num_axes, caps.NumButtons); stick.Details = new WinMMJoyDetails(joycaps); // Make sure to reverse the vertical axes, so that +1 points up and -1 points down. for (int axis = 0; axis < caps.NumAxes; axis++) { stick.Details.Min[axis] = caps.GetMin(axis); stick.Details.Max[axis] = caps.GetMax(axis); } if ((caps.Capabilities & JoystCapsFlags.HasPov) != 0) { stick.Details.PovType = PovType.Exists; if ((caps.Capabilities & JoystCapsFlags.HasPov4Dir) != 0) stick.Details.PovType |= PovType.Discrete; if ((caps.Capabilities & JoystCapsFlags.HasPovContinuous) != 0) stick.Details.PovType |= PovType.Continuous; } // Todo: Implement joystick name detection for WinMM. stick.Description = String.Format("Joystick/Joystick #{0} ({1} axes, {2} buttons)", number, stick.Axis.Count, stick.Button.Count); // Todo: Try to get the device name from the registry. Oh joy! //string key_path = String.Format("{0}\\{1}\\{2}", RegistryJoyConfig, caps.RegKey, RegstryJoyCurrent); //RegistryKey key = Registry.LocalMachine.OpenSubKey(key_path, false); //if (key == null) // key = Registry.CurrentUser.OpenSubKey(key_path, false); //if (key == null) // stick.Description = String.Format("USB Joystick {0} ({1} axes, {2} buttons)", number, stick.Axis.Count, stick.Button.Count); //else //{ // key.Close(); //} Debug.Print("Found joystick on device number {0}", number); index_to_stick.Add(number, sticks.Count); player_to_index.Add(player_to_index.Count, number); sticks.Add(stick); } return stick; } } void UnplugJoystick(int player_index) { // Reset the system configuration. Without this, // joysticks that are reconnected on different // ports are given different ids, making it // impossible to reconnect a disconnected user. UnsafeNativeMethods.joyConfigChanged(0); Debug.Print("[Win] WinMM joystick {0} unplugged", player_index); CloseJoystick(player_index); } void CloseJoystick(int player_index) { lock (sync) { if (IsValid(player_index)) { int device_index = player_to_index[player_index]; JoystickDevice stick = sticks[index_to_stick[device_index]] as JoystickDevice; if (stick != null) { index_to_stick.Remove(device_index); player_to_index.Remove(player_index); } } } } bool IsValid(int player_index) { if (player_to_index.ContainsKey(player_index)) { return true; } //else if (index >= 0 && index < UnsafeNativeMethods.joyGetNumDevs()) //{ // return OpenJoystick(index) != null; //} return false; } static short CalculateOffset(int pos, int min, int max) { int offset = (ushort.MaxValue * (pos - min)) / (max - min) - short.MaxValue; return (short)offset; } #endregion #region IJoystickDriver public int DeviceCount { get { return sticks.Count; } } public IList Joysticks { get { return sticks_readonly; } } public void Poll() { lock (sync) { foreach (JoystickDevice js in sticks) { JoyInfoEx info = new JoyInfoEx(); info.Size = JoyInfoEx.SizeInBytes; info.Flags = JoystickFlags.All; UnsafeNativeMethods.joyGetPosEx(js.Id, ref info); int num_axes = js.Axis.Count; if ((js.Details.PovType & PovType.Exists) != 0) num_axes -= 2; // Because the last two axis are used for POV input. int axis = 0; if (axis < num_axes) { js.SetAxis((JoystickAxis)axis, js.Details.CalculateOffset((float)info.XPos, axis)); axis++; } if (axis < num_axes) { js.SetAxis((JoystickAxis)axis, js.Details.CalculateOffset((float)info.YPos, axis)); axis++; } if (axis < num_axes) { js.SetAxis((JoystickAxis)axis, js.Details.CalculateOffset((float)info.ZPos, axis)); axis++; } if (axis < num_axes) { js.SetAxis((JoystickAxis)axis, js.Details.CalculateOffset((float)info.RPos, axis)); axis++; } if (axis < num_axes) { js.SetAxis((JoystickAxis)axis, js.Details.CalculateOffset((float)info.UPos, axis)); axis++; } if (axis < num_axes) { js.SetAxis((JoystickAxis)axis, js.Details.CalculateOffset((float)info.VPos, axis)); axis++; } if ((js.Details.PovType & PovType.Exists) != 0) { float x = 0, y = 0; // A discrete POV returns specific values for left, right, etc. // A continuous POV returns an integer indicating an angle in degrees * 100, e.g. 18000 == 180.00 degrees. // The vast majority of joysticks have discrete POVs, so we'll treat all of them as discrete for simplicity. if ((JoystickPovPosition)info.Pov != JoystickPovPosition.Centered) { if (info.Pov > (int)JoystickPovPosition.Left || info.Pov < (int)JoystickPovPosition.Right) { y = 1; } if ((info.Pov > (int)JoystickPovPosition.Forward) && (info.Pov < (int)JoystickPovPosition.Backward)) { x = 1; } if ((info.Pov > (int)JoystickPovPosition.Right) && (info.Pov < (int)JoystickPovPosition.Left)) { y = -1; } if (info.Pov > (int)JoystickPovPosition.Backward) { x = -1; } } //if ((js.Details.PovType & PovType.Discrete) != 0) //{ // if ((JoystickPovPosition)info.Pov == JoystickPovPosition.Centered) // { x = 0; y = 0; } // else if ((JoystickPovPosition)info.Pov == JoystickPovPosition.Forward) // { x = 0; y = -1; } // else if ((JoystickPovPosition)info.Pov == JoystickPovPosition.Left) // { x = -1; y = 0; } // else if ((JoystickPovPosition)info.Pov == JoystickPovPosition.Backward) // { x = 0; y = 1; } // else if ((JoystickPovPosition)info.Pov == JoystickPovPosition.Right) // { x = 1; y = 0; } //} //else if ((js.Details.PovType & PovType.Continuous) != 0) //{ // if ((JoystickPovPosition)info.Pov == JoystickPovPosition.Centered) // { // // This approach moves the hat on a circle, not a square as it should. // float angle = (float)(System.Math.PI * info.Pov / 18000.0 + System.Math.PI / 2); // x = (float)System.Math.Cos(angle); // y = (float)System.Math.Sin(angle); // if (x < 0.001) // x = 0; // if (y < 0.001) // y = 0; // } //} //else // throw new NotImplementedException("Please post an issue report at http://www.opentk.com/issues"); js.SetAxis((JoystickAxis)axis++, x); js.SetAxis((JoystickAxis)axis++, y); } int button = 0; while (button < js.Button.Count) { js.SetButton((JoystickButton)button, (info.Buttons & (1 << button)) != 0); button++; } } } } #endregion #region IJoystickDriver2 Members public JoystickCapabilities GetCapabilities(int player_index) { lock (sync) { if (IsValid(player_index)) { int device_index = player_to_index[player_index]; JoystickDevice stick = sticks[index_to_stick[device_index]] as JoystickDevice; return stick.Details.Capabilities; } return new JoystickCapabilities(); } } public JoystickState GetState(int player_index) { lock (sync) { JoystickState state = new JoystickState(); if (IsValid(player_index)) { int device_index = player_to_index[player_index]; int index = index_to_stick[device_index]; JoystickDevice stick = sticks[index] as JoystickDevice; // For joysticks with fewer than three axes or four buttons, we must // use joyGetPos; otherwise, joyGetPosEx. This is not just a cosmetic // difference, simple devices will return incorrect results if we use // joyGetPosEx on them. if (stick.Details.Capabilities.AxisCount <= 3 || stick.Details.Capabilities.ButtonCount <= 4) { // Use joyGetPos JoyInfo info = new JoyInfo(); JoystickError result = UnsafeNativeMethods.joyGetPos(device_index, ref info); if (result == JoystickError.NoError) { for (int i = 0; i < stick.Details.Capabilities.AxisCount; i++) { state.SetAxis(JoystickAxis.Axis0 + i, CalculateOffset(info.GetAxis(i), stick.Details.Min[i], stick.Details.Max[i])); } for (int i = 0; i < stick.Details.Capabilities.ButtonCount; i++) { state.SetButton(JoystickButton.Button0 + i, (info.Buttons & 1 << i) != 0); } state.SetIsConnected(true); } else if (result == JoystickError.Unplugged) { UnplugJoystick(player_index); } } else { // Use joyGetPosEx JoyInfoEx info = new JoyInfoEx(); info.Size = JoyInfoEx.SizeInBytes; info.Flags = JoystickFlags.All; JoystickError result = UnsafeNativeMethods.joyGetPosEx(device_index, ref info); if (result == JoystickError.NoError) { for (int i = 0; i < stick.Details.Capabilities.AxisCount; i++) { state.SetAxis(JoystickAxis.Axis0 + i, CalculateOffset(info.GetAxis(i), stick.Details.Min[i], stick.Details.Max[i])); } for (int i = 0; i < stick.Details.Capabilities.ButtonCount; i++) { state.SetButton(JoystickButton.Button0 + i, (info.Buttons & 1 << i) != 0); } state.SetIsConnected(true); } else if (result == JoystickError.Unplugged) { UnplugJoystick(player_index); } } } return state; } } public Guid GetGuid(int index) { lock (sync) { Guid guid = new Guid(); if (IsValid(index)) { // Todo: implement WinMM Guid retrieval } return guid; } } #endregion #region IDisposable public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } void Dispose(bool manual) { if (!disposed) { if (manual) { } disposed = true; } } ~WinMMJoystick() { Dispose(false); } #endregion #region UnsafeNativeMethods [Flags] enum JoystickFlags { X = 0x1, Y = 0x2, Z = 0x4, R = 0x8, U = 0x10, V = 0x20, Pov = 0x40, Buttons = 0x80, All = X | Y | Z | R | U | V | Pov | Buttons } enum JoystickError : uint { NoError = 0, InvalidParameters = 165, NoCanDo = 166, Unplugged = 167 //MM_NoDriver = 6, //MM_InvalidParameter = 11 } [Flags] enum JoystCapsFlags { HasZ = 0x1, HasR = 0x2, HasU = 0x4, HasV = 0x8, HasPov = 0x16, HasPov4Dir = 0x32, HasPovContinuous = 0x64 } enum JoystickPovPosition : ushort { Centered = 0xFFFF, Forward = 0, Right = 9000, Backward = 18000, Left = 27000 } struct JoyCaps { public ushort Mid; public ushort ProductId; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string ProductName; public int XMin; public int XMax; public int YMin; public int YMax; public int ZMin; public int ZMax; public int NumButtons; public int PeriodMin; public int PeriodMax; public int RMin; public int RMax; public int UMin; public int UMax; public int VMin; public int VMax; public JoystCapsFlags Capabilities; public int MaxAxes; public int NumAxes; public int MaxButtons; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] public string RegKey; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string OemVxD; public static readonly int SizeInBytes; static JoyCaps() { SizeInBytes = Marshal.SizeOf(default(JoyCaps)); } public int GetMin(int i) { switch (i) { case 0: return XMin; case 1: return YMin; case 2: return ZMin; case 3: return RMin; case 4: return UMin; case 5: return VMin; default: return 0; } } public int GetMax(int i) { switch (i) { case 0: return XMax; case 1: return YMax; case 2: return ZMax; case 3: return RMax; case 4: return UMax; case 5: return VMax; default: return 0; } } } struct JoyInfo { public int XPos; public int YPos; public int ZPos; public uint Buttons; public int GetAxis(int i) { switch (i) { case 0: return XPos; case 1: return YPos; case 2: return ZPos; default: return 0; } } } struct JoyInfoEx { public int Size; [MarshalAs(UnmanagedType.I4)] public JoystickFlags Flags; public int XPos; public int YPos; public int ZPos; public int RPos; public int UPos; public int VPos; public uint Buttons; public uint ButtonNumber; public int Pov; #pragma warning disable 0169 uint Reserved1; uint Reserved2; #pragma warning restore 0169 public static readonly int SizeInBytes; static JoyInfoEx() { SizeInBytes = Marshal.SizeOf(default(JoyInfoEx)); } public int GetAxis(int i) { switch (i) { case 0: return XPos; case 1: return YPos; case 2: return ZPos; case 3: return RPos; case 4: return UPos; case 5: return VPos; default: return 0; } } } static class UnsafeNativeMethods { [DllImport("Winmm.dll"), SuppressUnmanagedCodeSecurity] public static extern JoystickError joyGetDevCaps(int uJoyID, out JoyCaps pjc, int cbjc); [DllImport("Winmm.dll"), SuppressUnmanagedCodeSecurity] public static extern JoystickError joyGetPos(int uJoyID, ref JoyInfo pji); [DllImport("Winmm.dll"), SuppressUnmanagedCodeSecurity] public static extern JoystickError joyGetPosEx(int uJoyID, ref JoyInfoEx pji); [DllImport("Winmm.dll"), SuppressUnmanagedCodeSecurity] public static extern int joyGetNumDevs(); [DllImport("Winmm.dll"), SuppressUnmanagedCodeSecurity] public static extern JoystickError joyConfigChanged(int flags); } #endregion #region enum PovType [Flags] enum PovType { None = 0x0, Exists = 0x1, Discrete = 0x2, Continuous = 0x4 } #endregion #region struct WinMMJoyDetails struct WinMMJoyDetails { public readonly int[] Min, Max; // Minimum and maximum offset of each axis. public PovType PovType; public JoystickCapabilities Capabilities; public WinMMJoyDetails(JoystickCapabilities caps) : this() { Min = new int[caps.AxisCount]; Max = new int[caps.AxisCount]; Capabilities = caps; } public float CalculateOffset(float pos, int axis) { float offset = (2 * (pos - Min[axis])) / (Max[axis] - Min[axis]) - 1; if (offset > 1) return 1; else if (offset < -1) return -1; else if (offset < 0.001f && offset > -0.001f) return 0; else return offset; } } #endregion } }