package net.fourbytes.shadow;
import com.badlogic.gdx.controllers.Controller;
import com.badlogic.gdx.controllers.ControllerListener;
import com.badlogic.gdx.controllers.Controllers;
import com.badlogic.gdx.controllers.PovDirection;
import com.badlogic.gdx.controllers.mappings.Ouya;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.JsonValue;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.ObjectMap.Entry;
import net.fourbytes.shadow.Input.Key;
import net.fourbytes.shadow.Input.Key.Triggerer;
import net.fourbytes.shadow.MenuLevel.MenuItem;
import net.fourbytes.shadow.utils.Cache;
import net.fourbytes.shadow.utils.Garbage;
import net.fourbytes.shadow.utils.Options;
import net.fourbytes.shadow.utils.backend.ControllerNumerator;
/**
* This class manages all controllers connected to the current
* application, handles their input and translates them into
* Shadow key presses. Due to the nature of GDX's Controllers
* class and it's ControllerManager not managing custom controllers,
* such as the controllers created by a GDXRemote server, it is
* advised to use this class to poll for currently connected
* controllers. Also, this class manages mapping with help of
* the ControllerHelper.ControllerInput class and it's subclasses
* ControllerHelper.ControllerButton and ControllerHelper.ControllerAxis.
* POVs, accelerators and more are to come.
* <br><br>
* This class is instance of ControllerListener but uses polling
* of Controllers' ControllerManager's controllers. Custom
* controllers may get added into the list of the Controllers'
* ControllerManager's controllers but must add the default
* instance of this class (sitting in Shadow by default) to the
* list of their ControllerListeners manually and load the
* mapping manually by calling connected manually.
* <br><br>
* Possible automatic mappings of the controller buttons to in-game buttons
* may happen by creating a new ControllerHelper.Mapping subclass and adding
* an instance of that class into the mappings array of the current
* ControllerHelper instance. Default mappings for OUYA, PS3 (Linux and
* OUYA only) and Sanmos (Linux only) controllers are given.
*/
public final class ControllerHelper implements ControllerListener {
//MAPPINGS (may move into separate package in future)
public static abstract interface Mapping {
public String getName();
/**
* @return true if mapped; false otherwise
*/
public boolean map(Controller controller, ControllerHelper controllerHelper);
}
public static class ConfigMapping implements Mapping {
@Override
public String getName() {
return "User-created mapping loaded from file";
}
@Override
public boolean map(Controller controller, ControllerHelper controllerHelper) {
String jsonString = Options.getString("controller.\""+controller.getName()+"\"", null);
if (jsonString == null) {
return false;
}
JsonValue json = Garbage.jsonReader.parse(jsonString);
JsonValue axes = json.get("axes");
for (JsonValue axis = axes.child; axis != null; axis = axis.next) {
int id = Integer.parseInt(axis.name);
boolean negative = axis.name.startsWith("-");
if (negative) {
id = -id;
}
for (JsonValue key = axis.child; key != null; key = key.next) {
String keyName = key.asString();
Key keyInput = Input.getKey(keyName);
ControllerAxis axisController = new ControllerAxis(controller, id, negative);
controllerHelper.map(keyInput, axisController);
}
}
JsonValue buttons = json.get("buttons");
for (JsonValue button = buttons.child; button != null; button = button.next) {
int id = Integer.parseInt(button.name);
for (JsonValue key = button.child; key != null; key = key.next) {
String keyName = key.asString();
Key keyInput = Input.getKey(keyName);
ControllerButton axisController = new ControllerButton(controller, id);
controllerHelper.map(keyInput, axisController);
}
}
return true;
}
}
public static class OuyaMapping implements Mapping {
@Override
public String getName() {
return "Default OUYA mapping";
}
@Override
public boolean map(Controller controller, ControllerHelper controllerHelper) {
if (Ouya.ID.equals(controller.getName())) {
System.out.println("Automapping OUYA controller...");
controllerHelper.map(Input.up, new ControllerButton(controller, Ouya.BUTTON_DPAD_UP));
controllerHelper.map(Input.down, new ControllerButton(controller, Ouya.BUTTON_DPAD_DOWN));
controllerHelper.map(Input.left, new ControllerButton(controller, Ouya.BUTTON_DPAD_LEFT));
controllerHelper.map(Input.right, new ControllerButton(controller, Ouya.BUTTON_DPAD_RIGHT));
controllerHelper.map(Input.up, new ControllerAxis(controller, Ouya.AXIS_LEFT_Y, true));
controllerHelper.map(Input.down, new ControllerAxis(controller, Ouya.AXIS_LEFT_Y, false));
controllerHelper.map(Input.left, new ControllerAxis(controller, Ouya.AXIS_LEFT_X, true));
controllerHelper.map(Input.right, new ControllerAxis(controller, Ouya.AXIS_LEFT_X, false));
controllerHelper.map(Input.jump, new ControllerButton(controller, Ouya.BUTTON_O));
controllerHelper.map(Input.pause, new ControllerButton(controller, Ouya.BUTTON_MENU));
controllerHelper.map(Input.enter, new ControllerButton(controller, Ouya.BUTTON_O));
controllerHelper.map(Input.androidBack, new ControllerButton(controller, Ouya.BUTTON_A));
return true;
}
return false;
}
}
public static class PS3Mapping implements Mapping {
@Override
public String getName() {
return "Default PS3 mapping";
}
@Override
public boolean map(Controller controller, ControllerHelper controllerHelper) {
String os = System.getProperty("os.name").toLowerCase();
if (controller.getName().contains("PLAYSTATION(R)3")) {
//Sidenote: Third-Party bluetooth PS3 controllers should use the same button mapping as the original controller.
//Otherwise, the PS3 itself would even have problems with the button IDs being different.
if (Input.isOuya) {
System.out.println("Automapping PS3 controller on Ouya...");
controllerHelper.map(Input.up, new ControllerButton(controller, 19));
controllerHelper.map(Input.down, new ControllerButton(controller, 20));
controllerHelper.map(Input.left, new ControllerButton(controller, 21));
controllerHelper.map(Input.right, new ControllerButton(controller, 22));
controllerHelper.map(Input.up, new ControllerAxis(controller, 1, true));
controllerHelper.map(Input.down, new ControllerAxis(controller, 1, false));
controllerHelper.map(Input.left, new ControllerAxis(controller, 0, true));
controllerHelper.map(Input.right, new ControllerAxis(controller, 0, false));
controllerHelper.map(Input.jump, new ControllerButton(controller, 96));
controllerHelper.map(Input.pause, new ControllerButton(controller, 108));
controllerHelper.map(Input.pause, new ControllerButton(controller, 86));
controllerHelper.map(Input.enter, new ControllerButton(controller, 96));
controllerHelper.map(Input.androidBack, new ControllerButton(controller, 97));
return true;
} else if ((os.equals("linux") || os.equals("unix"))) {
System.out.println("Automapping PS3 controller on Linux...");
controllerHelper.map(Input.up, new ControllerButton(controller, 4));
controllerHelper.map(Input.down, new ControllerButton(controller, 6));
controllerHelper.map(Input.left, new ControllerButton(controller, 7));
controllerHelper.map(Input.right, new ControllerButton(controller, 5));
controllerHelper.map(Input.up, new ControllerAxis(controller, 1, true));
controllerHelper.map(Input.down, new ControllerAxis(controller, 1, false));
controllerHelper.map(Input.left, new ControllerAxis(controller, 0, true));
controllerHelper.map(Input.right, new ControllerAxis(controller, 0, false));
controllerHelper.map(Input.jump, new ControllerButton(controller, 14));
controllerHelper.map(Input.pause, new ControllerButton(controller, 3));
controllerHelper.map(Input.enter, new ControllerButton(controller, 14));
controllerHelper.map(Input.androidBack, new ControllerButton(controller, 13));
return true;
}
}
return false;
}
}
public static class SanmosMapping implements Mapping {
@Override
public String getName() {
return "Default Sanmos mapping";
}
@Override
public boolean map(Controller controller, ControllerHelper controllerHelper) {
String os = System.getProperty("os.name").toLowerCase();
if ((os.equals("linux") || os.equals("unix")) && controller.getName().contains("Sanmos TWIN SHOCK")) {
System.out.println("Automapping Sanmos TWIN SHOCK on Linux...");
controllerHelper.map(Input.up, new ControllerAxis(controller, 1, true));
controllerHelper.map(Input.down, new ControllerAxis(controller, 1, false));
controllerHelper.map(Input.left, new ControllerAxis(controller, 0, true));
controllerHelper.map(Input.right, new ControllerAxis(controller, 0, false));
controllerHelper.map(Input.jump, new ControllerButton(controller, 2));
controllerHelper.map(Input.pause, new ControllerButton(controller, 11));
controllerHelper.map(Input.enter, new ControllerButton(controller, 2));
controllerHelper.map(Input.androidBack, new ControllerButton(controller, 3));
return true;
}
return false;
}
}
//INPUTS
public static abstract class ControllerInput {
public Controller controller;
protected ControllerInput() {
this(null);
}
protected ControllerInput(Controller controller) {
this.controller = controller;
}
public abstract String getLabel();
}
public static class ControllerButton extends ControllerInput {
public int buttonCode = -1;
public ControllerButton() {
super();
}
public ControllerButton(ControllerButton input) {
this(input.controller, input.buttonCode);
}
public ControllerButton(Controller controller, int buttonCode) {
super(controller);
this.buttonCode = buttonCode;
}
@Override
public String getLabel() {
return "Button "+buttonCode;
}
@Override
public int hashCode() {
return controller.hashCode()*buttonCode;
}
@Override
public boolean equals(Object o) {
if (o instanceof ControllerButton) {
ControllerButton button = (ControllerButton) o;
return button.controller.equals(controller) && button.buttonCode == buttonCode;
}
return false;
}
public ControllerButton set(Controller controller, int buttonCode) {
this.controller = controller;
this.buttonCode = buttonCode;
return this;
}
}
public static class ControllerAxis extends ControllerInput {
public int axisCode = -1;
public boolean negative = false;
public ControllerAxis() {
super();
}
public ControllerAxis(ControllerAxis input) {
this(input.controller, input.axisCode, input.negative);
}
public ControllerAxis(Controller controller, int axisCode, boolean negative) {
super(controller);
this.axisCode = axisCode;
this.negative = negative;
}
@Override
public String getLabel() {
return "Axis "+axisCode;
}
@Override
public int hashCode() {
int hash = controller.hashCode()*axisCode;
if (negative) {
hash = -hash;
}
return hash;
}
@Override
public boolean equals(Object o) {
if (o instanceof ControllerAxis) {
ControllerAxis axis = (ControllerAxis) o;
return axis.controller.equals(controller) && axis.axisCode == axisCode && axis.negative == negative;
}
return false;
}
public ControllerAxis set(Controller controller, int axisCode, boolean negative) {
this.controller = controller;
this.axisCode = axisCode;
this.negative = negative;
return this;
}
}
//CACHES
protected final static Cache<Array<Key>> cacheKeys = new Cache(Array.class, 32,
new Object[] {false, 4, Key.class}, new Class[] {boolean.class, int.class, Class.class});
protected final static Cache<ControllerButton> cacheButtons = new Cache<ControllerButton>(ControllerButton.class, 64);
protected final static Cache<ControllerAxis> cacheAxes = new Cache<ControllerAxis>(ControllerAxis.class, 64);
//CONTROLLERHELPER
public static float deadzone = 0.25f;
public final ObjectMap<Key, Array<ControllerInput>> keymap = new ObjectMap<Key, Array<ControllerInput>>();
public final ObjectMap<Key, Array<ControllerInput>> tmpmap = new ObjectMap<Key, Array<ControllerInput>>();
public final ObjectMap<Controller, JsonValue> tmpconfmap = new ObjectMap<Controller, JsonValue>();
public final Array<Mapping> mappings = new Array<Mapping>(Mapping.class);
private Key tmpkey;
public Key assignKey;
public MenuItem assignKeyHelper;
public Controller assignKeyController;
public Array<Controller> controllers = new Array<Controller>(Controller.class);
public Array<Controller> controllersAuto = new Array<Controller>(Controller.class);
public ControllerNumerator numerator;
public ControllerHelper() {
mappings.add(new ConfigMapping());
if (!Input.isOuya) {//The OUYA has got it's own mapping for the OUYA controller since OUYA Everywhere.
mappings.add(new OuyaMapping());
}
mappings.add(new PS3Mapping());
mappings.add(new SanmosMapping());
}
public void refreshKeymap() {
tmpmap.clear();
tmpconfmap.clear();
synchronized (keymap) {
for (Controller controller : controllers) {
JsonValue json = new JsonValue(JsonValue.ValueType.object);
json.child = new JsonValue(JsonValue.ValueType.object);
json.child.name = "buttons";
json.child.prev = new JsonValue(JsonValue.ValueType.object);
json.child.prev.next = json.child;
json.child = json.child.prev;
json.child.name = "axes";
tmpconfmap.put(controller, json);
}
for (Entry<Key, Array<ControllerInput>> entry : keymap.entries()) {
Array<ControllerInput> inputs = entry.value;
Key key = entry.key;
for (ControllerInput input : inputs) {
if (!controllers.contains(input.controller, true)) {
inputs.removeValue(input, false);
continue;
}
JsonValue json = tmpconfmap.get(input.controller);
if (input instanceof ControllerAxis) {
JsonValue axes = json.get("axes");
JsonValue axis = axes.get((((ControllerAxis) input).negative?"-":"") + ((ControllerAxis) input).axisCode);
if (axis == null) {
axis = new JsonValue(JsonValue.ValueType.array);
axis.name = (((ControllerAxis) input).negative?"-":"") + ((ControllerAxis) input).axisCode;
if (axes.child == null) {
axes.child = axis;
} else {
JsonValue child = axes.child;
axes.child = axis;
child.prev = axis;
axis.next = child;
}
}
JsonValue keyValue = new JsonValue(key.name);
keyValue.name = "";
if (axis.child == null) {
axis.child = keyValue;
} else {
JsonValue child = axis.child;
axis.child = keyValue;
child.prev = keyValue;
keyValue.next = child;
}
} else if (input instanceof ControllerButton) {
JsonValue buttons = json.get("buttons");
JsonValue button = buttons.get("" + ((ControllerButton) input).buttonCode);
if (button == null) {
button = new JsonValue(JsonValue.ValueType.array);
button.name = "" + ((ControllerButton) input).buttonCode;
if (buttons.child == null) {
buttons.child = button;
} else {
JsonValue child = buttons.child;
buttons.child = button;
child.prev = button;
button.next = child;
}
}
JsonValue keyValue = new JsonValue(key.name);
keyValue.name = "";
if (button.child == null) {
button.child = keyValue;
} else {
JsonValue child = button.child;
button.child = keyValue;
child.prev = keyValue;
keyValue.next = child;
}
}
}
if (inputs.size == 0) {
keymap.remove(key);
}
}
for (Entry<Controller, JsonValue> entry : tmpconfmap.entries()) {
Controller controller = entry.key;
JsonValue json = entry.value;
String jsonString = json.toString();
Options.putString("controller.\""+controller.getName()+"\"", jsonString);
}
Options.flush();
}
}
public Array<Key> getKeysForInput(ControllerInput input) {
Array<Key> keys = cacheKeys.getNext();
keys.clear();
for (Entry<Key, Array<ControllerInput>> entry : keymap.entries()) {
Array<ControllerInput> einputs = entry.value;
Key ekey = entry.key;
if (einputs.contains(input, false)) {
keys.add(ekey);
}
}
return keys;
}
public String getInputLabelForKey(Key key) {
Array<ControllerInput> inputs = keymap.get(key);
if (inputs == null) {
return "NONE";
}
if (inputs.size == 1) {
ControllerInput input = inputs.get(0);
if (input == null) {
return "NONE";
}
return input.getLabel();
}
if (inputs.size == 0) {
return "NONE";
}
return "MULTI";
}
public String getInputLabelForKey(Key key, Controller controller) {
Array<ControllerInput> inputs = keymap.get(key);
if (inputs == null) {
return "NONE";
}
int num = 0;
ControllerInput input = null;
for (ControllerInput cinput : inputs) {
if (cinput.controller == controller) {
num++;
input = cinput;
if (num == 2) {
break;
}
}
}
if (num == 0) {
return "NONE";
}
if (num == 1) {
return input.getLabel();
}
return "MULTI";
}
public void map(Key key, ControllerInput input) {
synchronized (keymap) {
Array<ControllerInput> inputs = keymap.get(key);
if (inputs == null) {
inputs = new Array<ControllerInput>(true, 2, ControllerInput.class);
keymap.put(key, inputs);
}
inputs.add(input);
}
}
public void tick() {
//Check for new controllers and update list
Array<Controller> controllersManaged = Controllers.getControllers();
if (controllersManaged.size > controllersAuto.size) {
for (Controller controller : controllersManaged) {
if (controllersAuto.contains(controller, true)) {
continue;
}
connected(controller);
}
} else if (controllersManaged.size < controllersAuto.size) {
for (Controller controller : controllersManaged) {
if (controllersManaged.contains(controller, true)) {
continue;
}
disconnected(controller);
}
}
//Assign key if asked for
if (assignKey != null) {
if (assignKeyHelper != null) {
assignKeyHelper.text = assignKey.name+" ("+Shadow.controllerHelper.getInputLabelForKey(assignKey, assignKeyController)+") ...";
}
if (tmpkey != null && tmpkey != assignKey) {
throw new Error("Switching key to assign while assigning key not supported!");
}
tmpkey = assignKey;
} else {
tmpkey = null;
}
//"Unstick" axes.
synchronized (keymap) {
for (Entry<Key, Array<ControllerInput>> entry : keymap.entries()) {
Array<ControllerInput> inputs = entry.value;
Key key = entry.key;
for (ControllerInput input : inputs) {
if (input instanceof ControllerAxis) {
ControllerAxis axis = (ControllerAxis) input;
float value = axis.controller.getAxis(axis.axisCode);
if ((value < -deadzone && axis.negative) || (value > deadzone && !axis.negative)) {
//Do nothing - the listener should handle this.
} else {
if (key.triggerer == Triggerer.CONTROLLER_AXIS && key.isDown) {
key.nextState = false;
}
}
}
}
}
}
}
@Override
public void connected(Controller controller) {
if (controllers.contains(controller, true)) {
return;
}
controller.addListener(this);
controllers.add(controller);
if (Controllers.getControllers().contains(controller, true)) {
controllersAuto.add(controller);
}
for (Mapping mapping : mappings) {
if (mapping.map(controller, this)) {
break;
}
}
refreshKeymap();
}
@Override
public void disconnected(Controller controller) {
if (!controllers.contains(controller, true)) {
return;
}
controller.removeListener(this);
controllers.removeValue(controller, true);
if (controllersAuto.contains(controller, true)) {
controllersAuto.removeValue(controller, true);
}
refreshKeymap();
}
@Override
public boolean buttonDown(Controller controller, int buttonCode) {
//System.out.println("Pressed button "+buttonCode+" on controller "+controller);
//ControllerButton button = new ControllerButton(controller, buttonCode);
ControllerButton button = cacheButtons.getNext().set(controller, buttonCode);
for (Key key : getKeysForInput(button)) {
//System.out.println("ControllerHelper triggered key \""+key.name+"\"'s nextstate to true");
key.triggerer = Triggerer.CONTROLLER_BUTTON;
key.nextState = true;
}
if (assignKey != null && controller == assignKeyController) {
System.out.println("Mapped in-game key \""+assignKey.name+"\" to button "+buttonCode+" on controller "+(controller.getName()));
map(assignKey, new ControllerButton(button));
refreshKeymap();
if (assignKeyHelper != null) {
assignKeyHelper.text = assignKey.name+" ("+Shadow.controllerHelper.getInputLabelForKey(assignKey, assignKeyController)+")";
}
assignKey = null;
assignKeyController = null;
}
return false;
}
@Override
public boolean buttonUp(Controller controller, int buttonCode) {
//System.out.println("Released button "+buttonCode+" on controller "+controller);
//ControllerButton button = new ControllerButton(controller, buttonCode);
ControllerButton button = cacheButtons.getNext().set(controller, buttonCode);
for (Key key : getKeysForInput(button)) {
//System.out.println("ControllerHelper triggered key \""+key.name+"\"'s nextstate to false");
key.tick();
key.triggerer = Triggerer.CONTROLLER_BUTTON;
key.nextState = false;
}
return false;
}
@Override
public boolean axisMoved(Controller controller, int axisCode, float value) {
float pvalue = value;
boolean negative = false;
if (value < 0f) {
pvalue = -value;
negative = true;
}
//ControllerAxis axis = new ControllerAxis(controller, axisCode, negative);
ControllerAxis axis = cacheAxes.getNext().set(controller, axisCode, negative);
if (pvalue >= deadzone) {//Internal deadzone.
//System.out.println("Moved axis "+axisCode+" with current value "+value+" on controller "+controller);
for (Key key : getKeysForInput(axis)) {
//System.out.println("ControllerHelper triggered key \""+key.name+"\"'s nextstate to true");
key.triggerer = Triggerer.CONTROLLER_AXIS;
key.nextState = true;
}
if (pvalue >= 0.3f && assignKey != null && controller == assignKeyController) {//To eliminate minor accidental movements while assigning
System.out.println("Mapped in-game key \""+assignKey.name+"\" to axis "+axisCode+" on controller "+(controller.getName()));
map(assignKey, new ControllerAxis(axis));
refreshKeymap();
if (assignKeyHelper != null) {
assignKeyHelper.text = assignKey.name+" ("+Shadow.controllerHelper.getInputLabelForKey(assignKey, assignKeyController)+")";
}
assignKey = null;
assignKeyController = null;
}
} else {
for (Key key : getKeysForInput(axis)) {
//System.out.println("ControllerHelper triggered key \""+key.name+"\"'s nextstate to true");
key.triggerer = Triggerer.CONTROLLER_AXIS;
key.nextState = false;
}
//System.out.println("Moved axis "+axisCode+" with current value "+value+" on controller "+controller+" inside \"internal deadzone\"");
}
return false;
}
@Override
public boolean povMoved(Controller controller, int povCode,
PovDirection value) {
//TODO Implement
//System.out.println("Moved pov "+povCode+" with current value "+value+" on controller "+controller);
return false;
}
@Override
public boolean xSliderMoved(Controller controller, int sliderCode,
boolean value) {
//TODO Implement
//System.out.println("Moved X slider "+sliderCode+" with current value "+value+" on controller "+controller);
return false;
}
@Override
public boolean ySliderMoved(Controller controller, int sliderCode,
boolean value) {
//TODO Implement
//System.out.println("Moved Y slider "+sliderCode+" with current value "+value+" on controller "+controller);
return false;
}
@Override
public boolean accelerometerMoved(Controller controller,
int accelerometerCode, Vector3 value) {
//TODO Implement
//System.out.println("Moved accelerometer "+accelerometerCode+" with current value "+value+" on controller "+controller);
return false;
}
}