1
0
forked from Clones/Controlify

compound joysticks, button guide in screens, improve API

This commit is contained in:
isXander
2023-03-26 18:13:02 +01:00
parent de210df84f
commit 0d9321e3ba
55 changed files with 1188 additions and 287 deletions

View File

@ -65,8 +65,8 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
}
@Override
public String guid() {
return this.guid;
public int joystickId() {
return this.joystickId;
}
@Override

View File

@ -5,7 +5,7 @@ import com.google.gson.JsonElement;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
import dev.isxander.controlify.debug.DebugProperties;
import org.lwjgl.glfw.GLFW;
@ -14,7 +14,7 @@ import java.util.Map;
public interface Controller<S extends ControllerState, C extends ControllerConfig> {
String uid();
String guid();
int joystickId();
ControllerBindings<S> bindings();
@ -31,26 +31,27 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
String name();
void updateState();
void clearState();
default boolean canBeUsed() {
return true;
}
Map<Integer, Controller<?, ?>> CONTROLLERS = new HashMap<>();
Map<String, Controller<?, ?>> CONTROLLERS = new HashMap<>();
static Controller<?, ?> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
if (CONTROLLERS.containsKey(joystickId)) {
return CONTROLLERS.get(joystickId);
if (CONTROLLERS.containsKey(hidInfo.createControllerUID())) {
return CONTROLLERS.get(hidInfo.createControllerUID());
}
if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK) {
GamepadController controller = new GamepadController(joystickId, hidInfo);
CONTROLLERS.put(joystickId, controller);
CONTROLLERS.put(controller.uid(), controller);
return controller;
}
JoystickController controller = new JoystickController(joystickId, hidInfo);
CONTROLLERS.put(joystickId, controller);
SingleJoystickController controller = new SingleJoystickController(joystickId, hidInfo);
CONTROLLERS.put(controller.uid(), controller);
return controller;
}
@ -74,8 +75,8 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
}
@Override
public String guid() {
return "DUMMY";
public int joystickId() {
return -1;
}
@Override
@ -127,5 +128,10 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
public void updateState() {
}
@Override
public void clearState() {
}
};
}

View File

@ -16,7 +16,8 @@ public abstract class ControllerConfig {
public String customName = null;
public boolean showGuide = true;
public boolean showIngameGuide = true;
public boolean showScreenGuide = true;
public float chatKeyboardHeight = 0f;

View File

@ -43,6 +43,11 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
state = new GamepadState(axesState, rawAxesState, buttonState);
}
@Override
public void clearState() {
state = GamepadState.EMPTY;
}
public void consumeButtonState() {
this.state = new GamepadState(state().gamepadAxes(), state().rawGamepadAxes(), GamepadState.ButtonState.EMPTY);
}

View File

@ -36,7 +36,7 @@ public class ControllerHIDService implements HidServicesListener {
services.scan();
try {
// wait for scan to complete on separate thread
Thread.sleep(800);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
@ -78,5 +78,8 @@ public class ControllerHIDService implements HidServicesListener {
}
public record ControllerHIDInfo(ControllerType type, Optional<String> path) {
public String createControllerUID() {
return UUID.nameUUIDFromBytes(path().get().getBytes()).toString();
}
}
}

View File

@ -0,0 +1,158 @@
package dev.isxander.controlify.controller.joystick;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.ControllerType;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping;
import org.lwjgl.glfw.GLFW;
import java.util.List;
public class CompoundJoystickController implements JoystickController<JoystickConfig> {
private final String uid;
private final List<Integer> joysticks;
private final int axisCount, buttonCount, hatCount;
private final ControllerType compoundType;
private final ControllerBindings<JoystickState> bindings;
private final JoystickMapping mapping;
private JoystickConfig config;
private final JoystickConfig defaultConfig;
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
public CompoundJoystickController(List<Integer> joystickIds, String uid, ControllerType compoundType) {
this.joysticks = ImmutableList.copyOf(joystickIds);
this.uid = uid;
this.compoundType = compoundType;
this.axisCount = joystickIds.stream().mapToInt(this::getAxisCountForJoystick).sum();
this.buttonCount = joystickIds.stream().mapToInt(this::getButtonCountForJoystick).sum();
this.hatCount = joystickIds.stream().mapToInt(this::getHatCountForJoystick).sum();
this.mapping = RPJoystickMapping.fromType(type());
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);
this.bindings = new ControllerBindings<>(this);
}
@Override
public String uid() {
return this.uid;
}
@Override
public ControllerBindings<JoystickState> bindings() {
return this.bindings;
}
@Override
public JoystickState state() {
return this.state;
}
@Override
public JoystickState prevState() {
return this.prevState;
}
@Override
public void updateState() {
this.prevState = this.state;
var states = this.joysticks.stream().map(joystick -> JoystickState.fromJoystick(this, joystick)).toList();
this.state = JoystickState.merged(mapping(), states);
}
@Override
public void clearState() {
this.state = JoystickState.empty(this);
}
@Override
public JoystickConfig config() {
return this.config;
}
@Override
public JoystickConfig defaultConfig() {
return this.defaultConfig;
}
@Override
public void resetConfig() {
this.config = new JoystickConfig(this);
}
@Override
public void setConfig(Gson gson, JsonElement json) {
JoystickConfig newConfig = gson.fromJson(json, JoystickConfig.class);
if (newConfig != null) {
this.config = newConfig;
} else {
Controlify.LOGGER.error("Could not set config for controller " + name() + " (" + uid() + ")! Using default config instead.");
this.config = defaultConfig();
}
this.config.setup(this);
}
@Override
public ControllerType type() {
return this.compoundType;
}
@Override
public String name() {
return type().friendlyName();
}
@Override
public JoystickMapping mapping() {
return this.mapping;
}
@Override
public int axisCount() {
return this.axisCount;
}
@Override
public int buttonCount() {
return this.buttonCount;
}
@Override
public int hatCount() {
return this.hatCount;
}
@Override
public boolean canBeUsed() {
return JoystickController.super.canBeUsed()
&& joysticks.stream().allMatch(GLFW::glfwJoystickPresent);
}
@Override
public int joystickId() {
return -1;
}
private int getAxisCountForJoystick(int joystick) {
return GLFW.glfwGetJoystickAxes(joystick).capacity();
}
private int getButtonCountForJoystick(int joystick) {
return GLFW.glfwGetJoystickButtons(joystick).capacity();
}
private int getHatCountForJoystick(int joystick) {
return GLFW.glfwGetJoystickHats(joystick).capacity();
}
}

View File

@ -0,0 +1,46 @@
package dev.isxander.controlify.controller.joystick;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerType;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public record CompoundJoystickInfo(Collection<String> joystickUids, String friendlyName) {
public ControllerType type() {
return new ControllerType(friendlyName, createUID(joystickUids));
}
public boolean canBeUsed() {
List<Controller<?, ?>> joysticks = Controller.CONTROLLERS.values().stream().filter(c -> joystickUids.contains(c.uid())).toList();
if (joysticks.size() != joystickUids().size()) {
return false; // not all controllers are connected
}
if (joysticks.stream().anyMatch(c -> !c.canBeUsed())) {
return false; // not all controllers can be used
}
return true;
}
public boolean isLoaded() {
return Controller.CONTROLLERS.containsKey(createUID(joystickUids));
}
public Optional<CompoundJoystickController> attemptCreate() {
if (!canBeUsed()) return Optional.empty();
List<Integer> joystickIDs = Controller.CONTROLLERS.values().stream()
.filter(c -> joystickUids.contains(c.uid()))
.map(Controller::joystickId)
.toList();
ControllerType type = type();
return Optional.of(new CompoundJoystickController(joystickIDs, type.identifier(), type));
}
public static String createUID(Collection<String> joystickUIDs) {
return "compound-" + String.join("_", joystickUIDs);
}
}

View File

@ -1,6 +1,7 @@
package dev.isxander.controlify.controller.joystick;
import dev.isxander.controlify.controller.ControllerConfig;
import org.apache.commons.lang3.Validate;
import java.util.HashMap;
import java.util.Map;
@ -8,9 +9,10 @@ import java.util.Map;
public class JoystickConfig extends ControllerConfig {
private Map<String, Float> deadzones;
private transient JoystickController controller;
private transient JoystickController<?> controller;
public JoystickConfig(JoystickController controller) {
public JoystickConfig(JoystickController<?> controller) {
Validate.notNull(controller);
setup(controller);
}
@ -30,7 +32,7 @@ public class JoystickConfig extends ControllerConfig {
return deadzones.getOrDefault(controller.mapping().axis(axis).identifier(), 0.2f);
}
void setup(JoystickController controller) {
void setup(JoystickController<?> controller) {
this.controller = controller;
if (this.deadzones == null) {
deadzones = new HashMap<>();

View File

@ -1,74 +1,20 @@
package dev.isxander.controlify.controller.joystick;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.isxander.controlify.controller.AbstractController;
import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.joystick.JoystickConfig;
import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
import org.lwjgl.glfw.GLFW;
import java.util.Objects;
public interface JoystickController<T extends JoystickConfig> extends Controller<JoystickState, T> {
JoystickMapping mapping();
public class JoystickController extends AbstractController<JoystickState, JoystickConfig> {
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
private final int axisCount, buttonCount, hatCount;
private final JoystickMapping mapping;
public JoystickController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
super(joystickId, hidInfo);
this.axisCount = GLFW.glfwGetJoystickAxes(joystickId).capacity();
this.buttonCount = GLFW.glfwGetJoystickButtons(joystickId).capacity();
this.hatCount = GLFW.glfwGetJoystickHats(joystickId).capacity();
this.mapping = Objects.requireNonNull(RPJoystickMapping.fromType(type()));
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);
}
int axisCount();
int buttonCount();
int hatCount();
@Override
public JoystickState state() {
return state;
}
@Override
public JoystickState prevState() {
return prevState;
}
@Override
public void updateState() {
prevState = state;
state = JoystickState.fromJoystick(this, joystickId);
}
public JoystickMapping mapping() {
return mapping;
}
@Override
public boolean canBeUsed() {
default boolean canBeUsed() {
return !(mapping() instanceof UnmappedJoystickMapping);
}
public int axisCount() {
return axisCount;
}
public int buttonCount() {
return buttonCount;
}
public int hatCount() {
return hatCount;
}
@Override
public void setConfig(Gson gson, JsonElement json) {
super.setConfig(gson, json);
this.config.setup(this);
}
}

View File

@ -11,6 +11,7 @@ import org.lwjgl.glfw.GLFW;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.IntStream;
@ -58,7 +59,17 @@ public class JoystickState implements ControllerState {
|| hats().stream().anyMatch(hat -> hat != HatState.CENTERED);
}
public static JoystickState fromJoystick(JoystickController joystick, int joystickId) {
@Override
public String toString() {
return "JoystickState{" +
"axes=" + axes +
", rawAxes=" + rawAxes +
", buttons=" + buttons +
", hats=" + hats +
'}';
}
public static JoystickState fromJoystick(JoystickController<?> joystick, int joystickId) {
FloatBuffer axesBuffer = GLFW.glfwGetJoystickAxes(joystickId);
List<Float> axes = new ArrayList<>();
List<Float> rawAxes = new ArrayList<>();
@ -107,6 +118,40 @@ public class JoystickState implements ControllerState {
return new JoystickState(joystick.mapping(), axes, rawAxes, buttons, hats);
}
public static JoystickState empty(JoystickController<?> joystick) {
var axes = new ArrayList<Float>();
var buttons = new ArrayList<Boolean>();
var hats = new ArrayList<HatState>();
for (int i = 0; i < joystick.axisCount(); i++) {
axes.add(joystick.mapping().axis(i).restingValue());
}
for (int i = 0; i < joystick.buttonCount(); i++) {
buttons.add(false);
}
for (int i = 0; i < joystick.hatCount(); i++) {
hats.add(HatState.CENTERED);
}
return new JoystickState(joystick.mapping(), axes, axes, buttons, hats);
}
public static JoystickState merged(JoystickMapping mapping, Collection<JoystickState> states) {
var axes = new ArrayList<Float>();
var rawAxes = new ArrayList<Float>();
var buttons = new ArrayList<Boolean>();
var hats = new ArrayList<HatState>();
for (var state : states) {
axes.addAll(state.axes);
rawAxes.addAll(state.rawAxes);
buttons.addAll(state.buttons);
hats.addAll(state.hats);
}
return new JoystickState(mapping, axes, rawAxes, buttons, hats);
}
public enum HatState implements NameableEnum {
CENTERED,
UP,

View File

@ -0,0 +1,83 @@
package dev.isxander.controlify.controller.joystick;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.isxander.controlify.controller.AbstractController;
import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
import org.lwjgl.glfw.GLFW;
import java.util.Objects;
public class SingleJoystickController extends AbstractController<JoystickState, JoystickConfig> implements JoystickController<JoystickConfig> {
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
private final int axisCount, buttonCount, hatCount;
private final JoystickMapping mapping;
public SingleJoystickController(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
super(joystickId, hidInfo);
this.axisCount = GLFW.glfwGetJoystickAxes(joystickId).capacity();
this.buttonCount = GLFW.glfwGetJoystickButtons(joystickId).capacity();
this.hatCount = GLFW.glfwGetJoystickHats(joystickId).capacity();
this.mapping = Objects.requireNonNull(RPJoystickMapping.fromType(type()));
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);
}
@Override
public JoystickState state() {
return state;
}
@Override
public JoystickState prevState() {
return prevState;
}
@Override
public void updateState() {
prevState = state;
state = JoystickState.fromJoystick(this, joystickId);
}
@Override
public void clearState() {
this.state = JoystickState.empty(this);
}
@Override
public JoystickMapping mapping() {
return mapping;
}
@Override
public int axisCount() {
return axisCount;
}
@Override
public int buttonCount() {
return buttonCount;
}
@Override
public int hatCount() {
return hatCount;
}
@Override
public int joystickId() {
return joystickId;
}
@Override
public void setConfig(Gson gson, JsonElement json) {
super.setConfig(gson, json);
this.config.setup(this);
}
}

View File

@ -21,6 +21,8 @@ public interface JoystickMapping {
boolean isAxisResting(float value);
float restingValue();
String getDirectionIdentifier(int axis, JoystickAxisBind.AxisDirection direction);
}

View File

@ -111,7 +111,7 @@ public class RPJoystickMapping implements JoystickMapping {
}
}
private record AxisMapping(List<Integer> ids, String identifier, Vec2 inpRange, Vec2 outRange, float restState, boolean requiresDeadzone, String typeId, List<List<String>> axisNames) implements Axis {
private record AxisMapping(List<Integer> ids, String identifier, Vec2 inpRange, Vec2 outRange, float restingValue, boolean requiresDeadzone, String typeId, List<List<String>> axisNames) implements Axis {
@Override
public float modifyAxis(float value) {
if (inpRange() == null || outRange() == null)
@ -122,7 +122,7 @@ public class RPJoystickMapping implements JoystickMapping {
@Override
public boolean isAxisResting(float value) {
return value == restState();
return value == restingValue();
}
@Override

View File

@ -45,7 +45,12 @@ public class UnmappedJoystickMapping implements JoystickMapping {
@Override
public boolean isAxisResting(float value) {
return value == 0;
return value == restingValue();
}
@Override
public float restingValue() {
return 0;
}
@Override