1
0
forked from Clones/Controlify

🎮📳 Controller Vibration! (#38)

This commit is contained in:
Xander
2023-04-04 17:17:01 +01:00
committed by GitHub
parent 2bf7cf4792
commit ebbc549e32
32 changed files with 686 additions and 42 deletions

View File

@ -6,17 +6,23 @@ import com.google.gson.JsonElement;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
import dev.isxander.controlify.rumble.RumbleCapable;
import dev.isxander.controlify.rumble.RumbleManager;
import org.libsdl.SDL;
import org.lwjgl.glfw.GLFW;
import java.util.Objects;
import java.util.UUID;
public abstract class AbstractController<S extends ControllerState, C extends ControllerConfig> implements Controller<S, C> {
public abstract class AbstractController<S extends ControllerState, C extends ControllerConfig> implements Controller<S, C>, RumbleCapable {
protected final int joystickId;
protected String name;
private final String uid;
private final String guid;
private final ControllerType type;
private final long ptrJoystick;
private final RumbleManager rumbleManager;
private final ControllerBindings<S> bindings;
protected C config, defaultConfig;
@ -30,12 +36,16 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
this.joystickId = joystickId;
this.guid = GLFW.glfwGetJoystickGUID(joystickId);
this.ptrJoystick = SDL2NativesManager.isLoaded() ? SDL.SDL_JoystickOpen(joystickId) : 0;
this.rumbleManager = new RumbleManager(this);
if (hidInfo.path().isPresent()) {
this.uid = UUID.nameUUIDFromBytes(hidInfo.path().get().getBytes()).toString();
this.uid = hidInfo.createControllerUID().orElseThrow();
this.type = hidInfo.type();
} else {
this.uid = "unidentified-guid-" + UUID.nameUUIDFromBytes(this.guid.getBytes());
this.type = ControllerType.UNKNOWN;
}
this.type = hidInfo.type();
var joystickName = GLFW.glfwGetJoystickName(joystickId);
String name = type != ControllerType.UNKNOWN || joystickName == null ? type.friendlyName() : joystickName;
@ -53,8 +63,9 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
protected void setName(String name) {
String uniqueName = name;
int i = 0;
while (CONTROLLERS.values().stream().map(Controller::name).anyMatch(name::equalsIgnoreCase)) {
while (CONTROLLERS.values().stream().map(Controller::name).anyMatch(uniqueName::equalsIgnoreCase)) {
uniqueName = name + " (" + i++ + ")";
if (i > 1000) throw new IllegalStateException("Could not find a unique name for controller " + name + " (" + uid() + ")! (tried " + i + " times)");
}
this.name = uniqueName;
}
@ -105,6 +116,34 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
}
}
@Override
public boolean setRumble(float strongMagnitude, float weakMagnitude) {
if (!canRumble()) return false;
// the duration doesn't matter because we are not updating the joystick state,
// so there is never any SDL check to stop the rumble after the desired time.
if (!SDL.SDL_JoystickRumble(ptrJoystick, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 1)) {
Controlify.LOGGER.error("Could not rumble controller " + name() + ": " + SDL.SDL_GetError());
return false;
}
return true;
}
@Override
public boolean canRumble() {
return SDL2NativesManager.isLoaded() && config().allowVibrations;
}
@Override
public RumbleManager rumbleManager() {
return this.rumbleManager;
}
@Override
public void close() {
SDL.SDL_JoystickClose(ptrJoystick);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -7,10 +7,12 @@ import dev.isxander.controlify.controller.gamepad.GamepadController;
import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
import dev.isxander.controlify.debug.DebugProperties;
import dev.isxander.controlify.rumble.RumbleManager;
import org.lwjgl.glfw.GLFW;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public interface Controller<S extends ControllerState, C extends ControllerConfig> {
String uid();
@ -33,15 +35,22 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
void updateState();
void clearState();
RumbleManager rumbleManager();
default boolean canBeUsed() {
return true;
}
default void close() {
}
Map<String, Controller<?, ?>> CONTROLLERS = new HashMap<>();
static Controller<?, ?> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
if (CONTROLLERS.containsKey(hidInfo.createControllerUID())) {
return CONTROLLERS.get(hidInfo.createControllerUID());
Optional<String> uid = hidInfo.createControllerUID();
if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) {
return CONTROLLERS.get(uid.get());
}
if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK) {
@ -55,6 +64,11 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
return controller;
}
static void remove(Controller<?, ?> controller) {
CONTROLLERS.remove(controller.uid());
controller.close();
}
Controller<?, ?> DUMMY = new Controller<>() {
private final ControllerBindings<ControllerState> bindings = new ControllerBindings<>(this);
private final ControllerConfig config = new ControllerConfig() {
@ -133,5 +147,10 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
public void clearState() {
}
@Override
public RumbleManager rumbleManager() {
return null;
}
};
}

View File

@ -21,7 +21,9 @@ public abstract class ControllerConfig {
public float chatKeyboardHeight = 0f;
public boolean reduceBowSensitivity = true;
public boolean reduceAimingSensitivity = true;
public boolean allowVibrations = true;
public boolean calibrated = false;

View File

@ -78,8 +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();
public Optional<String> createControllerUID() {
return path.map(p -> UUID.nameUUIDFromBytes(p.getBytes())).map(UUID::toString);
}
}
}

View File

@ -8,11 +8,13 @@ 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 dev.isxander.controlify.rumble.RumbleCapable;
import dev.isxander.controlify.rumble.RumbleManager;
import org.lwjgl.glfw.GLFW;
import java.util.List;
public class CompoundJoystickController implements JoystickController<JoystickConfig> {
public class CompoundJoystickController implements JoystickController<JoystickConfig>, RumbleCapable {
private final String uid;
private final List<Integer> joysticks;
private final int axisCount, buttonCount, hatCount;
@ -23,6 +25,8 @@ public class CompoundJoystickController implements JoystickController<JoystickCo
private JoystickConfig config;
private final JoystickConfig defaultConfig;
private final RumbleManager rumbleManager;
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
public CompoundJoystickController(List<Integer> joystickIds, String uid, ControllerType compoundType) {
@ -39,6 +43,8 @@ public class CompoundJoystickController implements JoystickController<JoystickCo
this.config = new JoystickConfig(this);
this.defaultConfig = new JoystickConfig(this);
this.rumbleManager = new RumbleManager(this);
this.bindings = new ControllerBindings<>(this);
}
@ -133,6 +139,21 @@ public class CompoundJoystickController implements JoystickController<JoystickCo
return this.hatCount;
}
@Override
public boolean setRumble(float strongMagnitude, float weakMagnitude) {
return false;
}
@Override
public boolean canRumble() {
return false;
}
@Override
public RumbleManager rumbleManager() {
return this.rumbleManager;
}
@Override
public boolean canBeUsed() {
return JoystickController.super.canBeUsed()

View File

@ -0,0 +1,137 @@
package dev.isxander.controlify.controller.sdl2;
import dev.isxander.controlify.Controlify;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.Util;
import org.libsdl.SDL;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
public class SDL2NativesManager {
private static final Path NATIVES_FOLDER = FabricLoader.getInstance().getGameDir().resolve("controlify-natives");
private static final Map<Target, String> NATIVE_LIBRARIES = Map.of(
new Target(Util.OS.WINDOWS, true), "windows64/sdl2gdx64.dll",
new Target(Util.OS.WINDOWS, false), "windows32/sdl2gdx.dll",
new Target(Util.OS.LINUX, true), "linux64/libsdl2gdx64.so",
new Target(Util.OS.OSX, true), "macosx64/libsdl2gdx64.dylib"
);
private static final String NATIVE_LIBRARY_URL = "https://raw.githubusercontent.com/isXander/sdl2-jni/master/libs/";
private static Path osNativePath;
private static boolean loaded = false;
public static void initialise() {
if (loaded) return;
Controlify.LOGGER.info("Initialising SDL2 native library");
osNativePath = getNativesPathForOS().orElseGet(() -> {
Controlify.LOGGER.warn("No native library found for SDL2");
return null;
});
if (osNativePath == null) return;
if (!loadCachedLibrary()) {
downloadLibrary();
if (!loadCachedLibrary()) {
Controlify.LOGGER.warn("Failed to download and load SDL2 native library");
}
}
}
private static void startSDL2() {
SDL.SDL_SetHint("SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS", "1");
SDL.SDL_SetHint("SDL_ACCELEROMETER_AS_JOYSTICK", "0");
SDL.SDL_SetHint("SDL_MAC_BACKGROUND_APP", "1");
SDL.SDL_SetHint("SDL_XINPUT_ENABLED", "1");
SDL.SDL_SetHint("SDL_JOYSTICK_RAWINPUT", "0");
int joystickSubsystem = 0x00000200; // implies event subsystem
if (SDL.SDL_Init(joystickSubsystem) != 0) {
Controlify.LOGGER.error("Failed to initialise SDL2: " + SDL.SDL_GetError());
throw new RuntimeException("Failed to initialise SDL2: " + SDL.SDL_GetError());
}
Controlify.LOGGER.info("Initialised SDL2");
}
private static boolean loadCachedLibrary() {
if (!Files.exists(osNativePath)) return false;
Controlify.LOGGER.info("Loading SDL2 native library from " + osNativePath);
try {
SDL.load(osNativePath);
startSDL2();
loaded = true;
return true;
} catch (UnsatisfiedLinkError e) {
e.printStackTrace();
return false;
}
}
private static boolean downloadLibrary() {
Controlify.LOGGER.info("Downloading SDL2 native library");
try {
Files.deleteIfExists(osNativePath);
Files.createDirectories(osNativePath.getParent());
Files.createFile(osNativePath);
} catch (IOException e) {
e.printStackTrace();
return false;
}
try(FileOutputStream fileOutputStream = new FileOutputStream(osNativePath.toFile())) {
String downloadUrl = NATIVE_LIBRARY_URL + NATIVE_LIBRARIES.get(getNativeLibraryType());
URL url = new URL(downloadUrl);
ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());
FileChannel fileChannel = fileOutputStream.getChannel();
fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
Controlify.LOGGER.info("Downloaded SDL2 native library from " + downloadUrl);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
private static Target getNativeLibraryType() {
Util.OS os = Util.getPlatform();
boolean is64bit = System.getProperty("os.arch").contains("64");
return new Target(os, is64bit);
}
private static Optional<Path> getNativesPathForOS() {
String path = NATIVE_LIBRARIES.get(getNativeLibraryType());
if (path == null) {
Controlify.LOGGER.warn("No native library found for SDL " + getNativeLibraryType());
return Optional.empty();
}
return Optional.of(NATIVES_FOLDER.resolve(path));
}
public static boolean isLoaded() {
return loaded;
}
private record Target(Util.OS os, boolean is64Bit) {
}
}