// // Copyright(C) 2005-2014 Simon Howard // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // #include #include #include "doomtype.h" #include "i_joystick.h" #include "m_config.h" #include "m_controls.h" #include "m_misc.h" #include "textscreen.h" #include "execute.h" #include "joystick.h" #include "mode.h" #include "txt_joybinput.h" typedef enum { CALIBRATE_CENTER, CALIBRATE_LEFT, CALIBRATE_UP, // These are only used when defining button axes: CALIBRATE_RIGHT, CALIBRATE_DOWN, } calibration_stage_t; typedef struct { char *name; // Config file name int value; } joystick_config_t; typedef struct { char *name; int axes, buttons, hats; const joystick_config_t *configs; } known_joystick_t; // SDL joystick successfully initialized? static int joystick_initted = 0; // Joystick enable/disable static int usejoystick = 0; // Joystick to use, as an SDL joystick index: int joystick_index = -1; // Calibration button. This is the button the user pressed at the // start of the calibration sequence. They *must* press this button // for each subsequent sequence. static int calibrate_button = -1; // "Bad" joystick axes. Sometimes an axis can be stuck or "bad". An // example I found is that if you unplug the nunchuck extension from // a Wii remote, the axes from the nunchuck can be stuck at one of // the maximum values. These have to be ignored, so when we ask the // user to center the joystick, we look for bad axes that are not // close to zero. static boolean *bad_axis = NULL; // Which joystick axis to use for horizontal movement, and whether to // invert the direction: static int joystick_x_axis = 0; static int joystick_x_invert = 0; // Which joystick axis to use for vertical movement, and whether to // invert the direction: static int joystick_y_axis = 1; static int joystick_y_invert = 0; // Strafe axis. static int joystick_strafe_axis = -1; static int joystick_strafe_invert = 0; // Virtual to physical mapping. int joystick_physical_buttons[NUM_VIRTUAL_BUTTONS] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; static txt_button_t *joystick_button; // // Calibration // static txt_window_t *calibration_window; static txt_label_t *calibration_label; static calibration_stage_t calibrate_stage; static SDL_Joystick **all_joysticks = NULL; // Known controllers. // There are lots of game controllers on the market. Try to configure // them in a consistent way: // // * Use the D-pad rather than an analog stick. left/right turns the // player, up/down moves forward/backward - ie. a "traditional" // layout like Vanilla Doom rather than something more elaborate. // * No strafe axis. // * Fire and run keys together, on the main right-side buttons, // ideally arranged so both can be controlled/covered simultaneously // with the thumb. // * Jump/use keys in the same cluster if possible. // * Strafe left/right configured to map to shoulder buttons if they // are present. No "strafe on" key unless shoulder buttons not present. // * If a second set of shoulder buttons are also present, these map // to prev weapon/next weapon. // * Menu button mapped to start button. // // With the common right-side button arrangement that looks like this, // which is similar to the Vanilla default configuration when using // a Gravis Gamepad: // // B A = Fire // A D B = Jump // C C = Speed // D = Use // Always loaded before others, to get a known starting configuration. static const joystick_config_t empty_defaults[] = { {"joystick_x_axis", -1}, {"joystick_x_invert", 0}, {"joystick_y_axis", -1}, {"joystick_y_invert", 0}, {"joystick_strafe_axis", -1}, {"joystick_strafe_invert", 0}, {"joyb_fire", -1}, {"joyb_use", -1}, {"joyb_strafe", -1}, {"joyb_speed", -1}, {"joyb_strafeleft", -1}, {"joyb_straferight", -1}, {"joyb_prevweapon", -1}, {"joyb_nextweapon", -1}, {"joyb_menu_activate", -1}, {NULL, 0}, }; static const joystick_config_t ps3_controller[] = { {"joystick_x_axis", CREATE_BUTTON_AXIS(7, 5)}, {"joystick_y_axis", CREATE_BUTTON_AXIS(4, 6)}, {"joyb_fire", 15}, // Square {"joyb_speed", 14}, // X {"joyb_use", 13}, // Circle {"joyb_jump", 12}, // Triangle {"joyb_strafeleft", 8}, // Bottom shoulder buttons {"joyb_straferight", 9}, {"joyb_prevweapon", 10}, // Top shoulder buttons {"joyb_nextweapon", 11}, {"joyb_menu_activate", 3}, // Start {NULL, 0}, }; static const joystick_config_t airflo_controller[] = { {"joystick_x_axis", CREATE_HAT_AXIS(0, HAT_AXIS_HORIZONTAL)}, {"joystick_y_axis", CREATE_HAT_AXIS(0, HAT_AXIS_VERTICAL)}, {"joyb_fire", 2}, // "3" {"joyb_speed", 0}, // "1" {"joyb_jump", 3}, // "4" {"joyb_use", 1}, // "2" {"joyb_strafeleft", 6}, // Bottom shoulder buttons {"joyb_straferight", 7}, {"joyb_prevweapon", 4}, // Top shoulder buttons {"joyb_nextweapon", 5}, {"joyb_menu_activate", 9}, // "10", where "Start" usually is. {NULL, 0}, }; // Wii controller is weird, so we have to take some liberties. // Also it's not a HID device, so it won't appear the same everywhere. // Assume there is no nunchuk or classic controller attached. // When using WJoy on OS X. static const joystick_config_t wii_controller_wjoy[] = { {"joystick_x_axis", CREATE_BUTTON_AXIS(2, 3)}, {"joystick_y_axis", CREATE_BUTTON_AXIS(1, 0)}, {"joyb_fire", 9}, // Button 1 {"joyb_speed", 10}, // Button 2 {"joyb_use", 5}, // Button B (trigger) {"joyb_prevweapon", 7}, // - {"joyb_nextweapon", 6}, // + {"joyb_menu_activate", 9}, // Button A {NULL, 0}, }; static const known_joystick_t known_joysticks[] = { { "PLAYSTATION(R)3 Controller", 4, 19, 0, ps3_controller, }, { "AIRFLO ", 4, 13, 1, airflo_controller, }, { "Wiimote (*", // WJoy includes the Wiimote MAC address. 6, 26, 0, wii_controller_wjoy, }, }; static const known_joystick_t *GetJoystickType(int index) { SDL_Joystick *joystick; const char *name; int axes, buttons, hats; int i; joystick = all_joysticks[index]; name = SDL_JoystickName(index); axes = SDL_JoystickNumAxes(joystick); buttons = SDL_JoystickNumButtons(joystick); hats = SDL_JoystickNumHats(joystick); for (i = 0; i < arrlen(known_joysticks); ++i) { // Check for a name match. If the name ends in '*', this means // ignore the rest. if (M_StringEndsWith(known_joysticks[i].name, "*")) { if (strncmp(known_joysticks[i].name, name, strlen(known_joysticks[i].name) - 1) != 0) { continue; } } else { if (strcmp(known_joysticks[i].name, name) != 0) { continue; } } if (known_joysticks[i].axes == axes && known_joysticks[i].buttons == buttons && known_joysticks[i].hats == hats) { return &known_joysticks[i]; } } printf("Unknown joystick '%s' with %i axes, %i buttons, %i hats\n", name, axes, buttons, hats); printf("Please consider sending in details about your gamepad!\n"); return NULL; } // Query if the joystick at the given index is a known joystick type. static boolean IsKnownJoystick(int index) { return GetJoystickType(index) != NULL; } // Load a configuration set. static void LoadConfigurationSet(const joystick_config_t *configs) { const joystick_config_t *config; char buf[10]; int button; int i; button = 0; for (i = 0; configs[i].name != NULL; ++i) { config = &configs[i]; // Don't overwrite autorun if it is set. if (!strcmp(config->name, "joyb_speed") && joybspeed >= 20) { continue; } // For buttons, set the virtual button mapping as well. if (M_StringStartsWith(config->name, "joyb_") && config->value >= 0) { joystick_physical_buttons[button] = config->value; M_snprintf(buf, sizeof(buf), "%i", button); M_SetVariable(config->name, buf); ++button; } else { M_snprintf(buf, sizeof(buf), "%i", config->value); M_SetVariable(config->name, buf); } } } // Load configuration for joystick_index based on known types. static void LoadKnownConfiguration(void) { const known_joystick_t *jstype; jstype = GetJoystickType(joystick_index); if (jstype == NULL) { return; } LoadConfigurationSet(empty_defaults); LoadConfigurationSet(jstype->configs); } // Set the label showing the name of the currently selected joystick static void SetJoystickButtonLabel(void) { char *name; name = "None set"; if (joystick_initted && joystick_index >= 0 && joystick_index < SDL_NumJoysticks()) { name = (char *) SDL_JoystickName(joystick_index); } TXT_SetButtonLabel(joystick_button, name); } // Try to open all joysticks visible to SDL. static int OpenAllJoysticks(void) { int i; int num_joysticks; int result; if (!joystick_initted) { return 0; } // SDL_JoystickOpen() all joysticks. num_joysticks = SDL_NumJoysticks(); all_joysticks = malloc(sizeof(SDL_Joystick *) * num_joysticks); result = 0; for (i=0; i best_value) { best_value = abs(axis_value); best_invert = axis_value > 0; best_axis = i; } } // Did we find one axis that had a significant value? if (best_value > 32768 / 4) { // Save the best values we have found *axis_index = best_axis; *axis_invert = best_invert; return true; } // Otherwise, maybe this is a "button axis", like the PS3 SIXAXIS // controller that exposes the D-pad as four individual buttons. // Search for a button. i = FindPressedAxisButton(); if (i >= 0) { *axis_index = CREATE_BUTTON_AXIS(i, 0); *axis_invert = 0; return true; } // Maybe it's a D-pad that is presented as a hat. This sounds weird // but gamepads like this really do exist; an example is the // Nyko AIRFLO Ex. i = FindUncenteredHat(axis_invert); if (i >= 0) { *axis_index = i; return true; } // User pressed the button without pushing the joystick anywhere. return false; } static boolean SetButtonAxisPositive(int *axis_index) { int button; button = FindPressedAxisButton(); if (button >= 0) { *axis_index |= CREATE_BUTTON_AXIS(0, button); return true; } return false; } static int NextCalibrateStage(void) { switch (calibrate_stage) { case CALIBRATE_CENTER: return CALIBRATE_LEFT; // After pushing to the left, there are two possibilities: // either it is a button axis, in which case we need to find // the other button, or we can just move on to the next axis. case CALIBRATE_LEFT: if (IS_BUTTON_AXIS(joystick_x_axis)) { return CALIBRATE_RIGHT; } else { return CALIBRATE_UP; } case CALIBRATE_RIGHT: return CALIBRATE_UP; case CALIBRATE_UP: if (IS_BUTTON_AXIS(joystick_y_axis)) { return CALIBRATE_DOWN; } else { // Finished. return CALIBRATE_CENTER; } case CALIBRATE_DOWN: // Finished. return CALIBRATE_CENTER; } } static void IdentifyBadAxes(void) { SDL_Joystick *joystick; int i, val; free(bad_axis); joystick = all_joysticks[joystick_index]; bad_axis = calloc(SDL_JoystickNumAxes(joystick), sizeof(boolean)); // Look for uncentered axes. for (i = 0; i < SDL_JoystickNumAxes(joystick); ++i) { val = SDL_JoystickGetAxis(joystick, i); bad_axis[i] = abs(val) > (32768 / 5); if (bad_axis[i]) { printf("Ignoring uncentered joystick axis #%i: %i\n", i, val); } } } static int CalibrationEventCallback(SDL_Event *event, void *user_data) { boolean advance; if (event->type != SDL_JOYBUTTONDOWN) { return 0; } // At this point, we have a button press. // In the first "center" stage, we're just trying to work out which // joystick is being configured and which button the user is pressing. if (calibrate_stage == CALIBRATE_CENTER) { joystick_index = event->jbutton.which; calibrate_button = event->jbutton.button; IdentifyBadAxes(); // If the joystick is a known one, auto-load default // config for it. if (IsKnownJoystick(joystick_index)) { LoadKnownConfiguration(); usejoystick = 1; TXT_CloseWindow(calibration_window); } else { // Advance to next stage. calibrate_stage = CALIBRATE_LEFT; SetCalibrationLabel(); } return 1; } // In subsequent stages, the user is asked to push in a specific // direction and press the button. They must push the same button // as they did before; this is necessary to support button axes. if (event->jbutton.which == joystick_index && event->jbutton.button == calibrate_button) { switch (calibrate_stage) { default: case CALIBRATE_LEFT: advance = CalibrateAxis(&joystick_x_axis, &joystick_x_invert); break; case CALIBRATE_RIGHT: advance = SetButtonAxisPositive(&joystick_x_axis); break; case CALIBRATE_UP: advance = CalibrateAxis(&joystick_y_axis, &joystick_y_invert); break; case CALIBRATE_DOWN: advance = SetButtonAxisPositive(&joystick_y_axis); break; } // Advance to the next calibration stage? if (advance) { calibrate_stage = NextCalibrateStage(); SetCalibrationLabel(); // Finished? if (calibrate_stage == CALIBRATE_CENTER) { usejoystick = 1; TXT_CloseWindow(calibration_window); } return 1; } } return 0; } static void NoJoystick(void) { TXT_MessageBox(NULL, "No joysticks or gamepads could be found.\n\n" "Try configuring your controller from within\n" "your OS first. Maybe you need to install\n" "some drivers or otherwise configure it."); joystick_index = -1; SetJoystickButtonLabel(); } static void CalibrateWindowClosed(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(unused)) { CloseAllJoysticks(); TXT_SDL_SetEventCallback(NULL, NULL); SetJoystickButtonLabel(); } static void CalibrateJoystick(TXT_UNCAST_ARG(widget), TXT_UNCAST_ARG(unused)) { calibrate_stage = CALIBRATE_CENTER; // Try to open all available joysticks. If none are opened successfully, // bomb out with an error. if (!OpenAllJoysticks()) { NoJoystick(); return; } calibration_window = TXT_NewWindow("Gamepad/Joystick calibration"); TXT_AddWidgets(calibration_window, TXT_NewLabel("Please follow the following instructions\n" "in order to calibrate your controller."), TXT_NewStrut(0, 1), calibration_label = TXT_NewLabel("zzz"), TXT_NewStrut(0, 1), NULL); TXT_SetWindowAction(calibration_window, TXT_HORIZ_LEFT, NULL); TXT_SetWindowAction(calibration_window, TXT_HORIZ_CENTER, TXT_NewWindowAbortAction(calibration_window)); TXT_SetWindowAction(calibration_window, TXT_HORIZ_RIGHT, NULL); TXT_SetWidgetAlign(calibration_label, TXT_HORIZ_CENTER); TXT_SDL_SetEventCallback(CalibrationEventCallback, NULL); TXT_SignalConnect(calibration_window, "closed", CalibrateWindowClosed, NULL); // Start calibration joystick_index = -1; calibrate_stage = CALIBRATE_CENTER; SetCalibrationLabel(); } // // GUI // static void JoystickWindowClosed(TXT_UNCAST_ARG(window), TXT_UNCAST_ARG(unused)) { if (joystick_initted) { SDL_QuitSubSystem(SDL_INIT_JOYSTICK); joystick_initted = 0; } } static void AddJoystickControl(txt_table_t *table, char *label, int *var) { txt_joystick_input_t *joy_input; joy_input = TXT_NewJoystickInput(var); TXT_AddWidget(table, TXT_NewLabel(label)); TXT_AddWidget(table, joy_input); } void ConfigJoystick(void) { txt_window_t *window; txt_table_t *button_table; txt_table_t *joystick_table; if (!joystick_initted) { joystick_initted = SDL_Init(SDL_INIT_JOYSTICK) >= 0; } window = TXT_NewWindow("Gamepad/Joystick configuration"); TXT_AddWidgets(window, TXT_NewCheckBox("Enable gamepad/joystick", &usejoystick), joystick_table = TXT_NewTable(2), TXT_NewSeparator("Buttons"), button_table = TXT_NewTable(2), NULL); TXT_SetColumnWidths(joystick_table, 20, 15); TXT_AddWidgets(joystick_table, TXT_NewLabel("Current controller"), joystick_button = TXT_NewButton("zzzz"), NULL); TXT_SetColumnWidths(button_table, 20, 15); AddJoystickControl(button_table, "Fire/Attack", &joybfire); AddJoystickControl(button_table, "Use", &joybuse); // High values of joybspeed are used to activate the "always run mode" // trick in Vanilla Doom. If this has been enabled, not only is the // joybspeed value meaningless, but the control itself is useless. if (joybspeed < 20) { AddJoystickControl(button_table, "Speed", &joybspeed); } AddJoystickControl(button_table, "Strafe", &joybstrafe); AddJoystickControl(button_table, "Strafe Left", &joybstrafeleft); AddJoystickControl(button_table, "Strafe Right", &joybstraferight); AddJoystickControl(button_table, "Previous weapon", &joybprevweapon); AddJoystickControl(button_table, "Next weapon", &joybnextweapon); if (gamemission == hexen || gamemission == strife) { AddJoystickControl(button_table, "Jump", &joybjump); } AddJoystickControl(button_table, "Activate menu", &joybmenu); TXT_SignalConnect(joystick_button, "pressed", CalibrateJoystick, NULL); TXT_SignalConnect(window, "closed", JoystickWindowClosed, NULL); TXT_SetWindowAction(window, TXT_HORIZ_CENTER, TestConfigAction()); SetJoystickButtonLabel(); } void BindJoystickVariables(void) { int i; M_BindVariable("use_joystick", &usejoystick); M_BindVariable("joystick_index", &joystick_index); M_BindVariable("joystick_x_axis", &joystick_x_axis); M_BindVariable("joystick_y_axis", &joystick_y_axis); M_BindVariable("joystick_strafe_axis", &joystick_strafe_axis); M_BindVariable("joystick_x_invert", &joystick_x_invert); M_BindVariable("joystick_y_invert", &joystick_y_invert); M_BindVariable("joystick_strafe_invert",&joystick_strafe_invert); for (i = 0; i < NUM_VIRTUAL_BUTTONS; ++i) { char name[32]; M_snprintf(name, sizeof(name), "joystick_physical_button%i", i); M_BindVariable(name, &joystick_physical_buttons[i]); } }