Alex Szpakowski avatar Alex Szpakowski committed 7bdd2f6

Added joystick vibration support for most common controllers (resolves issue #159).

Added Joystick:setVibration(left, right), Joystick:getVibration, and Joystick:isVibrationSupported.
left and right are numbers between 0 and 1 which correspond to the strength of the gamepad's left and right rumble motors, if it has them.

Comments (1)

Files changed (9)

src/modules/filesystem/physfs/Filesystem.cpp

 {
 	std::string getDriveRoot(const std::string &input)
 	{
-		for (int i = 0; i < input.size(); ++i)
+		for (size_t i = 0; i < input.size(); ++i)
 			if (input[i] == '/' || input[i] == '\\')
 				return input.substr(0, i+1);
 		// Something's horribly wrong

src/modules/joystick/Joystick.h

 	virtual int getInstanceID() const = 0;
 	virtual int getID() const = 0;
 
+	virtual bool isVibrationSupported() = 0;
+	virtual bool setVibration(float left, float right) = 0;
+	virtual bool setVibration() = 0;
+	virtual void getVibration(float &left, float &right) const = 0;
+
 	static bool getConstant(const char *in, Hat &out);
 	static bool getConstant(Hat in, const char *&out);
 

src/modules/joystick/sdl/Joystick.cpp

  * 3. This notice may not be removed or altered from any source distribution.
  **/
 
+// LOVE
+#include "common/config.h"
 #include "Joystick.h"
+#include "common/int.h"
+
+// C++
+#include <algorithm>
 
 namespace love
 {
 Joystick::Joystick(int id)
 	: joyhandle(0)
 	, controller(0)
+	, haptic(0)
 	, instanceid(-1)
 	, id(id)
+	, vibration()
 {
 }
 
 Joystick::Joystick(int id, int joyindex)
 	: joyhandle(0)
 	, controller(0)
+	, haptic(0)
 	, instanceid(-1)
 	, id(id)
+	, vibration()
 {
 	open(joyindex);
 }
 
 void Joystick::close()
 {
+	if (haptic)
+	{
+		SDL_HapticRumbleStop(haptic);
+		SDL_HapticClose(haptic);
+	}
+
 	if (controller)
 		SDL_GameControllerClose(controller);
 
 
 	joyhandle = 0;
 	controller = 0;
+	haptic = 0;
 	instanceid = -1;
+	vibration = Vibration();
 }
 
 bool Joystick::isConnected() const
 	return id;
 }
 
+bool Joystick::checkCreateHaptic()
+{
+	if (!isConnected())
+		return false;
+
+	if (!SDL_WasInit(SDL_INIT_HAPTIC) && SDL_InitSubSystem(SDL_INIT_HAPTIC) < 0)
+		return false;
+
+	if (haptic && SDL_HapticIndex(haptic) != -1)
+		return true;
+
+	if (haptic)
+	{
+		SDL_HapticClose(haptic);
+		haptic = 0;
+	}
+
+	haptic = SDL_HapticOpenFromJoystick(joyhandle);
+	vibration = Vibration();
+
+	return haptic != 0;
+}
+
+bool Joystick::isVibrationSupported()
+{
+	if (!checkCreateHaptic())
+		return false;
+
+	unsigned int features = SDL_HapticQuery(haptic);
+
+	if ((features & SDL_HAPTIC_LEFTRIGHT) != 0)
+		return true;
+
+	// Some gamepad drivers only support left/right motors via a custom effect.
+	if (isGamepad() && (features & SDL_HAPTIC_CUSTOM) != 0)
+		return true;
+
+	// Check SDL's simple rumble as a last resort.
+	if (SDL_HapticRumbleSupported(haptic) == 1)
+		return true;
+
+	return false;
+}
+
+bool Joystick::runVibrationEffect()
+{
+	if (vibration.id != -1)
+	{
+		if (SDL_HapticUpdateEffect(haptic, vibration.id, &vibration.effect) == 0)
+		{
+			if (SDL_HapticRunEffect(haptic, vibration.id, 1) != -1)
+				return true;
+		}
+
+		// If the effect fails to update, we should destroy and re-create it.
+		SDL_HapticDestroyEffect(haptic, vibration.id);
+		vibration.id = -1;
+	}
+
+	vibration.id = SDL_HapticNewEffect(haptic, &vibration.effect);
+
+	if (vibration.id != -1 && SDL_HapticRunEffect(haptic, vibration.id, 1) != -1)
+		return true;
+	
+	return false;
+}
+
+bool Joystick::setVibration(float left, float right)
+{
+	// TODO: support non-infinite durations? The working Tattiebogle Xbox
+	// controller driver in OS X seems to ignore durations under 1 second.
+
+	left = std::min(std::max(left, 0.0f), 1.0f);
+	right = std::min(std::max(right, 0.0f), 1.0f);
+
+	if (left == 0.0f && right == 0.0f)
+		return setVibration();
+
+	if (!checkCreateHaptic())
+		return false;
+
+	bool success = false;
+	unsigned int features = SDL_HapticQuery(haptic);
+
+	if ((features & SDL_HAPTIC_LEFTRIGHT) != 0)
+	{
+		memset(&vibration.effect, 0, sizeof(SDL_HapticEffect));
+		vibration.effect.type = SDL_HAPTIC_LEFTRIGHT;
+
+		// SDL's Xinput code has an int. overflow bug with SDL_HAPTIC_INFINITY.
+		const Uint32 long_time = 1000 * 60 * 60 * 2;
+
+		vibration.effect.leftright.length = long_time;
+		vibration.effect.leftright.large_magnitude = Uint16(left * LOVE_UINT16_MAX);
+		vibration.effect.leftright.small_magnitude = Uint16(right * LOVE_UINT16_MAX);
+
+		success = runVibrationEffect();
+	}
+
+	// Some gamepad drivers only give support for controlling the motors via
+	// a custom FF effect.
+	if (!success && isGamepad() && (features & SDL_HAPTIC_CUSTOM) != 0)
+	{
+		// NOTE: this may cause issues with drivers which support custom effects
+		// but aren't similar to https://github.com/d235j/360Controller .
+
+		// Custom effect data is clamped to 0x7FFF in SDL.
+		vibration.data[0] = vibration.data[2] = Uint16(left * 0x7FFF);
+		vibration.data[1] = vibration.data[3] = Uint16(right * 0x7FFF);
+
+		memset(&vibration.effect, 0, sizeof(SDL_HapticEffect));
+		vibration.effect.type = SDL_HAPTIC_CUSTOM;
+
+		vibration.effect.custom.length = SDL_HAPTIC_INFINITY;
+		vibration.effect.custom.channels = 2;
+		vibration.effect.custom.period = 10;
+		vibration.effect.custom.samples = 2;
+		vibration.effect.custom.data = vibration.data;
+
+		success = runVibrationEffect();
+	}
+
+	// Fall back to a simple rumble if all else fails. SDL's simple rumble API
+	// only supports a single strength value.
+	if (!success && SDL_HapticRumbleInit(haptic) == 0)
+	{
+		float strength = std::max(left, right);
+		int played = SDL_HapticRumblePlay(haptic, strength, SDL_HAPTIC_INFINITY);
+		success = (played == 0);
+	}
+
+	if (success)
+	{
+		vibration.left = left;
+		vibration.right = right;
+	}
+
+	return success;
+}
+
+bool Joystick::setVibration()
+{
+	bool success = true;
+
+	if (SDL_WasInit(SDL_INIT_HAPTIC) && haptic && SDL_HapticIndex(haptic) != -1)
+	{
+		// Stop all playing effects on the haptic device.
+		// FIXME: We should only stop the vibration effect, in case we use the
+		// Haptic API for other things in the future.
+		success = (SDL_HapticStopAll(haptic) == 0);
+	}
+
+	if (success)
+		vibration.left = vibration.right = 0.0f;
+
+	return success;
+}
+
+void Joystick::getVibration(float &left, float &right) const
+{
+	left = vibration.left;
+	right = vibration.right;
+}
+
 bool Joystick::getConstant(Uint8 in, Joystick::Hat &out)
 {
 	return hats.find(in, out);

src/modules/joystick/sdl/Joystick.h

 #include "common/EnumMap.h"
 
 // SDL
-#include <SDL_joystick.h>
-#include <SDL_gamecontroller.h>
+#include <SDL.h>
 
 namespace love
 {
 	int getInstanceID() const;
 	int getID() const;
 
+	bool isVibrationSupported();
+	bool setVibration(float left, float right);
+	bool setVibration();
+	void getVibration(float &left, float &right) const;
+
 	static bool getConstant(Hat in, Uint8 &out);
 	static bool getConstant(Uint8 in, Hat &out);
 
 
 	Joystick() {}
 
+	bool checkCreateHaptic();
+	bool runVibrationEffect();
+
 	SDL_Joystick *joyhandle;
 	SDL_GameController *controller;
+	SDL_Haptic *haptic;
 
 	SDL_JoystickID instanceid;
 	std::string pguid;
 
 	std::string name;
 
+	struct Vibration
+	{
+		float left, right;
+		SDL_HapticEffect effect;
+		Uint16 data[4];
+		int id;
+
+		Vibration()
+			: left(0.0f), right(0.0f), effect(), data(), id(-1)
+		{}
+
+	} vibration;
+
 	static EnumMap<Hat, Uint8, Joystick::HAT_MAX_ENUM>::Entry hatEntries[];
 	static EnumMap<Hat, Uint8, Joystick::HAT_MAX_ENUM> hats;
 

src/modules/joystick/sdl/JoystickModule.cpp

 		(*it)->release();
 	}
 
+	if (SDL_WasInit(SDL_INIT_HAPTIC) != 0)
+		SDL_QuitSubSystem(SDL_INIT_HAPTIC);
+
 	SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER);
 }
 

src/modules/joystick/sdl/wrap_Joystick.cpp

 	return 1;
 }
 
+int w_Joystick_isVibrationSupported(lua_State *L)
+{
+	Joystick *j = luax_checkjoystick(L, 1);
+	luax_pushboolean(L, j->isVibrationSupported());
+	return 1;
+}
+
+int w_Joystick_setVibration(lua_State *L)
+{
+	Joystick *j = luax_checkjoystick(L, 1);
+	bool success = false;
+
+	if (lua_isnoneornil(L, 2))
+	{
+		// Disable joystick vibration if no argument is given.
+		success = j->setVibration();
+	}
+	else
+	{
+		float left = (float) luaL_checknumber(L, 2);
+		float right = (float) luaL_optnumber(L, 3, left);
+		success = j->setVibration(left, right);
+	}
+
+	luax_pushboolean(L, success);
+	return 1;
+}
+
+int w_Joystick_getVibration(lua_State *L)
+{
+	Joystick *j = luax_checkjoystick(L, 1);
+	float left, right;
+	j->getVibration(left, right);
+	lua_pushnumber(L, left);
+	lua_pushnumber(L, right);
+	return 2;
+}
+
 // List of functions to wrap.
 static const luaL_Reg functions[] =
 {
 	{ "getAxes", w_Joystick_getAxes },
 	{ "getHat", w_Joystick_getHat },
 	{ "isDown", w_Joystick_isDown },
+
 	{ "isGamepad", w_Joystick_isGamepad },
 	{ "getGamepadAxis", w_Joystick_getGamepadAxis },
 	{ "isGamepadDown", w_Joystick_isGamepadDown },
 
+	{ "isVibrationSupported", w_Joystick_isVibrationSupported },
+	{ "setVibration", w_Joystick_setVibration },
+	{ "getVibration", w_Joystick_getVibration },
+
 	// From wrap_JoystickModule.
 	{ "getConnectedIndex", w_getIndex },
 	{ "getGamepadMapping", w_getGamepadMapping },

src/modules/joystick/sdl/wrap_Joystick.h

 int w_Joystick_isGamepad(lua_State *L);
 int w_Joystick_getGamepadAxis(lua_State *L);
 int w_Joystick_isGamepadDown(lua_State *L);
+int w_Joystick_isVibrationSupported(lua_State *L);
+int w_Joystick_setVibration(lua_State *L);
+int w_Joystick_getVibration(lua_State *L);
 extern "C" int luaopen_joystick(lua_State *L);
 
 } // sdl

src/scripts/boot.lua

 		end
 	end
 
-	-- Load.
+	-- Reset state.
 	if love.mouse then
 		love.mouse.setVisible(true)
 		love.mouse.setGrabbed(false)
 	end
+	if love.joystick then
+		-- Stop all joystick vibrations.
+		for i,v in ipairs(love.joystick.getJoysticks()) do
+			v:setVibration()
+		end
+	end
 	if love.audio then love.audio.stop() end
 	love.graphics.reset()
 	love.graphics.setBackgroundColor(89, 157, 220)

src/scripts/boot.lua.h

 	0x09, 0x09, 0x09, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x0a,
 	0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x65, 0x6e, 0x64, 0x0a,
-	0x09, 0x2d, 0x2d, 0x20, 0x4c, 0x6f, 0x61, 0x64, 0x2e, 0x0a,
+	0x09, 0x2d, 0x2d, 0x20, 0x52, 0x65, 0x73, 0x65, 0x74, 0x20, 0x73, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x0a,
 	0x09, 0x69, 0x66, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x20, 0x74, 0x68, 0x65, 
 	0x6e, 0x0a,
 	0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x2e, 0x73, 0x65, 0x74, 0x56, 0x69, 
 	0x09, 0x09, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x6d, 0x6f, 0x75, 0x73, 0x65, 0x2e, 0x73, 0x65, 0x74, 0x47, 0x72, 
 	0x61, 0x62, 0x62, 0x65, 0x64, 0x28, 0x66, 0x61, 0x6c, 0x73, 0x65, 0x29, 0x0a,
 	0x09, 0x65, 0x6e, 0x64, 0x0a,
+	0x09, 0x69, 0x66, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x6a, 0x6f, 0x79, 0x73, 0x74, 0x69, 0x63, 0x6b, 0x20, 
+	0x74, 0x68, 0x65, 0x6e, 0x0a,
+	0x09, 0x09, 0x2d, 0x2d, 0x20, 0x53, 0x74, 0x6f, 0x70, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x6a, 0x6f, 0x79, 0x73, 
+	0x74, 0x69, 0x63, 0x6b, 0x20, 0x76, 0x69, 0x62, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x0a,
+	0x09, 0x09, 0x66, 0x6f, 0x72, 0x20, 0x69, 0x2c, 0x76, 0x20, 0x69, 0x6e, 0x20, 0x69, 0x70, 0x61, 0x69, 0x72, 
+	0x73, 0x28, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x6a, 0x6f, 0x79, 0x73, 0x74, 0x69, 0x63, 0x6b, 0x2e, 0x67, 0x65, 
+	0x74, 0x4a, 0x6f, 0x79, 0x73, 0x74, 0x69, 0x63, 0x6b, 0x73, 0x28, 0x29, 0x29, 0x20, 0x64, 0x6f, 0x0a,
+	0x09, 0x09, 0x09, 0x76, 0x3a, 0x73, 0x65, 0x74, 0x56, 0x69, 0x62, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x28, 
+	0x29, 0x0a,
+	0x09, 0x09, 0x65, 0x6e, 0x64, 0x0a,
+	0x09, 0x65, 0x6e, 0x64, 0x0a,
 	0x09, 0x69, 0x66, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x20, 0x74, 0x68, 0x65, 
 	0x6e, 0x20, 0x6c, 0x6f, 0x76, 0x65, 0x2e, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x2e, 0x73, 0x74, 0x6f, 0x70, 0x28, 
 	0x29, 0x20, 0x65, 0x6e, 0x64, 0x0a,
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.