Opentk/Source/OpenTK/Platform/Windows/WinMMJoystick.cs
2014-02-01 16:01:33 +01:00

701 lines
25 KiB
C#

#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<JoystickDevice> sticks = new List<JoystickDevice>();
IList<JoystickDevice> sticks_readonly;
// Matches a WinMM device index to a specific stick above
readonly Dictionary<int, int> index_to_stick =
new Dictionary<int, int>();
// Matches a player index to a WinMM device index
readonly Dictionary<int, int> player_to_index =
new Dictionary<int, int>();
// 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<WinMMJoyDetails> OpenJoystick(int number)
{
lock (sync)
{
JoystickDevice<WinMMJoyDetails> 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<WinMMJoyDetails>(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<WinMMJoyDetails> stick =
sticks[index_to_stick[device_index]] as JoystickDevice<WinMMJoyDetails>;
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<JoystickDevice> Joysticks
{
get { return sticks_readonly; }
}
public void Poll()
{
lock (sync)
{
foreach (JoystickDevice<WinMMJoyDetails> 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<WinMMJoyDetails> stick =
sticks[index_to_stick[device_index]] as JoystickDevice<WinMMJoyDetails>;
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<WinMMJoyDetails> stick =
sticks[index] as JoystickDevice<WinMMJoyDetails>;
// 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
}
}