Bläddra i källkod

Added DirectX GameController (#83)

Pascal Peridont 7 år sedan
förälder
incheckning
6204ef9f09

+ 4 - 3
libs/directx/directx.vcxproj

@@ -28,6 +28,7 @@
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="directx.cpp" />
+    <ClCompile Include="gamecontroller.c" />
     <ClCompile Include="window.c" />
   </ItemGroup>
   <PropertyGroup Label="Globals">
@@ -149,7 +150,7 @@
       <SubSystem>Windows</SubSystem>
       <GenerateDebugInformation>true</GenerateDebugInformation>
       <OutputFile>../../$(Configuration)/$(TargetName).hdll</OutputFile>
-      <AdditionalDependencies>libhl.lib;user32.lib;gdi32.lib;d3d11.lib;dxgi.lib;d3dcompiler.lib</AdditionalDependencies>
+      <AdditionalDependencies>libhl.lib;user32.lib;gdi32.lib;d3d11.lib;dxgi.lib;d3dcompiler.lib;xinput9_1_0.lib;dinput8.lib</AdditionalDependencies>
     </Link>
   </ItemDefinitionGroup>
   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
@@ -182,7 +183,7 @@
       <OptimizeReferences>true</OptimizeReferences>
       <GenerateDebugInformation>true</GenerateDebugInformation>
       <OutputFile>../../$(Configuration)/$(TargetName).hdll</OutputFile>
-      <AdditionalDependencies>libhl.lib;user32.lib;gdi32.lib;d3d11.lib;dxgi.lib;d3dcompiler.lib</AdditionalDependencies>
+      <AdditionalDependencies>libhl.lib;user32.lib;gdi32.lib;d3d11.lib;dxgi.lib;d3dcompiler.lib;xinput9_1_0.lib;dinput8.lib</AdditionalDependencies>
     </Link>
   </ItemDefinitionGroup>
   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseVS2013|Win32'">
@@ -201,7 +202,7 @@
       <OptimizeReferences>true</OptimizeReferences>
       <GenerateDebugInformation>true</GenerateDebugInformation>
       <OutputFile>../../$(Configuration)/$(TargetName).hdll</OutputFile>
-      <AdditionalDependencies>libhl.lib;user32.lib;gdi32.lib;d3d11.lib;dxgi.lib;d3dcompiler.lib</AdditionalDependencies>
+      <AdditionalDependencies>libhl.lib;user32.lib;gdi32.lib;d3d11.lib;dxgi.lib;d3dcompiler.lib;xinput9_1_0.lib;dinput8.lib</AdditionalDependencies>
     </Link>
   </ItemDefinitionGroup>
   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">

+ 1 - 0
libs/directx/directx.vcxproj.filters

@@ -3,5 +3,6 @@
   <ItemGroup>
     <ClCompile Include="window.c" />
     <ClCompile Include="directx.cpp" />
+    <ClCompile Include="gamecontroller.c" />
   </ItemGroup>
 </Project>

+ 188 - 0
libs/directx/dx/GameController.hx

@@ -0,0 +1,188 @@
+package dx;
+
+@:noCompletion
+typedef GameControllerPtr = hl.Abstract<"dx_gctrl_device">;
+
+private class DInputButton {
+	var num : Int;
+	var mask : Int;
+	
+	public function new( num : Int, mask : Int ){
+		this.num = num;
+		this.mask = mask;
+	}
+}
+
+private class DInputMapping {
+	var guid : Int;
+	var name : hl.Bytes;
+	var button : hl.NativeArray<DInputButton>;
+	var axis : hl.NativeArray<Int>;
+	
+	function new( s : String ){
+		var a = s.split(",");
+		var suid = a.shift();
+		var vendor = Std.parseInt( "0x" + suid.substr(8,4) );
+		var product = Std.parseInt( "0x" + suid.substr(16,4) );
+		guid = (product&0xFF)<<24 | (product&0xFF00)<<8 | (vendor&0xFF)<<8 | (vendor&0xFF00)>>8;
+		name = @:privateAccess a.shift().toUtf8();
+		button = new hl.NativeArray(14);
+		axis = new hl.NativeArray(6);
+		
+		for( e in a ){
+			var p = e.split(":");
+			if( p.length != 2 ) continue;
+			if( p[1].charCodeAt(0) == 'a'.code ){
+				var num = Std.parseInt(p[1].substr(1));
+				switch( p[0] ){
+					case "leftx":        axis[0] = num;
+					case "lefty":        axis[1] = num;
+					case "rightx":       axis[2] = num;
+					case "righty":       axis[3] = num;
+					case "lefttrigger":  axis[4] = num;
+					case "righttrigger": axis[5] = num;
+					default:
+				}
+			}else{
+				var btn : DInputButton = switch( p[1].charCodeAt(0) ){
+					case 'b'.code: new DInputButton(Std.parseInt(p[1].substr(1)), 0);
+					case 'h'.code: {
+						var ba = p[1].substr(1).split(".");
+						if( ba == null ) 
+							null;
+						else
+							new DInputButton(Std.parseInt(ba[0]), Std.parseInt(ba[1]));
+					}
+					default: null;
+				}
+				var idx = switch( p[0] ){
+					case "dpup":          Btn_DPadUp;
+					case "dpdown":        Btn_DPadDown;
+					case "dpleft":        Btn_DPadLeft;
+					case "dpright":       Btn_DPadRight;
+					case "start":         Btn_Start;
+					case "back":          Btn_Back;
+					case "leftstick":     Btn_LeftStick;
+					case "rightstick":    Btn_RightStick;
+					case "leftshoulder":  Btn_LB;
+					case "rightshoulder": Btn_RB;
+					case "a":             Btn_A;
+					case "b":             Btn_B;
+					case "x":             Btn_X;
+					case "y":             Btn_Y;
+					default: null;
+				}
+				if( idx != null && btn != null )
+					button[Type.enumIndex(idx)] = btn;
+			}
+		}
+	}
+	
+	// Default DInput mappings based on libSDL ( https://www.libsdl.org/ )
+	static var DEFAULTS = [
+		"03000000022000000090000000000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,",
+		"03000000203800000900000000000000,8Bitdo NES30 Pro,a:b1,b:b0,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:b9,rightx:a3,righty:a4,start:b11,x:b4,y:b3,",
+		"03000000102800000900000000000000,8Bitdo SFC30 GamePad,a:b1,b:b0,back:b10,leftshoulder:b6,leftx:a0,lefty:a1,rightshoulder:b7,start:b11,x:b4,y:b3,",
+		"03000000a00500003232000000000000,8Bitdo Zero GamePad,a:b0,b:b1,back:b10,dpdown:+a2,dpleft:-a0,dpright:+a0,dpup:-a2,leftshoulder:b6,rightshoulder:b7,start:b11,x:b3,y:b4,",
+		"03000000341a00003608000000000000,Afterglow PS3 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,",
+		"03000000e82000006058000000000000,Cideko AK08b,a:b2,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b3,y:b0,",
+		"03000000260900008888000000000000,Cyber Gadget GameCube Controller,a:b0,b:b1,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a5,leftx:a0,lefty:a1,rightshoulder:b6,righttrigger:a4,rightx:a2,righty:a3~,start:b7,x:b2,y:b3,",
+		"03000000a306000022f6000000000000,Cyborg V.3 Rumble Pad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:+a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:-a3,rightx:a2,righty:a4,start:b9,x:b0,y:b3,",
+		"03000000ffff00000000000000000000,GameStop Gamepad,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,",
+		"030000000d0f00006e00000000000000,HORIPAD 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,",
+		"030000000d0f00006600000000000000,HORIPAD 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,",
+		"030000000d0f00005f00000000000000,Hori Fighting Commander 4 (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,",
+		"030000000d0f00005e00000000000000,Hori Fighting Commander 4 (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,",
+		"030000008f0e00001330000000000000,HuiJia SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b6,rightshoulder:b7,start:b9,x:b3,y:b0,",
+		"030000006d04000016c2000000000000,Logitech Dual Action,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,",
+		"030000006d04000018c2000000000000,Logitech F510 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,",
+		"030000006d04000019c2000000000000,Logitech F710 Gamepad,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,", /* Guide button doesn't seem to be sent in DInput mode. */
+		"03000000380700005032000000000000,Mad Catz FightPad PRO (PS3),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b0,y:b3,",
+		"03000000380700005082000000000000,Mad Catz FightPad PRO (PS4),a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,",
+		"03000000790000004418000000000000,Mayflash GameCube Controller,a:b1,b:b2,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b7,righttrigger:a4,rightx:a5,righty:a2,start:b9,x:b0,y:b3,",
+		"030000001008000001e5000000000000,NEXT SNES Controller,a:b2,b:b1,back:b8,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b6,start:b9,x:b3,y:b0,",
+		"030000007e0500000920000000000000,Nintendo Switch Pro Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,",
+		"03000000362800000100000000000000,OUYA Game Controller,a:b0,b:b3,dpdown:b9,dpleft:b10,dpright:b11,dpup:b8,guide:b14,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:b13,rightx:a3,righty:a4,x:b1,y:b2,",
+		"03000000888800000803000000000000,PS3 Controller,a:b2,b:b1,back:b8,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b9,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b10,righttrigger:b7,rightx:a3,righty:a4,start:b11,x:b0,y:b3,",
+		"030000004c0500006802000000000000,PS3 Controller,a:b14,b:b13,back:b0,dpdown:b6,dpleft:b7,dpright:b5,dpup:b4,guide:b16,leftshoulder:b10,leftstick:b1,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b11,rightstick:b2,righttrigger:b9,rightx:a2,righty:a3,start:b3,x:b15,y:b12,",
+		"03000000250900000500000000000000,PS3 DualShock,a:b2,b:b1,back:b9,dpdown:h0.8,dpleft:h0.4,dpright:h0.2,dpup:h0.1,guide:,leftshoulder:b6,leftstick:b10,lefttrigger:b4,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b11,righttrigger:b5,rightx:a2,righty:a3,start:b8,x:b0,y:b3,",
+		"030000004c050000c405000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,", // DualShock 4 v1 (Wired or Wireless)
+		"030000004c050000cc09000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,", // DualShock 4 v2 (Wired or Wireless)
+		"030000004c050000a00b000000000000,PS4 Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,", // DualShock 4 (Wireless with Sony adapter)
+		"03000000790000001100000000000000,Retrolink SNES Controller,a:b2,b:b1,back:b8,dpdown:+a4,dpleft:-a3,dpright:+a3,dpup:-a4,leftshoulder:b4,rightshoulder:b5,start:b9,x:b3,y:b0,",
+		"030000006b140000010d000000000000,Revolution Pro Controller,a:b1,b:b2,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b12,leftshoulder:b4,leftstick:b10,lefttrigger:a3,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:a4,rightx:a2,righty:a5,start:b9,x:b0,y:b3,",
+		"03000000a30600000cff000000000000,Saitek P2500 Force Rumble Pad,a:b2,b:b3,back:b11,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b10,leftshoulder:b4,leftstick:b8,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b9,righttrigger:b7,rightx:a2,righty:a3,x:b0,y:b1,",
+		"03000000172700004431000000000000,XiaoMi Game Controller,a:b0,b:b1,back:b10,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,guide:b20,leftshoulder:b6,leftstick:b13,lefttrigger:b8,leftx:a0,lefty:a1,rightshoulder:b7,rightstick:b14,righttrigger:a7,rightx:a2,righty:a5,start:b11,x:b3,y:b4,",
+		"03000000830500006020000000000000,iBuffalo SNES Controller,a:b1,b:b0,back:b6,dpdown:+a1,dpleft:-a0,dpright:+a0,dpup:-a1,leftshoulder:b4,rightshoulder:b5,start:b7,x:b3,y:b2,",
+	];
+	
+	public static function parseDefaults() : hl.NativeArray<DInputMapping> {
+		var a = [for( s in DEFAULTS ) new DInputMapping(s)];
+		var n = new hl.NativeArray(a.length);
+		for( i in 0...a.length ) n[i] = a[i];
+		return n;
+	}
+}
+
+enum GameControllerButton {
+	Btn_DPadUp;
+	Btn_DPadDown;
+	Btn_DPadLeft;
+	Btn_DPadRight;
+	Btn_Start;
+	Btn_Back;
+	Btn_LeftStick;
+	Btn_RightStick;
+	Btn_LB;
+	Btn_RB;
+	Btn_A;
+	Btn_B;
+	Btn_X;
+	Btn_Y;
+}
+
+@:keep @:hlNative("directx")
+class GameController {
+	
+	public static function init(){
+		var mappings = DInputMapping.parseDefaults();
+		gctrlInit( mappings );
+	}
+	
+	public static function detect( onDetect : GameControllerPtr -> hl.Bytes -> Void ){
+		gctrlDetect(onDetect);
+	}
+
+	
+	public var ptr(default,null) : GameControllerPtr;
+	public var name(default,null) : String;
+	public var buttons : haxe.EnumFlags<GameControllerButton>;
+	public var lx : Float;
+	public var ly : Float;
+	public var rx : Float;
+	public var ry : Float;
+	public var lt : Float;
+	public var rt : Float;
+	
+	public function new( ptr : GameControllerPtr, name : hl.Bytes ){
+		this.ptr = ptr;
+		this.name = @:privateAccess String.fromUTF8(name);
+	}
+	
+	public inline function update(){
+		gctrlUpdate(this);
+	}
+	
+	public inline function setVibration( strength : Float ){
+		gctrlSetVibration(ptr,strength);
+	}
+	
+	// 
+	
+	static function gctrlInit( mappings : hl.NativeArray<DInputMapping> ){}
+	static function gctrlDetect( onDetect : GameControllerPtr -> hl.Bytes -> Void ){}
+	static function gctrlUpdate( pad : GameController ){}
+	static function gctrlSetVibration( ptr : GameControllerPtr, strength : Float ){}
+
+	
+}

+ 343 - 0
libs/directx/gamecontroller.c

@@ -0,0 +1,343 @@
+#define HL_NAME(n) directx_##n
+#include <hl.h>
+#include <xinput.h>
+#include <InitGuid.h>
+#define DIRECTINPUT_VERSION 0x0800
+#include <dinput.h>
+
+#define HAT_CENTERED    0x00
+#define HAT_UP          0x01
+#define HAT_RIGHT       0x02
+#define HAT_DOWN        0x04
+#define HAT_LEFT        0x08
+
+typedef struct {
+	hl_type *t;
+	int num;
+	int mask; // for hat only
+} dinput_mapping_btn;
+
+typedef struct {
+	hl_type *t;
+	unsigned int guid;
+	vbyte *name;
+	varray *button; // dinput_mapping_btn
+	varray *axis; // int
+} dinput_mapping;
+
+typedef struct _dx_gctrl_device dx_gctrl_device;
+struct _dx_gctrl_device {
+	char *name;
+	bool isNew;
+	int xUID;
+	GUID dGuid;
+	LPDIRECTINPUTDEVICE8 dDevice;
+	dinput_mapping *dMapping;
+	dx_gctrl_device *next;
+};
+
+typedef struct {
+	hl_type *t;
+	dx_gctrl_device *device;
+	vstring *name;
+	int buttons;
+	double lx;
+	double ly;
+	double rx;
+	double ry;
+	double lt;
+	double rt;
+} dx_gctrl_data;
+
+static dx_gctrl_device *dx_gctrl_devices = NULL;
+static dx_gctrl_device *dx_gctrl_removed = NULL;
+static CRITICAL_SECTION dx_gctrl_cs;
+static bool dx_gctrl_syncNeeded = FALSE;
+
+// DirectInput specific
+static LPDIRECTINPUT8 gctrl_dinput = NULL;
+static varray *gctrl_dinput_mappings = NULL;
+
+// XInput
+static void gctrl_xinput_add(int uid, dx_gctrl_device **current) {
+	dx_gctrl_device *device = *current;
+	dx_gctrl_device *prev = NULL;
+	while (device) {
+		if (device->xUID == uid) {
+			if (device == *current) *current = device->next;
+			else if (prev) prev->next = device->next;
+
+			device->next = dx_gctrl_devices;
+			dx_gctrl_devices = device;
+			return;
+		}
+
+		prev = device;
+		device = device->next;
+	}
+
+	device = (dx_gctrl_device*)malloc(sizeof(dx_gctrl_device));
+	ZeroMemory(device, sizeof(dx_gctrl_device));
+	device->name = "XInput controller";
+	device->xUID = uid;
+	device->isNew = TRUE;
+	device->next = dx_gctrl_devices;
+	dx_gctrl_devices = device;
+	dx_gctrl_syncNeeded = TRUE;
+}
+
+static void gctrl_xinput_update(dx_gctrl_data *data) {
+	XINPUT_STATE state;
+	HRESULT r = XInputGetState(data->device->xUID, &state);
+	if (FAILED(r))
+		return;
+
+	data->buttons = (state.Gamepad.wButtons & 0x0FFF) | (state.Gamepad.wButtons & 0xF000) >> 2;
+	data->lx = (double)(state.Gamepad.sThumbLX < -32767 ? -1. : (state.Gamepad.sThumbLX / 32767.));
+	data->ly = (double)(state.Gamepad.sThumbLY < -32767 ? -1. : (state.Gamepad.sThumbLY / 32767.));
+	data->rx = (double)(state.Gamepad.sThumbRX < -32767 ? -1. : (state.Gamepad.sThumbRX / 32767.));
+	data->ry = (double)(state.Gamepad.sThumbRY < -32767 ? -1. : (state.Gamepad.sThumbRY / 32767.));
+	data->lt = (double)(state.Gamepad.bLeftTrigger / 255.);
+	data->rt = (double)(state.Gamepad.bRightTrigger / 255.);
+}
+
+// DirectInput
+
+static void gctrl_dinput_init( varray *mappings ) {
+	if( !gctrl_dinput) {
+		HINSTANCE instance = GetModuleHandle(NULL);
+		if( instance )
+			DirectInput8Create(instance, DIRECTINPUT_VERSION, &IID_IDirectInput8, &gctrl_dinput, NULL);
+	}
+
+	if( mappings ) {
+		if( gctrl_dinput_mappings )
+			hl_remove_root(&gctrl_dinput_mappings);
+		gctrl_dinput_mappings = mappings;
+		hl_add_root(&gctrl_dinput_mappings);
+	}
+}
+
+static BOOL CALLBACK gctrl_dinput_deviceCb(const DIDEVICEINSTANCE *instance, void *context) {
+	dx_gctrl_device **current = (dx_gctrl_device**)context;
+	dx_gctrl_device *device = *current;
+	dx_gctrl_device *prev = NULL;
+	LPDIRECTINPUTDEVICE8 di_device;
+	HRESULT result;
+	dinput_mapping *mapping = NULL;
+
+	while( device ) {
+		if(device->xUID < 0 && !memcmp(&device->dGuid, &instance->guidInstance, sizeof(device->dGuid)) ) {
+			if( device == *current ) *current = device->next;
+			else if( prev ) prev->next = device->next;
+
+			device->next = dx_gctrl_devices;
+			dx_gctrl_devices = device;
+			return DIENUM_CONTINUE;
+		}
+
+		prev = device;
+		device = device->next;
+	}
+
+	// Find mapping
+	for( int i=0; i<gctrl_dinput_mappings->size; i++ ){
+		dinput_mapping *m = hl_aptr(gctrl_dinput_mappings, dinput_mapping*)[i];
+		if( instance->guidProduct.Data1 == m->guid ) {
+			mapping = m;
+			break;
+		}
+	}
+	if (!mapping ) return DIENUM_CONTINUE;
+
+	result = IDirectInput8_CreateDevice(gctrl_dinput, &instance->guidInstance, &di_device, NULL);
+	if( FAILED(result) ) return DIENUM_CONTINUE;
+	
+	device = (dx_gctrl_device*)malloc(sizeof(dx_gctrl_device));
+	ZeroMemory(device, sizeof(dx_gctrl_device));
+
+	result = IDirectInputDevice8_QueryInterface(di_device, &IID_IDirectInputDevice8, (LPVOID *)&device->dDevice);
+	IDirectInputDevice8_Release(di_device);
+	if( FAILED(result) ) {
+		free(device);
+		return DIENUM_CONTINUE;
+	}
+
+	result = IDirectInputDevice8_SetDataFormat(device->dDevice, &c_dfDIJoystick2);
+	if( FAILED(result) ) {
+		free(device);
+		return DIENUM_CONTINUE;
+	}
+
+	device->name = mapping->name;
+	device->xUID = -1;
+	device->dMapping = mapping;
+	memcpy(&device->dGuid, &instance->guidInstance, sizeof(device->dGuid));
+	device->isNew = TRUE;
+	device->next = dx_gctrl_devices;
+	dx_gctrl_devices = device;
+	dx_gctrl_syncNeeded = TRUE;
+
+	return DIENUM_CONTINUE;
+}
+
+static void gctrl_dinput_remove(dx_gctrl_device *device) {
+	IDirectInputDevice8_Unacquire(device->dDevice);
+	IDirectInputDevice8_Release(device->dDevice);
+}
+
+// Based on SDL ( https://hg.libsdl.org/SDL/file/007dfe83abf8/src/joystick/windows/SDL_dinputjoystick.c )
+static int gctrl_dinput_translatePOV(DWORD value) {
+	const int HAT_VALS[] = {
+		HAT_UP,
+		HAT_UP | HAT_RIGHT,
+		HAT_RIGHT,
+		HAT_DOWN | HAT_RIGHT,
+		HAT_DOWN,
+		HAT_DOWN | HAT_LEFT,
+		HAT_LEFT,
+		HAT_UP | HAT_LEFT
+	};
+
+	if( LOWORD(value) == 0xFFFF ) return HAT_CENTERED;
+
+	value += 4500 / 2;
+	value %= 36000;
+	value /= 4500;
+
+	if( value < 0 || value >= 8 ) return HAT_CENTERED;
+
+	return HAT_VALS[value];
+}
+
+static void gctrl_dinput_update(dx_gctrl_data *data) {
+	DIJOYSTATE2 state;
+	long *p = (long*)&state;
+	dinput_mapping *mapping = data->device->dMapping;
+	LPDIRECTINPUTDEVICE8 dDevice = data->device->dDevice;
+
+	int *axis = hl_aptr(mapping->axis, int);
+
+	HRESULT result = IDirectInputDevice8_GetDeviceState(dDevice, sizeof(DIJOYSTATE2), &state);
+	if( result == DIERR_INPUTLOST || result == DIERR_NOTACQUIRED ) {
+		IDirectInputDevice8_Acquire(dDevice);
+		result = IDirectInputDevice8_GetDeviceState(dDevice, sizeof(DIJOYSTATE2), &state);
+	}
+	if( result != DI_OK )
+		return;
+	
+	if( mapping->axis->size > 0 ) data->lx = (double)((*(p + axis[0])) / 65535.) * 2 - 1.;
+	if( mapping->axis->size > 1 ) data->ly = (double)((*(p + axis[1])) / 65535.) * -2 + 1.;
+	if( mapping->axis->size > 2 ) data->rx = (double)((*(p + axis[2])) / 65535.) * 2 - 1.;
+	if( mapping->axis->size > 3 ) data->ry = (double)((*(p + axis[3])) / 65535.) * -2 + 1.;
+	if( mapping->axis->size > 4 ) data->lt = (double)((*(p + axis[4])) / 65535.);
+	if( mapping->axis->size > 5 ) data->rt = (double)((*(p + axis[5])) / 65535.);
+
+	data->buttons = 0;
+	for( int i = 0; i < mapping->button->size; i++ ) {
+		dinput_mapping_btn *bmap = hl_aptr(mapping->button, dinput_mapping_btn*)[i];
+		if( bmap->mask == 0 ) {
+			data->buttons |= (state.rgbButtons[bmap->num] > 0 ? 1 : 0) << i;
+		} else {
+			data->buttons |= ((gctrl_dinput_translatePOV(state.rgdwPOV[bmap->num])&bmap->mask) > 0 ? 1 : 0) << i;
+		}
+	}
+}
+
+// 
+
+void gctrl_detect_thread(void *p) {
+	while (true) {
+		EnterCriticalSection(&dx_gctrl_cs);
+
+		dx_gctrl_device *current = dx_gctrl_devices;
+		dx_gctrl_devices = NULL;
+
+		// XInput
+		for (int uid = XUSER_MAX_COUNT - 1; uid >= 0; uid--) {
+			XINPUT_CAPABILITIES capabilities;
+			if (XInputGetCapabilities(uid, XINPUT_FLAG_GAMEPAD, &capabilities) == ERROR_SUCCESS)
+				gctrl_xinput_add(uid, &current);
+		}
+
+		// DInput
+		if (gctrl_dinput)
+			IDirectInput8_EnumDevices(gctrl_dinput, DI8DEVCLASS_GAMECTRL, gctrl_dinput_deviceCb, &current, DIEDFL_ATTACHEDONLY);
+
+		while (current) {
+			dx_gctrl_device *next = current->next;
+			current->next = dx_gctrl_removed;
+			dx_gctrl_removed = current;
+			current = next;
+			dx_gctrl_syncNeeded = TRUE;
+		}
+
+		LeaveCriticalSection(&dx_gctrl_cs);
+
+		Sleep(1);
+	}
+}
+
+HL_PRIM void HL_NAME(gctrl_init)( varray *mappings ) {
+	gctrl_dinput_init( mappings );
+	InitializeCriticalSection(&dx_gctrl_cs);
+	hl_thread_start(gctrl_detect_thread, NULL, FALSE);
+}
+
+
+static void gctrl_onDetect( vclosure *onDetect, dx_gctrl_device *device, const char *name) {
+	if( !onDetect ) return;
+	if( onDetect->hasValue )
+		((void(*)(void *, dx_gctrl_device*, vbyte*))onDetect->fun)(onDetect->value, device, (vbyte*)name);
+	else
+		((void(*)(dx_gctrl_device*, vbyte*))onDetect->fun)(device, (vbyte*)name);
+}
+
+HL_PRIM void HL_NAME(gctrl_detect)( vclosure *onDetect ) {
+	if (dx_gctrl_syncNeeded) {
+		EnterCriticalSection(&dx_gctrl_cs);
+
+		dx_gctrl_device *device = dx_gctrl_devices;
+		while (device) {
+			if (device->isNew) {
+				gctrl_onDetect(onDetect, device, device->name);
+				device->isNew = FALSE;
+			}
+			device = device->next;
+		}
+
+		while (dx_gctrl_removed) {
+			device = dx_gctrl_removed;
+			dx_gctrl_removed = device->next;
+
+			gctrl_onDetect(onDetect, device, NULL);
+			if (device->dDevice)
+				gctrl_dinput_remove(device);
+			free(device);
+		}
+
+		dx_gctrl_syncNeeded = FALSE;
+		LeaveCriticalSection(&dx_gctrl_cs);
+	}
+}
+
+HL_PRIM void HL_NAME(gctrl_update)(dx_gctrl_data *data) {
+	if( data->device->xUID >= 0 )
+		gctrl_xinput_update(data);
+	else if( data->device->dDevice && data->device->dMapping )
+		gctrl_dinput_update(data);
+}
+
+HL_PRIM void HL_NAME(gctrl_set_vibration)(dx_gctrl_device *device, double strength) {
+	if( device->xUID >= 0 ){
+		XINPUT_VIBRATION vibration;
+		vibration.wLeftMotorSpeed = vibration.wRightMotorSpeed = (WORD)(strength * 65535);
+		XInputSetState(device->xUID, &vibration);
+	}
+}
+
+#define TGAMECTRL _ABSTRACT(dx_gctrl_device)
+DEFINE_PRIM(_VOID, gctrl_init, _ARR);
+DEFINE_PRIM(_VOID, gctrl_detect, _FUN(_VOID, TGAMECTRL _BYTES));
+DEFINE_PRIM(_VOID, gctrl_update, _OBJ(TGAMECTRL _STRING _I32 _F64 _F64 _F64 _F64 _F64 _F64));
+DEFINE_PRIM(_VOID, gctrl_set_vibration, TGAMECTRL _F64);