1 /++
2 
3 	Provides a polling-based API to use gamepads/joysticks on Linux and Windows.
4 
5 	Pass `-version=ps1_style` or `-version=xbox_style` to pick your API style - the constants will use the names of the buttons on those controllers and attempt to emulate the other. ps1_style is compatible with more hardware and thus the default. XBox controllers work with either, though.
6 
7 	The docs for this file are quite weak, I suggest you view source of [arsd.gamehelers] for an example of how it might be used.
8 +/
9 
10 /*
11 	FIXME: a simple function to integrate with sdpy event loop. templated function
12 
13 	HIGH LEVEL NOTES
14 
15 	This will offer a pollable state of two styles of controller: a PS1 or an XBox 360.
16 
17 
18 	Actually, maybe I'll combine the two controller types. Make L2 and R2 just digital aliases
19 	for the triggers, which are analog aliases for it.
20 
21 	Then have a virtual left stick which has the dpad aliases, while keeping the other two independent
22 	(physical dpad and physical left stick).
23 
24 	Everything else should basically just work. We'll simply be left with naming and I can do them with
25 	aliases too.
26 
27 
28 	I do NOT bother with pressure sensitive other buttons, though Xbox original and PS2 had them, they
29 	have been removed from the newer models. It makes things simpler anyway since we can check "was just
30 	pressed" instead of all deltas.
31 
32 
33 	The PS1 controller style works for a lot of games:
34 		* The D-pad is an alias for the left stick. Analog input still works too.
35 		* L2 and R2 are given as buttons
36 		* The keyboard works as buttons
37 		* The mouse is an alias for the right stick
38 		* Buttons are given as labeled on a playstation controller
39 
40 	The XBox controller style works if you need full, modern features:
41 		* The left stick and D-pad works independently of one another
42 			the d pad works as additional buttons.
43 		* The triggers work as independent analog inputs
44 			note that the WinMM driver doesn't support full independence
45 			since it is sent as a z-axis. Linux and modern Windows does though.
46 		* Buttons are labeled as they are on the XBox controller
47 		* The rumble motors are available, if the underlying driver supports it and noop if not.
48 		* Audio I/O is available, if the underlying driver supports it. NOT IMPLEMENTED.
49 
50 	You chose which one you want at compile time with a -version=xbox_style or -version=ps1_style switch.
51 	The default is ps1_style which works with xbox controllers too, it just simplifies them.
52 
53 	TODO:
54 		handling keyboard+mouse input as joystick aliases
55 		remapping support
56 		network transparent joysticks for at least the basic stuff.
57 
58 	=================================
59 
60 	LOW LEVEL NOTES
61 
62 	On Linux, I'll just use /dev/input/js*. It is easy and works with everything I care about. It can fire
63 	events to arsd.eventloop and also maintains state internally for polling. You do have to let it get
64 	events though to handle that input - either doing your own select (etc.) on the js file descriptor,
65 	or running the event loop (which is what I recommend).
66 
67 	On Windows, I'll support the mmsystem messages as far as I can, and XInput for more capabilities
68 	of the XBox 360 controller. (The mmsystem should support my old PS1 controller and xbox is the
69 	other one I have. I have PS3 controllers too which would be nice but since they require additional
70 	drivers, meh.)
71 
72 	linux notes:
73 		all basic input is available, no audio (I think), no force feedback (I think)
74 
75 	winmm notes:
76 		the xbox 360 controller basically works and sends events to the window for the buttons,
77 		left stick, and triggers. It doesn't send events for the right stick or dpad, but these
78 		are available through joyGetPosEx (the dpad is the POV hat and the right stick is
79 		the other axes).
80 
81 		The triggers are considered a z-axis with the left one going negative and right going positive.
82 
83 	windows xinput notes:
84 		all xbox 360 controller features are available via a polling api.
85 
86 		it doesn't seem to support events. That's OK for games generally though, because we just
87 		want to check state on each loop.
88 
89 		For non-games however, using the traditional message loop is probably easier.
90 
91 		XInput is only supported on newer operating systems (Vista I think),
92 		so I'm going to dynamically load it all and fallback on the old one if
93 		it fails.
94 
95 
96 
97 	Other fancy joysticks work low level on linux at least but the high level api reduces them to boredom but like
98 	hey the events are still there and it still basically works, you'd just have to give a custom mapping.
99 */
100 module arsd.joystick;
101 
102 // --------------------------------
103 // High level interface
104 // --------------------------------
105 
106 version(xbox_style) {
107 	version(ps1_style)
108 		static assert(0, "Pass only one xbox_style OR ps1_style");
109 } else
110 	version=ps1_style; // default is PS1 style as it is a lower common denominator
111 
112 version(xbox_style) {
113 	alias Axis = XBox360Axes;
114 	alias Button = XBox360Buttons;
115 } else version(ps1_style) {
116 	alias Axis = PS1AnalogAxes;
117 	alias Button = PS1Buttons;
118 }
119 
120 
121 version(Windows) {
122 	WindowsXInput wxi;
123 }
124 
125 version(OSX) {
126 	struct JoystickState {}
127 }
128 
129 JoystickState[4] joystickState;
130 
131 version(linux) {
132 	int[4] joystickFds = -1;
133 
134 
135 	// On Linux, we have to track state ourselves since we only get events from the OS
136 	struct JoystickState {
137 		short[8] axes;
138 		ubyte[16] buttons;
139 	}
140 
141 	const(JoystickMapping)*[4] joystickMapping;
142 
143 	struct JoystickMapping {
144 		// maps virtual buttons to real buttons, etc.
145 		int[__traits(allMembers, Axis).length] axisOffsets = -1;
146 		int[__traits(allMembers, Button).length] buttonOffsets = -1;
147 	}
148 
149 	/// If you have a real xbox 360 controller, use this mapping
150 	version(xbox_style) // xbox style maps directly to an xbox controller (of course)
151 	static immutable xbox360Mapping = JoystickMapping(
152 		[0,1,2,3,4,5,6,7],
153 		[0,1,2,3,4,5,6,7,8,9,10]
154 	);
155 	else version(ps1_style)
156 	static immutable xbox360Mapping = JoystickMapping(
157 		// PS1AnalogAxes index to XBox360Axes values
158 		[XBox360Axes.horizontalLeftStick,
159 		XBox360Axes.verticalLeftStick,
160 		XBox360Axes.verticalRightStick,
161 		XBox360Axes.horizontalRightStick,
162 		XBox360Axes.horizontalDpad,
163 		XBox360Axes.verticalDpad],
164 		// PS1Buttons index to XBox360Buttons values
165 		[XBox360Buttons.y, XBox360Buttons.b, XBox360Buttons.a, XBox360Buttons.x,
166 			cast(XBox360Buttons) -1, cast(XBox360Buttons) -1, // L2 and R2 don't map easily
167 			XBox360Buttons.lb, XBox360Buttons.rb,
168 			XBox360Buttons.back, XBox360Buttons.start,
169 			XBox360Buttons.leftStick, XBox360Buttons.rightStick]
170 	);
171 
172 
173 	/// For a real ps1 controller
174 	version(ps1_style)
175 	static immutable ps1Mapping = JoystickMapping(
176 		[0,1,2,3,4,5],
177 		[0,1,2,3,4,5,6,7,8,9,10,11]
178 
179 	);
180 	else version(xbox_style)
181 	static immutable ps1Mapping = JoystickMapping(
182 		// FIXME... if we're going to support this at all
183 		// I think if I were to write a program using the xbox style,
184 		// I'd just use my xbox controller.
185 	);
186 
187 	/// For Linux only, reads the latest joystick events into the change buffer, if available.
188 	/// It is non-blocking
189 	void readJoystickEvents(int fd) {
190 		js_event event;
191 
192 		while(true) {
193 			auto r = read(fd, &event, event.sizeof);
194 			if(r == -1) {
195 				import core.stdc.errno;
196 				if(errno == EAGAIN || errno == EWOULDBLOCK)
197 					break;
198 				else assert(0); // , to!string(fd) ~ " " ~ to!string(errno));
199 			}
200 			assert(r == event.sizeof);
201 
202 			ptrdiff_t player = -1;
203 			foreach(i, f; joystickFds)
204 				if(f == fd) {
205 					player = i;
206 					break;
207 				}
208 
209 			assert(player >= 0 && player < joystickState.length);
210 
211 			if(event.type & JS_EVENT_AXIS) {
212 				joystickState[player].axes[event.number] = event.value;
213 
214 				if(event.type & JS_EVENT_INIT) {
215 					if(event.number == 5) {
216 						// After being initialized, if axes[6] == 32767, it seems to be my PS1 controller
217 						// If axes[5] is -32767, it might be an Xbox controller.
218 
219 						if(event.value == -32767 && joystickMapping[player] is null) {
220 							joystickMapping[player] = &xbox360Mapping;
221 						}
222 					} else if(event.number == 6) {
223 						if((event.value == 32767 || event.value == -32767) && joystickMapping[player] is null) {
224 							joystickMapping[player] = &ps1Mapping;
225 						}
226 					}
227 				}
228 			}
229 			if(event.type & JS_EVENT_BUTTON) {
230 				joystickState[player].buttons[event.number] = event.value ? 255 : 0;
231 			}
232 		}
233 	}
234 }
235 
236 version(Windows) {
237 	extern(Windows)
238 	DWORD function(DWORD, XINPUT_STATE*) getJoystickOSState;
239 
240 	extern(Windows)
241 	DWORD winMMFallback(DWORD id, XINPUT_STATE* state) {
242 		JOYINFOEX info;
243 		auto result = joyGetPosEx(id, &info);
244 		if(result == 0) {
245 			// FIXME
246 
247 		}
248 		return result;
249 	}
250 
251 	alias JoystickState = XINPUT_STATE;
252 }
253 
254 /// Returns the number of players actually connected
255 ///
256 /// The controller ID
257 int enableJoystickInput(
258 	int player1ControllerId = 0,
259 	int player2ControllerId = 1,
260 	int player3ControllerId = 2,
261 	int player4ControllerId = 3)
262 {
263 	version(linux) {
264 		bool preparePlayer(int player, int id) {
265 			if(id < 0)
266 				return false;
267 
268 			assert(player >= 0 && player < joystickFds.length);
269 			assert(id < 10);
270 			assert(id >= 0);
271 			char[] filename = "/dev/input/js0\0".dup;
272 			filename[$-2] = cast(char) (id + '0');
273 
274 			int fd = open(filename.ptr, O_RDONLY);
275 			if(fd > 0) {
276 				joystickFds[player] = fd;
277 
278 				version(with_eventloop) {
279 					import arsd.eventloop;
280 					makeNonBlocking(fd);
281 					addFileEventListeners(fd, &readJoystickEvents, null, null);
282 				} else {
283 					// for polling, we will set nonblocking mode anyway,
284 					// the readJoystickEvents function will handle this fine
285 					// so we can call it when needed even on like a game timer.
286 					auto flags = fcntl(fd, F_GETFL, 0);
287 					if(flags == -1)
288 						throw new Exception("fcntl get");
289 					flags |= O_NONBLOCK;
290 					auto s = fcntl(fd, F_SETFL, flags);
291 					if(s == -1)
292 						throw new Exception("fcntl set");
293 				}
294 
295 				return true;
296 			}
297 			return false;
298 		}
299 
300 		if(!preparePlayer(0, player1ControllerId) ? 1 : 0)
301 			return 0;
302 		if(!preparePlayer(1, player2ControllerId) ? 1 : 0)
303 			return 1;
304 		if(!preparePlayer(2, player3ControllerId) ? 1 : 0)
305 			return 2;
306 		if(!preparePlayer(3, player4ControllerId) ? 1 : 0)
307 			return 3;
308 		return 4; // all players successfully initialized
309 	} else version(Windows) {
310 		if(wxi.loadDll()) {
311 			getJoystickOSState = wxi.XInputGetState;
312 		} else {
313 			// WinMM fallback
314 			getJoystickOSState = &winMMFallback;
315 		}
316 
317 		assert(getJoystickOSState !is null);
318 
319 		if(getJoystickOSState(player1ControllerId, &(joystickState[0])))
320 			return 0;
321 		if(getJoystickOSState(player2ControllerId, &(joystickState[1])))
322 			return 1;
323 		if(getJoystickOSState(player3ControllerId, &(joystickState[2])))
324 			return 2;
325 		if(getJoystickOSState(player4ControllerId, &(joystickState[3])))
326 			return 3;
327 
328 		return 4;
329 	} else static assert(0, "Unsupported OS");
330 
331 	// return 0;
332 }
333 
334 ///
335 void closeJoysticks() {
336 	version(linux) {
337 		foreach(ref fd; joystickFds) {
338 			if(fd > 0) {
339 				version(with_eventloop) {
340 					import arsd.eventloop;
341 					removeFileEventListeners(fd);
342 				}
343 				close(fd);
344 			}
345 			fd = -1;
346 		}
347 	} else version(Windows) {
348 		getJoystickOSState = null;
349 		wxi.unloadDll();
350 	} else static assert(0);
351 }
352 
353 ///
354 struct JoystickUpdate {
355 	///
356 	int player;
357 
358 	JoystickState old;
359 	JoystickState current;
360 
361 	/// changes from last update
362 	bool buttonWasJustPressed(Button button) {
363 		return buttonIsPressed(button) && !oldButtonIsPressed(button);
364 	}
365 
366 	/// ditto
367 	bool buttonWasJustReleased(Button button) {
368 		return !buttonIsPressed(button) && oldButtonIsPressed(button);
369 	}
370 
371 	/// this is normalized down to a 16 step change
372 	/// and ignores a dead zone near the middle
373 	short axisChange(Axis axis) {
374 		return cast(short) (axisPosition(axis) - oldAxisPosition(axis));
375 	}
376 
377 	/// current state
378 	bool buttonIsPressed(Button button) {
379 		return buttonIsPressedHelper(button, &current);
380 	}
381 
382 	/// Note: UP is negative!
383 	/// Value will actually be -16 to 16 ish.
384 	short axisPosition(Axis axis, short digitalFallbackValue = short.max) {
385 		return axisPositionHelper(axis, &current, digitalFallbackValue);
386 	}
387 
388 	/* private */
389 
390 	// old state
391 	bool oldButtonIsPressed(Button button) {
392 		return buttonIsPressedHelper(button, &old);
393 	}
394 
395 	short oldAxisPosition(Axis axis, short digitalFallbackValue = short.max) {
396 		return axisPositionHelper(axis, &old, digitalFallbackValue);
397 	}
398 
399 	short axisPositionHelper(Axis axis, JoystickState* what, short digitalFallbackValue = short.max) {
400 		version(ps1_style) {
401 			// on PS1, the d-pad and left stick are synonyms for each other
402 			// the dpad takes precedence, if it is pressed
403 
404 			if(axis == PS1AnalogAxes.horizontalDpad || axis == PS1AnalogAxes.horizontalLeftStick) {
405 				auto it = axisPositionHelperRaw(PS1AnalogAxes.horizontalDpad, what, digitalFallbackValue);
406 				if(!it)
407 					it = axisPositionHelperRaw(PS1AnalogAxes.horizontalLeftStick, what, digitalFallbackValue);
408 				return it;
409 			}
410 
411 			if(axis == PS1AnalogAxes.verticalDpad || axis == PS1AnalogAxes.verticalLeftStick) {
412 				auto it = axisPositionHelperRaw(PS1AnalogAxes.verticalDpad, what, digitalFallbackValue);
413 				if(!it)
414 					it = axisPositionHelperRaw(PS1AnalogAxes.verticalLeftStick, what, digitalFallbackValue);
415 				return it;
416 			}
417 		}
418 
419 		return axisPositionHelperRaw(axis, what, digitalFallbackValue);
420 	}
421 
422 	static short normalizeAxis(short value) {
423 	/+
424 		auto v = normalizeAxisHack(value);
425 		import std.stdio;
426 		writeln(value, " :: ", v);
427 		return v;
428 	}
429 	static short normalizeAxisHack(short value) {
430 	+/
431 		if(value > -1600 && value < 1600)
432 			return 0; // the deadzone gives too much useless junk
433 		return cast(short) (value >>> 11);
434 	}
435 
436 	bool buttonIsPressedHelper(Button button, JoystickState* what) {
437 		version(linux) {
438 			int mapping = -1;
439 			if(auto ptr = joystickMapping[player])
440 				mapping = ptr.buttonOffsets[button];
441 			if(mapping != -1)
442 				return what.buttons[mapping] ? true : false;
443 			// otherwise what do we do?
444 			// FIXME
445 			return false; // the button isn't mapped, figure it isn't there and thus can't be pushed
446 		} else version(Windows) {
447 			// on Windows, I'm always assuming it is an XBox 360 controller
448 			// because that's what I have and the OS supports it so well
449 			version(xbox_style)
450 			final switch(button) {
451 				case XBox360Buttons.a: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_A) ? true : false;
452 				case XBox360Buttons.b: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_B) ? true : false;
453 				case XBox360Buttons.x: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_X) ? true : false;
454 				case XBox360Buttons.y: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_Y) ? true : false;
455 
456 				case XBox360Buttons.lb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) ? true : false;
457 				case XBox360Buttons.rb: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) ? true : false;
458 
459 				case XBox360Buttons.back: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) ? true : false;
460 				case XBox360Buttons.start: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_START) ? true : false;
461 
462 				case XBox360Buttons.leftStick: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_THUMB) ? true : false;
463 				case XBox360Buttons.rightStick: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) ? true : false;
464 
465 				case XBox360Buttons.xboxLogo: return false;
466 			}
467 			else version(ps1_style)
468 			final switch(button) {
469 				case PS1Buttons.triangle: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_Y) ? true : false;
470 				case PS1Buttons.square: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_X) ? true : false;
471 				case PS1Buttons.cross: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_A) ? true : false;
472 				case PS1Buttons.circle: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_B) ? true : false;
473 
474 				case PS1Buttons.select: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_BACK) ? true : false;
475 				case PS1Buttons.start: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_START) ? true : false;
476 
477 				case PS1Buttons.l1: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) ? true : false;
478 				case PS1Buttons.r1: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) ? true : false;
479 
480 				case PS1Buttons.l2: return (what.Gamepad.bLeftTrigger > 100);
481 				case PS1Buttons.r2: return (what.Gamepad.bRightTrigger > 100);
482 
483 				case PS1Buttons.l3: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_THUMB) ? true : false;
484 				case PS1Buttons.r3: return (what.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) ? true : false;
485 			}
486 		}
487 	}
488 
489 	short axisPositionHelperRaw(Axis axis, JoystickState* what, short digitalFallbackValue = short.max) {
490 		version(linux) {
491 			int mapping = -1;
492 			if(auto ptr = joystickMapping[player])
493 				mapping = ptr.axisOffsets[axis];
494 			if(mapping != -1)
495 				return normalizeAxis(what.axes[mapping]);
496 			return 0; // no such axis apparently, let the cooked one do something if it can
497 		} else version(Windows) {
498 			// on Windows, assuming it is an XBox 360 controller
499 			version(xbox_style)
500 			final switch(axis) {
501 				case XBox360Axes.horizontalLeftStick:
502 					return normalizeAxis(what.Gamepad.sThumbLX);
503 				case XBox360Axes.verticalLeftStick:
504 					return normalizeAxis(what.Gamepad.sThumbLY);
505 				case XBox360Axes.horizontalRightStick:
506 					return normalizeAxis(what.Gamepad.sThumbRX);
507 				case XBox360Axes.verticalRightStick:
508 					return normalizeAxis(what.Gamepad.sThumbRY);
509 				case XBox360Axes.verticalDpad:
510 					return (what.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_UP) ? -digitalFallbackValue :
511 					       (what.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_DOWN) ? digitalFallbackValue :
512 					       0;
513 				case XBox360Axes.horizontalDpad:
514 					return (what.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_LEFT) ? -digitalFallbackValue :
515 					       (what.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) ? digitalFallbackValue :
516 					       0;
517 				case XBox360Axes.lt:
518 					return normalizeTrigger(what.Gamepad.bLeftTrigger);
519 				case XBox360Axes.rt:
520 					return normalizeTrigger(what.Gamepad.bRightTrigger);
521 			}
522 			else version(ps1_style)
523 			final switch(axis) {
524 				case PS1AnalogAxes.horizontalDpad:
525 				case PS1AnalogAxes.horizontalLeftStick:
526 					short got = (what.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_LEFT) ? cast(short)-cast(int)digitalFallbackValue :
527 					       (what.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT) ? digitalFallbackValue :
528 					       0;
529 					if(got == 0)
530 						got = what.Gamepad.sThumbLX;
531 
532 					return normalizeAxis(got);
533 				case PS1AnalogAxes.verticalDpad:
534 				case PS1AnalogAxes.verticalLeftStick:
535 					short got = (what.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_UP) ? digitalFallbackValue :
536 					       (what.Gamepad.wButtons & XINPUT_GAMEPAD_DPAD_DOWN) ? cast(short)-cast(int)digitalFallbackValue :
537 						what.Gamepad.sThumbLY;
538 
539 					if(got == short.min)
540 						got++; // to avoid overflow on the axis inversion below
541 
542 					return normalizeAxis(cast(short)-cast(int)got);
543 				case PS1AnalogAxes.horizontalRightStick:
544 					return normalizeAxis(what.Gamepad.sThumbRX);
545 				case PS1AnalogAxes.verticalRightStick:
546 					return normalizeAxis(what.Gamepad.sThumbRY);
547 			}
548 		}
549 	}
550 
551 	version(Windows)
552 		short normalizeTrigger(BYTE b) {
553 			if(b < XINPUT_GAMEPAD_TRIGGER_THRESHOLD)
554 				return 0;
555 			return cast(short)((b << 8)|0xff);
556 		}
557 }
558 
559 ///
560 JoystickUpdate getJoystickUpdate(int player) {
561 	static JoystickState[4] previous;
562 
563 	version(Windows) {
564 		assert(getJoystickOSState !is null);
565 		if(getJoystickOSState(player, &(joystickState[player])))
566 			return JoystickUpdate();
567 			//throw new Exception("wtf");
568 	}
569 
570 	auto it = JoystickUpdate(player, previous[player], joystickState[player]);
571 
572 	previous[player] = joystickState[player];
573 
574 	return it;
575 }
576 
577 // --------------------------------
578 // Low level interface
579 // --------------------------------
580 
581 version(Windows) {
582 
583 	import core.sys.windows.windows;
584 
585 	alias MMRESULT = UINT;
586 
587 	struct JOYINFOEX {
588 		DWORD dwSize;
589 		DWORD dwFlags;
590 		DWORD dwXpos;
591 		DWORD dwYpos;
592 		DWORD dwZpos;
593 		DWORD dwRpos;
594 		DWORD dwUpos;
595 		DWORD dwVpos;
596 		DWORD dwButtons;
597 		DWORD dwButtonNumber;
598 		DWORD dwPOV;
599 		DWORD dwReserved1;
600 		DWORD dwReserved2;
601 	}
602 
603 	enum  : DWORD {
604 		JOY_POVCENTERED = -1,
605 		JOY_POVFORWARD  = 0,
606 		JOY_POVBACKWARD = 18000,
607 		JOY_POVLEFT     = 27000,
608 		JOY_POVRIGHT    = 9000
609 	}
610 
611 	extern(Windows)
612 	MMRESULT joySetCapture(HWND window, UINT stickId, UINT period, BOOL changed);
613 
614 	extern(Windows)
615 	MMRESULT joyGetPosEx(UINT stickId, JOYINFOEX* pji);
616 
617 	extern(Windows)
618 	MMRESULT joyReleaseCapture(UINT stickId);
619 
620 	// SEE ALSO:
621 	// http://msdn.microsoft.com/en-us/library/windows/desktop/dd757105%28v=vs.85%29.aspx
622 
623 	// Windows also provides joyGetThreshold, joySetThreshold
624 
625 	// there's also JOY2 messages
626 	enum MM_JOY1MOVE = 0; // FIXME
627 	enum MM_JOY1BUTTONDOWN = 0; // FIXME
628 	enum MM_JOY1BUTTONUP = 0; // FIXME
629 
630 	pragma(lib, "winmm");
631 
632 	version(arsd_js_test)
633 	void main() {
634 		/*
635 		// winmm test
636 		auto window = new SimpleWindow(500, 500);
637 
638 		joySetCapture(window.impl.hwnd, 0, 0, false);
639 
640 		window.handleNativeEvent = (HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
641 			import std.stdio;
642 			writeln(msg, " ", wparam, " ", lparam);
643 			return 1;
644 		};
645 
646 		window.eventLoop(0);
647 
648 		joyReleaseCapture(0);
649 		*/
650 
651 		import std.stdio;
652 
653 		// xinput test
654 
655 		WindowsXInput x;
656 		if(!x.loadDll()) {
657 			writeln("Load DLL failed");
658 			return;
659 		}
660 
661 		writeln("success");
662 
663 		assert(x.XInputSetState !is null);
664 		assert(x.XInputGetState !is null);
665 
666 		XINPUT_STATE state;
667 
668 		XINPUT_VIBRATION vibration;
669 
670 		if(!x.XInputGetState(0, &state)) {
671 			writeln("Player 1 detected");
672 		} else return;
673 		if(!x.XInputGetState(1, &state)) {
674 			writeln("Player 2 detected");
675 		} else writeln("Player 2 not found");
676 
677 		DWORD pn;
678 		foreach(i; 0 .. 60) {
679 			x.XInputGetState(0, &state);
680 			if(pn != state.dwPacketNumber) {
681 				writeln("c: ", state);
682 				pn = state.dwPacketNumber;
683 			}
684 			Sleep(50);
685 			if(i == 20) {
686 				vibration.wLeftMotorSpeed = WORD.max;
687 				vibration.wRightMotorSpeed = WORD.max;
688 				x.XInputSetState(0, &vibration);
689 				vibration = XINPUT_VIBRATION.init;
690 			}
691 
692 			if(i == 40)
693 				x.XInputSetState(0, &vibration);
694 		}
695 	}
696 
697 	struct XINPUT_GAMEPAD {
698 		WORD  wButtons;
699 		BYTE  bLeftTrigger;
700 		BYTE  bRightTrigger;
701 		SHORT sThumbLX;
702 		SHORT sThumbLY;
703 		SHORT sThumbRX;
704 		SHORT sThumbRY;
705 	}
706 
707 	// enum XInputGamepadButtons {
708 	// It is a bitmask of these
709 	enum XINPUT_GAMEPAD_DPAD_UP =	0x0001;
710 	enum XINPUT_GAMEPAD_DPAD_DOWN =	0x0002;
711 	enum XINPUT_GAMEPAD_DPAD_LEFT =	0x0004;
712 	enum XINPUT_GAMEPAD_DPAD_RIGHT =	0x0008;
713 	enum XINPUT_GAMEPAD_START =	0x0010;
714 	enum XINPUT_GAMEPAD_BACK =	0x0020;
715 	enum XINPUT_GAMEPAD_LEFT_THUMB =	0x0040; // pushing on the stick
716 	enum XINPUT_GAMEPAD_RIGHT_THUMB =	0x0080;
717 	enum XINPUT_GAMEPAD_LEFT_SHOULDER =	0x0100;
718 	enum XINPUT_GAMEPAD_RIGHT_SHOULDER =	0x0200;
719 	enum XINPUT_GAMEPAD_A =	0x1000;
720 	enum XINPUT_GAMEPAD_B =	0x2000;
721 	enum XINPUT_GAMEPAD_X =	0x4000;
722 	enum XINPUT_GAMEPAD_Y =	0x8000;
723 
724 	enum XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE =  7849;
725 	enum XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE = 8689;
726 	enum XINPUT_GAMEPAD_TRIGGER_THRESHOLD =   30;
727 
728 	struct XINPUT_STATE {
729 		DWORD dwPacketNumber;
730 		XINPUT_GAMEPAD Gamepad;
731 	}
732 
733 	struct XINPUT_VIBRATION {
734 		WORD wLeftMotorSpeed; // low frequency motor. use any value between 0-65535 here
735 		WORD wRightMotorSpeed; // high frequency motor. use any value between 0-65535 here
736 	}
737 
738 	struct WindowsXInput {
739 		HANDLE dll;
740 		bool loadDll() {
741 			// try Windows 8 first
742 			dll = LoadLibraryA("Xinput1_4.dll");
743 			if(dll is null) // then try Windows Vista
744 				dll = LoadLibraryA("Xinput9_1_0.dll");
745 
746 			if(dll is null)
747 				return false; // couldn't load it, tell user
748 
749 			XInputGetState = cast(typeof(XInputGetState)) GetProcAddress(dll, "XInputGetState");
750 			XInputSetState = cast(typeof(XInputSetState)) GetProcAddress(dll, "XInputSetState");
751 
752 			return true;
753 		}
754 
755 		~this() {
756 			unloadDll();
757 		}
758 
759 		void unloadDll() {
760 			if(dll !is null) {
761 				FreeLibrary(dll);
762 				dll = null;
763 			}
764 		}
765 
766 		// These are all dynamically loaded from the DLL
767 		extern(Windows) {
768 			DWORD function(DWORD, XINPUT_STATE*) XInputGetState;
769 			DWORD function(DWORD, XINPUT_VIBRATION*) XInputSetState;
770 		}
771 
772 		// there's other functions but I don't use them; my controllers
773 		// are corded, for example, and I don't have a headset that works
774 		// with them. But if I get ones, I'll add them too.
775 		//
776 		// There's also some Windows 8 and up functions I didn't use, I just
777 		// wanted the basics.
778 	}
779 }
780 
781 version(linux) {
782 
783 	// https://www.kernel.org/doc/Documentation/input/joystick-api.txt
784 	struct js_event {
785 		uint time;
786 		short value;
787 		ubyte type;
788 		ubyte number;
789 	}
790 
791 	enum JS_EVENT_BUTTON = 0x01;
792 	enum JS_EVENT_AXIS   = 0x02;
793 	enum JS_EVENT_INIT   = 0x80;
794 
795 	import core.sys.posix.unistd;
796 	import core.sys.posix.fcntl;
797 
798 	import std.stdio;
799 
800 	struct RawControllerEvent {
801 		int controller;
802 		int type;
803 		int number;
804 		int value;
805 	}
806 
807 	// These values are determined experimentally on my Linux box
808 	// and won't necessarily match what you have. I really don't know.
809 	// TODO: see if these line up on Windows
810 }
811 
812 	// My hardware:
813 	// a Sony PS1 dual shock controller on a PSX to USB adapter from Radio Shack
814 	// and a wired XBox 360 controller from Microsoft.
815 
816 	// FIXME: these are the values based on my linux box, but I also use them as the virtual codes
817 	// I want nicer virtual codes I think.
818 
819 	enum PS1Buttons {
820 		triangle = 0,
821 		circle,
822 		cross,
823 		square,
824 		l2,
825 		r2,
826 		l1,
827 		r1,
828 		select,
829 		start,
830 		l3,
831 		r3
832 	}
833 
834 	// Use if analog is turned off
835 	// Tip: if you just check this OR the analog one it will work in both cases easily enough
836 	enum PS1Axes {
837 		horizontalDpad = 0,
838 		verticalDpad = 1,
839 	}
840 
841 	// Use if analog is turned on
842 	enum PS1AnalogAxes {
843 		horizontalLeftStick = 0,
844 		verticalLeftStick,
845 		verticalRightStick,
846 		horizontalRightStick,
847 		horizontalDpad,
848 		verticalDpad,
849 	}
850 
851 
852 	enum XBox360Buttons {
853 		a = 0,
854 		b,
855 		x,
856 		y,
857 		lb,
858 		rb,
859 		back,
860 		start,
861 		xboxLogo,
862 		leftStick,
863 		rightStick
864 	}
865 
866 	enum XBox360Axes {
867 		horizontalLeftStick = 0,
868 		verticalLeftStick,
869 		lt,
870 		horizontalRightStick,
871 		verticalRightStick,
872 		rt,
873 		horizontalDpad,
874 		verticalDpad
875 	}
876 
877 version(linux) {
878 
879 	version(arsd_js_test)
880 	void main(string[] args) {
881 		int fd = open(args.length > 1 ? (args[1]~'\0').ptr : "/dev/input/js0".ptr, O_RDONLY);
882 		assert(fd > 0);
883 		js_event event;
884 
885 		short[8] axes;
886 		ubyte[16] buttons;
887 
888 		printf("\n");
889 
890 		while(true) {
891 			auto r = read(fd, &event, event.sizeof);
892 			assert(r == event.sizeof);
893 
894 			// writef("\r%12s", event);
895 			if(event.type & JS_EVENT_AXIS) {
896 				axes[event.number] = event.value >> 12;
897 			}
898 			if(event.type & JS_EVENT_BUTTON) {
899 				buttons[event.number] = cast(ubyte) event.value;
900 			}
901 			writef("\r%6s %1s", axes[0..8], buttons[0 .. 16]);
902 			stdout.flush();
903 		}
904 
905 		close(fd);
906 		printf("\n");
907 	}
908 
909 	version(joystick_demo)
910 	version(linux)
911 	void amain(string[] args) {
912 		import arsd.simpleaudio;
913 
914 		AudioOutput audio = AudioOutput(0);
915 
916 		int fd = open(args.length > 1 ? (args[1]~'\0').ptr : "/dev/input/js1".ptr, O_RDONLY | O_NONBLOCK);
917 		assert(fd > 0);
918 		js_event event;
919 
920 		short[512] buffer;
921 
922 		short val = short.max / 4;
923 		int swap = 44100 / 600;
924 		int swapCount = swap / 2;
925 
926 		short val2 = short.max / 4;
927 		int swap2 = 44100 / 600;
928 		int swapCount2 = swap / 2;
929 
930 		short[8] axes;
931 		ubyte[16] buttons;
932 
933 		while(true) {
934 			int r = read(fd, &event, event.sizeof);
935 			while(r >= 0) {
936 				import std.conv;
937 				assert(r == event.sizeof, to!string(r));
938 
939 				// writef("\r%12s", event);
940 				if(event.type & JS_EVENT_AXIS) {
941 					axes[event.number] = event.value; //  >> 12;
942 				}
943 				if(event.type & JS_EVENT_BUTTON) {
944 					buttons[event.number] = cast(ubyte) event.value;
945 				}
946 
947 
948 				int freq = axes[XBox360Axes.horizontalLeftStick];
949 				freq += short.max;
950 				freq /= 100;
951 				freq += 400;
952 
953 				swap = 44100 / freq;
954 
955 				val = (cast(int) axes[XBox360Axes.lt] + short.max) / 8;
956 
957 
958 				int freq2 = axes[XBox360Axes.horizontalRightStick];
959 				freq2 += short.max;
960 				freq2 /= 1000;
961 				freq2 += 400;
962 
963 				swap2 = 44100 / freq2;
964 
965 				val2 = (cast(int) axes[XBox360Axes.rt] + short.max) / 8;
966 
967 
968 				// try to starve the read
969 				r = read(fd, &event, event.sizeof);
970 			}
971 
972 			for(int i = 0; i < buffer.length / 2; i++) {
973 			import std.math;
974 				auto v = cast(ushort) (val * sin(cast(real) swapCount / (2*PI)));
975 				auto v2 = cast(ushort) (val2 * sin(cast(real) swapCount2 / (2*PI)));
976 				buffer[i*2] = cast(ushort)(v + v2);
977 				buffer[i*2+1] = cast(ushort)(v + v2);
978 				swapCount--;
979 				swapCount2--;
980 				if(swapCount == 0) {
981 					swapCount = swap / 2;
982 					// val = -val;
983 				}
984 				if(swapCount2 == 0) {
985 					swapCount2 = swap2 / 2;
986 					// val = -val;
987 				}
988 			}
989 
990 
991 			//audio.write(buffer[]);
992 		}
993 
994 		close(fd);
995 	}
996 }
997 
998 
999 
1000 	version(joystick_demo)
1001 	version(Windows)
1002 	void amain() {
1003 		import arsd.simpleaudio;
1004 		auto midi = MidiOutput(0);
1005 		ubyte[16] buffer = void;
1006 		ubyte[] where = buffer[];
1007 		midi.writeRawMessageData(where.midiProgramChange(1, 79));
1008 
1009 		auto x = WindowsXInput();
1010 		x.loadDll();
1011 
1012 		XINPUT_STATE state;
1013 		XINPUT_STATE oldstate;
1014 		DWORD pn;
1015 		while(true) {
1016 			oldstate = state;
1017 			x.XInputGetState(0, &state);
1018 			byte note = 72;
1019 			if(state.dwPacketNumber != oldstate.dwPacketNumber) {
1020 				if((state.Gamepad.wButtons & XINPUT_GAMEPAD_A) && !(oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_A))
1021 					midi.writeRawMessageData(where.midiNoteOn(1, note, 127));
1022 				if(!(state.Gamepad.wButtons & XINPUT_GAMEPAD_A) && (oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_A))
1023 					midi.writeRawMessageData(where.midiNoteOff(1, note, 127));
1024 
1025 				note = 75;
1026 
1027 				if((state.Gamepad.wButtons & XINPUT_GAMEPAD_B) && !(oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_B))
1028 					midi.writeRawMessageData(where.midiNoteOn(1, note, 127));
1029 				if(!(state.Gamepad.wButtons & XINPUT_GAMEPAD_B) && (oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_B))
1030 					midi.writeRawMessageData(where.midiNoteOff(1, note, 127));
1031 
1032 				note = 77;
1033 
1034 				if((state.Gamepad.wButtons & XINPUT_GAMEPAD_X) && !(oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_X))
1035 					midi.writeRawMessageData(where.midiNoteOn(1, note, 127));
1036 				if(!(state.Gamepad.wButtons & XINPUT_GAMEPAD_X) && (oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_X))
1037 					midi.writeRawMessageData(where.midiNoteOff(1, note, 127));
1038 
1039 				note = 79;
1040 				if((state.Gamepad.wButtons & XINPUT_GAMEPAD_Y) && !(oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_Y))
1041 					midi.writeRawMessageData(where.midiNoteOn(1, note, 127));
1042 				if(!(state.Gamepad.wButtons & XINPUT_GAMEPAD_Y) && (oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_Y))
1043 					midi.writeRawMessageData(where.midiNoteOff(1, note, 127));
1044 
1045 				note = 81;
1046 				if((state.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) && !(oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER))
1047 					midi.writeRawMessageData(where.midiNoteOn(1, note, 127));
1048 				if(!(state.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) && (oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER))
1049 					midi.writeRawMessageData(where.midiNoteOff(1, note, 127));
1050 
1051 				note = 83;
1052 				if((state.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) && !(oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER))
1053 					midi.writeRawMessageData(where.midiNoteOn(1, note, 127));
1054 				if(!(state.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) && (oldstate.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER))
1055 					midi.writeRawMessageData(where.midiNoteOff(1, note, 127));
1056 			}
1057 
1058 			Sleep(1);
1059 
1060 			where = buffer[];
1061 		}
1062 	}
1063 
Suggestion Box / Bug Report