forked from Clones/Controlify
Migrate to libsdl4j, SDL download screen, use gamecontrollerdb.txt
, calibration now detects joystick triggers
This commit is contained in:
@ -1,18 +1,19 @@
|
||||
package dev.isxander.controlify;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.mojang.blaze3d.Blaze3D;
|
||||
import com.sun.jna.Memory;
|
||||
import dev.isxander.controlify.api.ControlifyApi;
|
||||
import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint;
|
||||
import dev.isxander.controlify.compatibility.ControlifyCompat;
|
||||
import dev.isxander.controlify.controller.joystick.JoystickController;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
|
||||
import dev.isxander.controlify.gui.controllers.ControllerBindHandler;
|
||||
import dev.isxander.controlify.gui.screen.ControllerCarouselScreen;
|
||||
import dev.isxander.controlify.gui.screen.*;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controller.ControllerState;
|
||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||
import dev.isxander.controlify.driver.SDL2NativesManager;
|
||||
import dev.isxander.controlify.debug.DebugProperties;
|
||||
import dev.isxander.controlify.gui.screen.ControllerCalibrationScreen;
|
||||
import dev.isxander.controlify.gui.screen.SDLOnboardingScreen;
|
||||
import dev.isxander.controlify.gui.screen.SubmitUnknownControllerScreen;
|
||||
import dev.isxander.controlify.ingame.ControllerPlayerMovement;
|
||||
import dev.isxander.controlify.server.*;
|
||||
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
|
||||
@ -22,11 +23,13 @@ import dev.isxander.controlify.api.event.ControlifyEvents;
|
||||
import dev.isxander.controlify.gui.guide.InGameButtonGuide;
|
||||
import dev.isxander.controlify.ingame.InGameInputHandler;
|
||||
import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor;
|
||||
import dev.isxander.controlify.utils.ControllerUtils;
|
||||
import dev.isxander.controlify.utils.DebugLog;
|
||||
import dev.isxander.controlify.utils.Log;
|
||||
import dev.isxander.controlify.utils.ToastUtils;
|
||||
import dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
|
||||
import dev.isxander.controlify.wireless.LowBatteryNotifier;
|
||||
import io.github.libsdl4j.api.rwops.SDL_RWops;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||
@ -39,10 +42,15 @@ import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@ -50,6 +58,9 @@ import java.util.Queue;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static io.github.libsdl4j.api.gamecontroller.SdlGamecontroller.SDL_GameControllerAddMappingsFromRW;
|
||||
import static io.github.libsdl4j.api.rwops.SdlRWops.SDL_RWFromConstMem;
|
||||
|
||||
public class Controlify implements ControlifyApi {
|
||||
private static Controlify instance = null;
|
||||
|
||||
@ -67,7 +78,7 @@ public class Controlify implements ControlifyApi {
|
||||
private final ControlifyConfig config = new ControlifyConfig(this);
|
||||
|
||||
private final Queue<Controller<?, ?>> calibrationQueue = new ArrayDeque<>();
|
||||
private boolean canDiscoverControllers = true;
|
||||
private boolean hasDiscoveredControllers = false;
|
||||
|
||||
private int consecutiveInputSwitches = 0;
|
||||
private double lastInputSwitchTime = 0;
|
||||
@ -78,147 +89,19 @@ public class Controlify implements ControlifyApi {
|
||||
private double askSwitchTime = 0;
|
||||
private ToastUtils.ControlifyToast askSwitchToast = null;
|
||||
|
||||
public void initializeControlify() {
|
||||
Log.LOGGER.info("Initializing Controlify...");
|
||||
|
||||
config().load();
|
||||
|
||||
ControlifyCompat.init();
|
||||
|
||||
var controllersConnected = IntStream.range(0, GLFW.GLFW_JOYSTICK_LAST + 1).anyMatch(GLFW::glfwJoystickPresent);
|
||||
if (controllersConnected) {
|
||||
if (!config().globalSettings().delegateSetup) {
|
||||
askNatives().whenComplete((loaded, th) -> discoverControllers());
|
||||
} else {
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.setup_in_config.title"),
|
||||
Component.translatable(
|
||||
"controlify.toast.setup_in_config.description",
|
||||
Component.translatable("options.title"),
|
||||
Component.translatable("controls.keybinds.title"),
|
||||
Component.literal("Controlify")
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClientTickEvents.START_CLIENT_TICK.register(this::tick);
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(minecraft -> {
|
||||
controllerHIDService().stop();
|
||||
});
|
||||
|
||||
// listen for new controllers
|
||||
GLFW.glfwSetJoystickCallback((jid, event) -> {
|
||||
try {
|
||||
this.askNatives().whenComplete((loaded, th) -> {
|
||||
if (event == GLFW.GLFW_CONNECTED) {
|
||||
this.onControllerHotplugged(jid);
|
||||
} else if (event == GLFW.GLFW_DISCONNECTED) {
|
||||
this.onControllerDisconnect(jid);
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
notifyOfNewFeatures();
|
||||
}
|
||||
|
||||
private CompletableFuture<Boolean> askNatives() {
|
||||
if (nativeOnboardingFuture != null) return nativeOnboardingFuture;
|
||||
|
||||
if (config().globalSettings().vibrationOnboarded) {
|
||||
boolean loadNatives = config().globalSettings().loadVibrationNatives;
|
||||
if (loadNatives && !SDL2NativesManager.isInitialised()) {
|
||||
SDL2NativesManager.initialise();
|
||||
}
|
||||
return CompletableFuture.completedFuture(loadNatives);
|
||||
}
|
||||
|
||||
nativeOnboardingFuture = new CompletableFuture<>();
|
||||
|
||||
Screen parent = minecraft.screen;
|
||||
minecraft.setScreen(new SDLOnboardingScreen(
|
||||
() -> parent,
|
||||
answer -> {
|
||||
if (answer)
|
||||
SDL2NativesManager.initialise();
|
||||
nativeOnboardingFuture.complete(answer);
|
||||
}
|
||||
));
|
||||
|
||||
return nativeOnboardingFuture;
|
||||
}
|
||||
|
||||
public void discoverControllers() {
|
||||
if (!canDiscoverControllers) {
|
||||
throw new IllegalStateException("Already discovered/cannot discover controllers");
|
||||
}
|
||||
canDiscoverControllers = false;
|
||||
|
||||
DebugLog.log("Discovering and initializing controllers...");
|
||||
|
||||
if (config().globalSettings().loadVibrationNatives)
|
||||
SDL2NativesManager.initialise();
|
||||
|
||||
// find already connected controllers
|
||||
for (int jid = 0; jid <= GLFW.GLFW_JOYSTICK_LAST; jid++) {
|
||||
if (GLFW.glfwJoystickPresent(jid)) {
|
||||
var controllerOpt = ControllerManager.createOrGet(jid, controllerHIDService.fetchType(jid));
|
||||
if (controllerOpt.isEmpty()) continue;
|
||||
var controller = controllerOpt.get();
|
||||
|
||||
Log.LOGGER.info("Controller found: " + controller.name());
|
||||
|
||||
config().loadOrCreateControllerData(controller);
|
||||
|
||||
if (SubmitUnknownControllerScreen.canSubmit(controller)) {
|
||||
minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen));
|
||||
}
|
||||
|
||||
if (controller.uid().equals(config().currentControllerUid()))
|
||||
setCurrentController(controller);
|
||||
|
||||
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
|
||||
controller.config().allowVibrations = false;
|
||||
config().setDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ControllerManager.getConnectedControllers().isEmpty()) {
|
||||
Log.LOGGER.info("No controllers found.");
|
||||
}
|
||||
|
||||
if (getCurrentController().isEmpty()) {
|
||||
var controller = ControllerManager.getConnectedControllers().stream().findFirst().orElse(null);
|
||||
if (controller != null && controller.config().delayedCalibration) {
|
||||
controller = null;
|
||||
}
|
||||
|
||||
this.setCurrentController(controller);
|
||||
} else {
|
||||
// setCurrentController saves config
|
||||
config().saveIfDirty();
|
||||
}
|
||||
|
||||
FabricLoader.getInstance().getEntrypoints("controlify", ControlifyEntrypoint.class).forEach(entrypoint -> {
|
||||
try {
|
||||
entrypoint.onControllersDiscovered(this);
|
||||
} catch (Throwable e) {
|
||||
Log.LOGGER.error("Failed to run `onControllersDiscovered` on Controlify entrypoint: " + entrypoint.getClass().getName(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at usual fabric client entrypoint.
|
||||
* Always runs, even with no controllers detected.
|
||||
* In this state, Controlify is only partially loaded, no controllers
|
||||
* have been initialised, nor has the config. This is done at {@link Controlify#initializeControlify()}.
|
||||
* This is where regular fabric callbacks are registered.
|
||||
*/
|
||||
public void preInitialiseControlify() {
|
||||
DebugProperties.printProperties();
|
||||
|
||||
Log.LOGGER.info("Pre-initializing Controlify...");
|
||||
|
||||
this.inGameInputHandler = null;
|
||||
this.inGameInputHandler = null; // set when the current controller changes
|
||||
this.virtualMouseHandler = new VirtualMouseHandler();
|
||||
|
||||
controllerHIDService = new ControllerHIDService();
|
||||
@ -262,9 +145,315 @@ public class Controlify implements ControlifyApi {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once Minecraft has completely loaded.
|
||||
* (When the loading overlay starts to fade).
|
||||
*
|
||||
* This is where controllers are usually initialised, as long
|
||||
* as one or more controllers are connected.
|
||||
*/
|
||||
public void initializeControlify() {
|
||||
Log.LOGGER.info("Initializing Controlify...");
|
||||
|
||||
config().load();
|
||||
|
||||
// initialise and compatability modules that controlify implements itself
|
||||
// this does NOT invoke any entrypoints. this is done in the pre-initialisation phase
|
||||
ControlifyCompat.init();
|
||||
|
||||
var controllersConnected = IntStream.range(0, GLFW.GLFW_JOYSTICK_LAST + 1)
|
||||
.anyMatch(GLFW::glfwJoystickPresent);
|
||||
if (controllersConnected) { // only initialise Controlify if controllers are detected
|
||||
if (!config().globalSettings().delegateSetup) {
|
||||
// check native onboarding then discover controllers
|
||||
askNatives().whenComplete((loaded, th) -> discoverControllers());
|
||||
} else {
|
||||
// delegate setup: don't auto set up controllers, require the user to open config screen
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.setup_in_config.title"),
|
||||
Component.translatable(
|
||||
"controlify.toast.setup_in_config.description",
|
||||
Component.translatable("options.title"),
|
||||
Component.translatable("controls.keybinds.title"),
|
||||
Component.literal("Controlify")
|
||||
),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// register events
|
||||
ClientTickEvents.START_CLIENT_TICK.register(this::tick);
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(minecraft -> {
|
||||
controllerHIDService().stop();
|
||||
});
|
||||
|
||||
// set up the hotplugging callback with GLFW
|
||||
// TODO: investigate if there is any benefit to implementing this with SDL
|
||||
GLFW.glfwSetJoystickCallback((jid, event) -> {
|
||||
try {
|
||||
this.askNatives().whenComplete((loaded, th) -> {
|
||||
if (event == GLFW.GLFW_CONNECTED) {
|
||||
this.onControllerHotplugged(jid);
|
||||
} else if (event == GLFW.GLFW_DISCONNECTED) {
|
||||
this.onControllerDisconnect(jid);
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
Log.LOGGER.error("Failed to handle controller connect/disconnect event", e);
|
||||
}
|
||||
});
|
||||
|
||||
// sends toasts of new features
|
||||
notifyOfNewFeatures();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loops through every controller slot and initialises it if it is connected.
|
||||
* This is guaranteed to be called at most once. If no controllers are connected
|
||||
* in the whole game lifecycle, this is never ran.
|
||||
*/
|
||||
public void discoverControllers() {
|
||||
if (hasDiscoveredControllers) {
|
||||
Log.LOGGER.warn("Attempted to discover controllers twice!");
|
||||
}
|
||||
hasDiscoveredControllers = true;
|
||||
|
||||
DebugLog.log("Discovering and initializing controllers...");
|
||||
|
||||
// load gamepad mappings before every
|
||||
minecraft.getResourceManager()
|
||||
.getResource(Controlify.id("controllers/gamecontrollerdb.txt"))
|
||||
.ifPresent(this::loadGamepadMappings);
|
||||
|
||||
// find already connected controllers
|
||||
// TODO: investigate if there is any benefit to implementing this with SDL
|
||||
for (int jid = 0; jid <= GLFW.GLFW_JOYSTICK_LAST; jid++) {
|
||||
if (GLFW.glfwJoystickPresent(jid)) {
|
||||
Optional<Controller<?, ?>> controllerOpt = ControllerManager.createOrGet(
|
||||
jid,
|
||||
controllerHIDService.fetchType(jid)
|
||||
);
|
||||
if (controllerOpt.isEmpty())
|
||||
continue;
|
||||
Controller<?, ?> controller = controllerOpt.get();
|
||||
|
||||
Log.LOGGER.info("Controller found: " + ControllerUtils.createControllerString(controller));
|
||||
|
||||
boolean newController = !config().loadOrCreateControllerData(controller);
|
||||
|
||||
if (SubmitUnknownControllerScreen.canSubmit(controller)) {
|
||||
minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen));
|
||||
}
|
||||
|
||||
// only "equip" the controller if it has already been calibrated
|
||||
if (!controller.config().deadzonesCalibrated) {
|
||||
calibrationQueue.add(controller);
|
||||
} else if (controller.uid().equals(config().currentControllerUid())) {
|
||||
setCurrentController(controller);
|
||||
}
|
||||
|
||||
// make sure that allow vibrations is not mismatched with the native library setting
|
||||
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
|
||||
controller.config().allowVibrations = false;
|
||||
config().setDirty();
|
||||
}
|
||||
|
||||
// if a joystick and unmapped, tell the user that they need to configure the controls
|
||||
// joysticks have an abstract number of inputs, so applying a default control scheme is impossible
|
||||
if (newController && controller instanceof JoystickController<?> joystick && joystick.mapping() instanceof UnmappedJoystickMapping) {
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.unmapped_joystick.title"),
|
||||
Component.translatable("controlify.toast.unmapped_joystick.description", controller.name()),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ControllerManager.getConnectedControllers().isEmpty()) {
|
||||
Log.LOGGER.info("No controllers found.");
|
||||
}
|
||||
|
||||
// if no controller is currently selected, select the first one
|
||||
if (getCurrentController().isEmpty()) {
|
||||
var controller = ControllerManager.getConnectedControllers().stream().findFirst().orElse(null);
|
||||
if (controller != null && (controller.config().delayedCalibration || !controller.config().deadzonesCalibrated)) {
|
||||
controller = null;
|
||||
}
|
||||
|
||||
this.setCurrentController(controller);
|
||||
} else {
|
||||
// setCurrentController saves config so there is no need to set dirty to save
|
||||
config().saveIfDirty();
|
||||
}
|
||||
|
||||
FabricLoader.getInstance().getEntrypoints("controlify", ControlifyEntrypoint.class).forEach(entrypoint -> {
|
||||
try {
|
||||
entrypoint.onControllersDiscovered(this);
|
||||
} catch (Throwable e) {
|
||||
Log.LOGGER.error("Failed to run `onControllersDiscovered` on Controlify entrypoint: " + entrypoint.getClass().getName(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a controller has been connected after mod initialisation.
|
||||
* If this is the first controller to be connected in the game's lifecycle,
|
||||
* this is delegated to {@link Controlify#discoverControllers()} for it to be "discovered",
|
||||
* otherwise the controller is initialised and added to the list of connected controllers.
|
||||
*/
|
||||
private void onControllerHotplugged(int jid) {
|
||||
if (!hasDiscoveredControllers) {
|
||||
discoverControllers();
|
||||
return;
|
||||
}
|
||||
|
||||
var controllerOpt = ControllerManager.createOrGet(jid, controllerHIDService.fetchType(jid));
|
||||
if (controllerOpt.isEmpty()) return;
|
||||
var controller = controllerOpt.get();
|
||||
|
||||
Log.LOGGER.info("Controller connected: " + ControllerUtils.createControllerString(controller));
|
||||
|
||||
config().loadOrCreateControllerData(controller);
|
||||
|
||||
if (SubmitUnknownControllerScreen.canSubmit(controller)) {
|
||||
minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen));
|
||||
}
|
||||
|
||||
if (config().globalSettings().delegateSetup) {
|
||||
config().globalSettings().delegateSetup = false;
|
||||
config().setDirty();
|
||||
}
|
||||
|
||||
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
|
||||
controller.config().allowVibrations = false;
|
||||
config().setDirty();
|
||||
}
|
||||
|
||||
if (ControllerManager.getConnectedControllers().size() == 1 && (controller.config().deadzonesCalibrated || controller.config().delayedCalibration)) {
|
||||
this.setCurrentController(controller);
|
||||
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.default_controller_connected.title"),
|
||||
Component.translatable("controlify.toast.default_controller_connected.description"),
|
||||
false
|
||||
);
|
||||
} else {
|
||||
this.askToSwitchController(controller);
|
||||
config().saveIfDirty();
|
||||
}
|
||||
|
||||
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
|
||||
controllerListScreen.refreshControllers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a controller is disconnected.
|
||||
* Equips another controller if available.
|
||||
*
|
||||
* @param jid the joystick id of the disconnected controller
|
||||
*/
|
||||
private void onControllerDisconnect(int jid) {
|
||||
ControllerManager.getConnectedControllers().stream().filter(controller -> controller.joystickId() == jid).findAny().ifPresent(controller -> {
|
||||
ControllerManager.disconnect(controller);
|
||||
|
||||
controller.hidInfo().ifPresent(controllerHIDService::unconsumeController);
|
||||
|
||||
setCurrentController(ControllerManager.getConnectedControllers().stream().findFirst().orElse(null));
|
||||
Log.LOGGER.info("Controller disconnected: " + controller.name());
|
||||
this.setInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER);
|
||||
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.controller_disconnected.title"),
|
||||
Component.translatable("controlify.toast.controller_disconnected.description", controller.name()),
|
||||
false
|
||||
);
|
||||
|
||||
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
|
||||
controllerListScreen.refreshControllers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the user if they want to download the SDL2 library,
|
||||
* or initialises it if it hasn't been already.
|
||||
* If the user has already been asked and SDL is already initialised,
|
||||
* a completed future is returned.
|
||||
* The future is completed once the user has made their choice and SDL
|
||||
* has been downloaded and initialised (or not).
|
||||
*/
|
||||
public CompletableFuture<Boolean> askNatives() {
|
||||
// if the future already exists, just return it
|
||||
if (nativeOnboardingFuture != null)
|
||||
return nativeOnboardingFuture;
|
||||
|
||||
// the user has already been asked, initialise SDL if necessary
|
||||
// and return a completed future
|
||||
if (config().globalSettings().vibrationOnboarded) {
|
||||
if (config().globalSettings().loadVibrationNatives) {
|
||||
return nativeOnboardingFuture = SDL2NativesManager.maybeLoad();
|
||||
}
|
||||
// micro-optimization. no need to create a new future every time. use the first not null check
|
||||
return nativeOnboardingFuture = CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
nativeOnboardingFuture = new CompletableFuture<>();
|
||||
|
||||
// open the SDL onboarding screen. complete the future when the user has made their choice
|
||||
Screen parent = minecraft.screen;
|
||||
minecraft.setScreen(new SDLOnboardingScreen(
|
||||
() -> parent,
|
||||
answer -> {
|
||||
if (answer) {
|
||||
SDL2NativesManager.maybeLoad().whenComplete((loaded, th) -> {
|
||||
if (th != null) nativeOnboardingFuture.completeExceptionally(th);
|
||||
else nativeOnboardingFuture.complete(loaded);
|
||||
});
|
||||
} else {
|
||||
nativeOnboardingFuture.complete(false);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
return nativeOnboardingFuture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the gamepad mappings for both GLFW and SDL2.
|
||||
* @param resource the already located `gamecontrollerdb.txt` resource
|
||||
*/
|
||||
private void loadGamepadMappings(Resource resource) {
|
||||
try (InputStream is = resource.open()) {
|
||||
byte[] bytes = ByteStreams.toByteArray(is);
|
||||
|
||||
ByteBuffer buffer = MemoryUtil.memASCIISafe(new String(bytes));
|
||||
if (!GLFW.glfwUpdateGamepadMappings(buffer)) {
|
||||
Log.LOGGER.error("GLFW failed to load gamepad mappings!");
|
||||
}
|
||||
|
||||
try (Memory memory = new Memory(bytes.length)) {
|
||||
memory.write(0, bytes, 0, bytes.length);
|
||||
SDL_RWops rw = SDL_RWFromConstMem(memory, (int) memory.size());
|
||||
int count = SDL_GameControllerAddMappingsFromRW(rw, 1);
|
||||
if (count < 1) {
|
||||
Log.LOGGER.error("SDL2 failed to load gamepad mappings!");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.LOGGER.error("Failed to load gamecontrollerdb.txt", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main loop of Controlify.
|
||||
* In Controlify's current state, only the current controller is ticked.
|
||||
*/
|
||||
public void tick(Minecraft client) {
|
||||
if (minecraft.getOverlay() == null) {
|
||||
if (!calibrationQueue.isEmpty() && !(minecraft.screen instanceof SDLOnboardingScreen)) {
|
||||
if (!calibrationQueue.isEmpty() && !(minecraft.screen instanceof DontInteruptScreen)) {
|
||||
Screen screen = minecraft.screen;
|
||||
while (!calibrationQueue.isEmpty()) {
|
||||
screen = new ControllerCalibrationScreen(calibrationQueue.poll(), screen);
|
||||
@ -372,70 +561,6 @@ public class Controlify implements ControlifyApi {
|
||||
return config;
|
||||
}
|
||||
|
||||
private void onControllerHotplugged(int jid) {
|
||||
var controllerOpt = ControllerManager.createOrGet(jid, controllerHIDService.fetchType(jid));
|
||||
if (controllerOpt.isEmpty()) return;
|
||||
var controller = controllerOpt.get();
|
||||
|
||||
Log.LOGGER.info("Controller connected: " + controller.name());
|
||||
|
||||
config().loadOrCreateControllerData(controller);
|
||||
|
||||
if (SubmitUnknownControllerScreen.canSubmit(controller)) {
|
||||
minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen));
|
||||
}
|
||||
|
||||
canDiscoverControllers = false;
|
||||
if (config().globalSettings().delegateSetup) {
|
||||
config().globalSettings().delegateSetup = false;
|
||||
config().setDirty();
|
||||
}
|
||||
|
||||
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
|
||||
controller.config().allowVibrations = false;
|
||||
config().setDirty();
|
||||
}
|
||||
|
||||
if (ControllerManager.getConnectedControllers().size() == 1) {
|
||||
this.setCurrentController(controller);
|
||||
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.default_controller_connected.title"),
|
||||
Component.translatable("controlify.toast.default_controller_connected.description"),
|
||||
false
|
||||
);
|
||||
} else {
|
||||
this.askToSwitchController(controller);
|
||||
config().saveIfDirty();
|
||||
}
|
||||
|
||||
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
|
||||
controllerListScreen.refreshControllers();
|
||||
}
|
||||
}
|
||||
|
||||
private void onControllerDisconnect(int jid) {
|
||||
ControllerManager.getConnectedControllers().stream().filter(controller -> controller.joystickId() == jid).findAny().ifPresent(controller -> {
|
||||
ControllerManager.disconnect(controller);
|
||||
|
||||
controller.hidInfo().ifPresent(controllerHIDService::unconsumeController);
|
||||
|
||||
setCurrentController(ControllerManager.getConnectedControllers().stream().findFirst().orElse(null));
|
||||
Log.LOGGER.info("Controller disconnected: " + controller.name());
|
||||
this.setInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER);
|
||||
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.controller_disconnected.title"),
|
||||
Component.translatable("controlify.toast.controller_disconnected.description", controller.name()),
|
||||
false
|
||||
);
|
||||
|
||||
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
|
||||
controllerListScreen.refreshControllers();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void askToSwitchController(Controller<?, ?> controller) {
|
||||
this.switchableController = controller;
|
||||
this.askSwitchTime = Blaze3D.getTime();
|
||||
|
@ -164,14 +164,16 @@ public class ControlifyConfig {
|
||||
}
|
||||
}
|
||||
|
||||
public void loadOrCreateControllerData(Controller<?, ?> controller) {
|
||||
public boolean loadOrCreateControllerData(Controller<?, ?> controller) {
|
||||
var uid = controller.uid();
|
||||
if (controllerData.has(uid)) {
|
||||
DebugLog.log("Loading controller data for " + uid);
|
||||
applyControllerConfig(controller, controllerData.getAsJsonObject(uid));
|
||||
return true;
|
||||
} else {
|
||||
DebugLog.log("New controller found, setting config dirty ({})", uid);
|
||||
setDirty();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,11 @@ package dev.isxander.controlify.config;
|
||||
|
||||
import com.terraformersmc.modmenu.api.ConfigScreenFactory;
|
||||
import com.terraformersmc.modmenu.api.ModMenuApi;
|
||||
import dev.isxander.controlify.gui.screen.ControllerCarouselScreen;
|
||||
import dev.isxander.controlify.gui.screen.ModConfigOpenerScreen;
|
||||
|
||||
public class ModMenuIntegration implements ModMenuApi {
|
||||
@Override
|
||||
public ConfigScreenFactory<?> getModConfigScreenFactory() {
|
||||
return ControllerCarouselScreen::createConfigScreen;
|
||||
return ModConfigOpenerScreen::new;
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,12 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
|
||||
return true;
|
||||
}
|
||||
|
||||
int axisCount();
|
||||
|
||||
int buttonCount();
|
||||
|
||||
int hatCount();
|
||||
|
||||
@Deprecated
|
||||
Controller<?, ?> DUMMY = new Controller<>() {
|
||||
private final ControllerBindings<ControllerState> bindings = new ControllerBindings<>(this);
|
||||
@ -165,5 +171,20 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
|
||||
public boolean supportsRumble() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int axisCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int buttonCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hatCount() {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ public record ControllerType(String friendlyName, String mappingId, String theme
|
||||
String friendlyName = null;
|
||||
String legacyIdentifier = null;
|
||||
String themeId = null;
|
||||
String mappingId = null;
|
||||
String mappingId = "unmapped";
|
||||
boolean forceJoystick = false;
|
||||
boolean dontLoad = false;
|
||||
Set<HIDIdentifier> hids = new HashSet<>();
|
||||
|
@ -149,4 +149,19 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
||||
String theme = config().theme == BuiltinGamepadTheme.DEFAULT ? type().themeId() : config().theme.id();
|
||||
return Controlify.id("textures/gui/gamepad/" + theme + "/icon.png");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int axisCount() {
|
||||
return 6;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int buttonCount() {
|
||||
return 15;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hatCount() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,11 @@ import dev.isxander.controlify.controller.ControllerConfig;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
public class JoystickConfig extends ControllerConfig {
|
||||
private Map<String, Float> deadzones;
|
||||
private Set<Integer> triggerAxes = new HashSet<>();
|
||||
|
||||
private transient JoystickController<?> controller;
|
||||
|
||||
@ -33,8 +33,27 @@ public class JoystickConfig extends ControllerConfig {
|
||||
return deadzones.getOrDefault(controller.mapping().axes()[axis].identifier(), 0.2f);
|
||||
}
|
||||
|
||||
public boolean isTriggerAxis(int axis) {
|
||||
if (axis < 0)
|
||||
throw new IllegalArgumentException("Axis cannot be negative!");
|
||||
|
||||
return triggerAxes.contains(axis);
|
||||
}
|
||||
|
||||
public void setTriggerAxis(int axis, boolean isTrigger) {
|
||||
if (axis < 0)
|
||||
throw new IllegalArgumentException("Axis cannot be negative!");
|
||||
|
||||
if (isTrigger) {
|
||||
triggerAxes.add(axis);
|
||||
} else {
|
||||
triggerAxes.remove(axis);
|
||||
}
|
||||
}
|
||||
|
||||
void setup(JoystickController<?> controller) {
|
||||
this.controller = controller;
|
||||
|
||||
if (this.deadzones == null) {
|
||||
deadzones = new HashMap<>();
|
||||
for (int i = 0; i < controller.mapping().axes().length; i++) {
|
||||
|
@ -16,13 +16,6 @@ public interface JoystickController<T extends JoystickConfig> extends Controller
|
||||
return Controlify.id("textures/gui/joystick/icon.png");
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
int axisCount();
|
||||
@Deprecated
|
||||
int buttonCount();
|
||||
@Deprecated
|
||||
int hatCount();
|
||||
|
||||
@Override
|
||||
default boolean canBeUsed() {
|
||||
return !(mapping() instanceof UnmappedJoystickMapping);
|
||||
|
@ -4,22 +4,25 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
||||
import dev.isxander.controlify.controller.AbstractController;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
|
||||
import dev.isxander.controlify.hid.ControllerHIDService;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.RPJoystickMapping;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
|
||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||
import dev.isxander.controlify.driver.SDL2NativesManager;
|
||||
import dev.isxander.controlify.rumble.RumbleManager;
|
||||
import dev.isxander.controlify.rumble.RumbleSource;
|
||||
import dev.isxander.controlify.utils.Log;
|
||||
import org.libsdl.SDL;
|
||||
import io.github.libsdl4j.api.joystick.SDL_Joystick;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static io.github.libsdl4j.api.error.SdlError.*;
|
||||
import static io.github.libsdl4j.api.joystick.SdlJoystick.*;
|
||||
|
||||
public class SingleJoystickController extends AbstractController<JoystickState, JoystickConfig> implements JoystickController<JoystickConfig> {
|
||||
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
|
||||
private final JoystickMapping mapping;
|
||||
|
||||
private final long ptrJoystick;
|
||||
private final SDL_Joystick ptrJoystick;
|
||||
private RumbleManager rumbleManager;
|
||||
private boolean rumbleSupported;
|
||||
|
||||
@ -31,8 +34,8 @@ public class SingleJoystickController extends AbstractController<JoystickState,
|
||||
this.config = new JoystickConfig(this);
|
||||
this.defaultConfig = new JoystickConfig(this);
|
||||
|
||||
this.ptrJoystick = SDL2NativesManager.isLoaded() ? SDL.SDL_JoystickOpen(joystickId) : 0;
|
||||
this.rumbleSupported = SDL2NativesManager.isLoaded() && SDL.SDL_JoystickHasRumble(this.ptrJoystick);
|
||||
this.ptrJoystick = SDL2NativesManager.isLoaded() ? SDL_JoystickOpen(joystickId) : new SDL_Joystick();
|
||||
this.rumbleSupported = SDL2NativesManager.isLoaded() && SDL_JoystickHasRumble(this.ptrJoystick);
|
||||
this.rumbleManager = new RumbleManager(this);
|
||||
|
||||
this.bindings = new ControllerBindings<>(this);
|
||||
@ -91,6 +94,16 @@ public class SingleJoystickController extends AbstractController<JoystickState,
|
||||
public void setConfig(Gson gson, JsonElement json) {
|
||||
super.setConfig(gson, json);
|
||||
this.config.setup(this);
|
||||
|
||||
if (mapping() instanceof UnmappedJoystickMapping unmapped) {
|
||||
for (int i = 0; i < unmapped.axes().length; i++) {
|
||||
unmapped.axes()[i].setTriggerAxis(this.config.isTriggerAxis(i));
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < mapping().axes().length; i++) {
|
||||
this.config.setTriggerAxis(i, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -99,8 +112,8 @@ public class SingleJoystickController extends AbstractController<JoystickState,
|
||||
|
||||
// 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_JoystickRumbleTriggers(ptrJoystick, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 1)) {
|
||||
Log.LOGGER.error("Could not rumble controller " + name() + ": " + SDL.SDL_GetError());
|
||||
if (SDL_JoystickRumbleTriggers(ptrJoystick, (short)(strongMagnitude * 65535.0F), (short)(weakMagnitude * 65535.0F), 1) != 0) {
|
||||
Log.LOGGER.error("Could not rumble controller " + name() + ": " + SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -118,9 +131,11 @@ public class SingleJoystickController extends AbstractController<JoystickState,
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (ptrJoystick != 0)
|
||||
SDL.SDL_JoystickClose(ptrJoystick);
|
||||
if (!ptrJoystick.equals(new SDL_Joystick()))
|
||||
SDL_JoystickClose(ptrJoystick);
|
||||
this.rumbleSupported = false;
|
||||
this.rumbleManager = null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ public class RPJoystickMapping implements JoystickMapping {
|
||||
public static JoystickMapping fromType(JoystickController<?> joystick) {
|
||||
var resource = Minecraft.getInstance().getResourceManager().getResource(new ResourceLocation("controlify", "mappings/" + joystick.type().mappingId() + ".json"));
|
||||
if (resource.isEmpty()) {
|
||||
Log.LOGGER.warn("No joystick mapping found for controller: '" + joystick.type().mappingId() + "'");
|
||||
Log.LOGGER.warn("No joystick mapping found for controller type: '{}' - using unmapped", joystick.type().mappingId());
|
||||
return new UnmappedJoystickMapping(joystick.joystickId());
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,8 @@ import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public class UnmappedJoystickMapping implements JoystickMapping {
|
||||
@ -21,7 +23,7 @@ public class UnmappedJoystickMapping implements JoystickMapping {
|
||||
private UnmappedJoystickMapping(int axisCount, int buttonCount, int hatCount) {
|
||||
this.axes = new UnmappedAxis[axisCount];
|
||||
for (int i = 0; i < axisCount; i++) {
|
||||
this.axes[i] = new UnmappedAxis(i, new GenericRenderer.Axis(Integer.toString(i + 1)));
|
||||
this.axes[i] = new UnmappedAxis(i, new GenericRenderer.Axis(Integer.toString(i + 1)), false);
|
||||
}
|
||||
|
||||
this.buttons = new UnmappedButton[buttonCount];
|
||||
@ -44,7 +46,7 @@ public class UnmappedJoystickMapping implements JoystickMapping {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Axis[] axes() {
|
||||
public UnmappedAxis[] axes() {
|
||||
return axes;
|
||||
}
|
||||
|
||||
@ -58,15 +60,32 @@ public class UnmappedJoystickMapping implements JoystickMapping {
|
||||
return hats;
|
||||
}
|
||||
|
||||
private record UnmappedAxis(int axis, GenericRenderer.Axis renderer) implements Axis {
|
||||
public void setTriggerAxes(int axis, boolean triggerAxis) {
|
||||
axes[axis].setTriggerAxis(triggerAxis);
|
||||
}
|
||||
|
||||
public static final class UnmappedAxis implements Axis {
|
||||
private final int axis;
|
||||
private final GenericRenderer.Axis renderer;
|
||||
private boolean triggerAxis;
|
||||
|
||||
private UnmappedAxis(int axis, GenericRenderer.Axis renderer, boolean triggerAxis) {
|
||||
this.axis = axis;
|
||||
this.renderer = renderer;
|
||||
this.triggerAxis = triggerAxis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getAxis(JoystickData data) {
|
||||
return data.axes()[axis];
|
||||
float value = data.axes()[axis];
|
||||
if (triggerAxis)
|
||||
value = (value + 1) / 2;
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String identifier() {
|
||||
return "axis-" + axis;
|
||||
return "axis-" + axis;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -76,7 +95,7 @@ public class UnmappedJoystickMapping implements JoystickMapping {
|
||||
|
||||
@Override
|
||||
public boolean requiresDeadzone() {
|
||||
return true;
|
||||
return !triggerAxis;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -93,6 +112,45 @@ public class UnmappedJoystickMapping implements JoystickMapping {
|
||||
public String getDirectionIdentifier(int axis, JoystickAxisBind.AxisDirection direction) {
|
||||
return direction.name().toLowerCase();
|
||||
}
|
||||
|
||||
public int index() {
|
||||
return axis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GenericRenderer.Axis renderer() {
|
||||
return renderer;
|
||||
}
|
||||
|
||||
public boolean isTriggerAxis() {
|
||||
return triggerAxis;
|
||||
}
|
||||
|
||||
public void setTriggerAxis(boolean triggerAxis) {
|
||||
this.triggerAxis = triggerAxis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (obj == null || obj.getClass() != this.getClass()) return false;
|
||||
var that = (UnmappedAxis) obj;
|
||||
return this.axis == that.axis &&
|
||||
Objects.equals(this.renderer, that.renderer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(axis, renderer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UnmappedAxis[" +
|
||||
"axis=" + axis + ", " +
|
||||
"renderer=" + renderer + ']';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record UnmappedButton(int button, GenericRenderer.Button renderer) implements Button {
|
||||
|
@ -1,163 +0,0 @@
|
||||
package dev.isxander.controlify.controller.sdl2;
|
||||
|
||||
import dev.isxander.controlify.utils.DebugLog;
|
||||
import dev.isxander.controlify.utils.Log;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.Util;
|
||||
import org.libsdl.SDL;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
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.Comparator;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.libsdl.SDL_Hints.*;
|
||||
|
||||
public class SDL2NativesManager {
|
||||
private static final String SDL2_VERSION = "<SDL2_VERSION>";
|
||||
private static final Map<Target, String> NATIVE_LIBRARIES = Map.of(
|
||||
new Target(Util.OS.WINDOWS, true, false), "windows64.dll",
|
||||
new Target(Util.OS.WINDOWS, false, false), "window32.dll",
|
||||
new Target(Util.OS.LINUX, true, false), "linux64.so",
|
||||
new Target(Util.OS.OSX, true, false), "macosx64.dylib",
|
||||
new Target(Util.OS.OSX, true, true), "macosxarm64.dylib"
|
||||
);
|
||||
private static final String NATIVE_LIBRARY_URL = "https://maven.isxander.dev/releases/dev/isxander/sdl2-jni-natives/%s/".formatted(SDL2_VERSION);
|
||||
|
||||
private static boolean loaded = false;
|
||||
private static boolean initialised = false;
|
||||
|
||||
public static void initialise() {
|
||||
if (initialised) return;
|
||||
initialised = true;
|
||||
|
||||
DebugLog.log("Initialising SDL2 native library");
|
||||
|
||||
if (!Target.CURRENT.hasNativeLibrary()) {
|
||||
Log.LOGGER.warn("SDL2 native library not available for OS: " + Target.CURRENT);
|
||||
return;
|
||||
}
|
||||
|
||||
Path localLibraryPath = Target.CURRENT.getLocalNativePath();
|
||||
if (Files.notExists(localLibraryPath)) {
|
||||
if (Files.exists(localLibraryPath.getParent())) {
|
||||
try(var walk = Files.walk(localLibraryPath.getParent())) {
|
||||
walk.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEachOrdered(File::delete);
|
||||
} catch (Exception e) {
|
||||
Log.LOGGER.error("Failed to delete old SDL2 native library", e);
|
||||
}
|
||||
}
|
||||
|
||||
Log.LOGGER.info("Downloading SDL2 native library: " + Target.CURRENT.getArtifactName());
|
||||
downloadLibrary(localLibraryPath);
|
||||
}
|
||||
|
||||
try {
|
||||
SDL.load(localLibraryPath);
|
||||
|
||||
startSDL2();
|
||||
|
||||
loaded = true;
|
||||
} catch (Exception e) {
|
||||
Log.LOGGER.error("Failed to load SDL2 native library", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void startSDL2() {
|
||||
// we have no windows, so all events are background events
|
||||
SDL.SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||
// accelerometer as joystick is not good UX. unexpected
|
||||
SDL.SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0");
|
||||
// see first hint
|
||||
SDL.SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1");
|
||||
// raw input requires controller correlation, which is impossible
|
||||
// without calling JoystickUpdate, which we don't do.
|
||||
SDL.SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0");
|
||||
// better rumble
|
||||
SDL.SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1");
|
||||
SDL.SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
||||
SDL.SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
|
||||
SDL.SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1");
|
||||
|
||||
int joystickSubsystem = 0x00000200; // implies event subsystem
|
||||
int gameControllerSubsystem = 0x00002000; // implies event subsystem
|
||||
if (SDL.SDL_Init(joystickSubsystem | gameControllerSubsystem) != 0) {
|
||||
Log.LOGGER.error("Failed to initialise SDL2: " + SDL.SDL_GetError());
|
||||
throw new RuntimeException("Failed to initialise SDL2: " + SDL.SDL_GetError());
|
||||
}
|
||||
|
||||
DebugLog.log("Initialised SDL2");
|
||||
}
|
||||
|
||||
private static boolean downloadLibrary(Path path) {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.createFile(path);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
try(FileOutputStream fileOutputStream = new FileOutputStream(path.toFile())) {
|
||||
String url = NATIVE_LIBRARY_URL + Target.CURRENT.getArtifactName();
|
||||
URL downloadUrl = new URL(url);
|
||||
ReadableByteChannel readableByteChannel = Channels.newChannel(downloadUrl.openStream());
|
||||
FileChannel fileChannel = fileOutputStream.getChannel();
|
||||
fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
|
||||
Log.LOGGER.info("Downloaded SDL2 native library from " + downloadUrl);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public static boolean isInitialised() {
|
||||
return initialised;
|
||||
}
|
||||
|
||||
public record Target(Util.OS os, boolean is64Bit, boolean isARM) {
|
||||
public static final Target CURRENT = Util.make(() -> {
|
||||
Util.OS os = Util.getPlatform();
|
||||
|
||||
String arch = System.getProperty("os.arch");
|
||||
boolean is64bit = arch.contains("64");
|
||||
boolean isARM = arch.contains("arm") || arch.contains("aarch");
|
||||
|
||||
return new Target(os, is64bit, isARM);
|
||||
});
|
||||
|
||||
public boolean hasNativeLibrary() {
|
||||
return NATIVE_LIBRARIES.containsKey(this);
|
||||
}
|
||||
|
||||
public String getArtifactName() {
|
||||
String suffix = NATIVE_LIBRARIES.get(Target.CURRENT);
|
||||
return "sdl2-jni-natives-" + SDL2_VERSION + "-" + suffix;
|
||||
}
|
||||
|
||||
public Path getLocalNativePath() {
|
||||
return FabricLoader.getInstance().getGameDir()
|
||||
.resolve("controlify-natives")
|
||||
.resolve(getArtifactName());
|
||||
}
|
||||
|
||||
public boolean isMacArm() {
|
||||
return os == Util.OS.OSX && isARM;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,21 +10,21 @@ public class DebugProperties {
|
||||
private static final List<DebugProperty> properties = new ArrayList<>();
|
||||
|
||||
public static final boolean DEBUG_LOGGING = boolProp("controlify.debug.logging", false, true);
|
||||
/* Print the VID and PID of every controller connected. */
|
||||
/** Print the VID and PID of every controller connected. */
|
||||
public static final boolean PRINT_VID_PID = boolProp("controlify.debug.print_vid_pid", false, true);
|
||||
/* Renders debug overlay for vmouse snapping */
|
||||
/** Renders debug overlay for vmouse snapping */
|
||||
public static final boolean DEBUG_SNAPPING = boolProp("controlify.debug.snapping", false, false);
|
||||
/* Forces all gamepads to be treated as a regular joystick */
|
||||
/** Forces all gamepads to be treated as a regular joystick */
|
||||
public static final boolean FORCE_JOYSTICK = boolProp("controlify.debug.force_joystick", false, false);
|
||||
/* Prints joystick input counts for making joystick mappings */
|
||||
/** Prints joystick input counts for making joystick mappings */
|
||||
public static final boolean PRINT_JOY_STATE = boolProp("controlify.debug.print_joy_state", false, false);
|
||||
/* Print gyro data if supported */
|
||||
/** Print gyro data if supported */
|
||||
public static final boolean PRINT_GYRO = boolProp("controlify.debug.print_gyro", false, false);
|
||||
/* Print what drivers are being used */
|
||||
/** Print what drivers are being used */
|
||||
public static final boolean PRINT_DRIVER = boolProp("controlify.debug.print_driver", true, true);
|
||||
/* Print the state of the left and right triggers on gamepads */
|
||||
/** Print the state of the left and right triggers on gamepads */
|
||||
public static final boolean PRINT_TRIGGER_STATE = boolProp("controlify.debug.print_trigger_state", false, false);
|
||||
/* Use experimental anti-snapback */
|
||||
/** Use experimental anti-snapback */
|
||||
public static final boolean USE_SNAPBACK = boolProp("controlify.debug.use_snapback", false, false);
|
||||
|
||||
public static void printProperties() {
|
||||
|
@ -1,7 +1,6 @@
|
||||
package dev.isxander.controlify.driver;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||
import dev.isxander.controlify.debug.DebugProperties;
|
||||
import dev.isxander.controlify.hid.HIDDevice;
|
||||
import dev.isxander.controlify.utils.Log;
|
||||
@ -45,7 +44,6 @@ public record GamepadDrivers(BasicGamepadInputDriver basicGamepadInputDriver, Gy
|
||||
gyroDriver = sdl2Driver;
|
||||
rumbleDriver = sdl2Driver;
|
||||
batteryDriver = sdl2Driver;
|
||||
|
||||
// SDL2 bypasses XInput abstraction
|
||||
guidProviderDriver = sdl2Driver;
|
||||
}
|
||||
|
@ -1,27 +1,37 @@
|
||||
package dev.isxander.controlify.driver;
|
||||
|
||||
import com.sun.jna.Memory;
|
||||
import com.sun.jna.Pointer;
|
||||
import dev.isxander.controlify.controller.BatteryLevel;
|
||||
import dev.isxander.controlify.controller.gamepad.GamepadState;
|
||||
import dev.isxander.controlify.debug.DebugProperties;
|
||||
import dev.isxander.controlify.utils.Log;
|
||||
import io.github.libsdl4j.api.gamecontroller.SDL_GameController;
|
||||
import net.minecraft.util.Mth;
|
||||
import org.libsdl.SDL;
|
||||
|
||||
import static io.github.libsdl4j.api.error.SdlError.*;
|
||||
import static io.github.libsdl4j.api.gamecontroller.SDL_GameControllerAxis.*;
|
||||
import static io.github.libsdl4j.api.gamecontroller.SDL_GameControllerButton.*;
|
||||
import static io.github.libsdl4j.api.gamecontroller.SdlGamecontroller.*;
|
||||
import static io.github.libsdl4j.api.joystick.SDL_JoystickPowerLevel.*;
|
||||
import static io.github.libsdl4j.api.joystick.SdlJoystick.*;
|
||||
import static io.github.libsdl4j.api.sensor.SDL_SensorType.*;
|
||||
|
||||
public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, RumbleDriver, BatteryDriver, GUIDProvider {
|
||||
private final long ptrGamepad;
|
||||
private final SDL_GameController ptrGamepad;
|
||||
private BasicGamepadState state = BasicGamepadState.EMPTY;
|
||||
private GamepadState.GyroState gyroDelta = new GamepadState.GyroState(0, 0, 0);
|
||||
private final boolean isGyroSupported, isRumbleSupported;
|
||||
private final String guid;
|
||||
|
||||
public SDL2GamepadDriver(int jid) {
|
||||
this.ptrGamepad = SDL.SDL_GameControllerOpen(jid);
|
||||
this.guid = SDL.SDL_JoystickGUIDString(SDL.SDL_GameControllerGetJoystick(ptrGamepad));
|
||||
this.isGyroSupported = SDL.SDL_GameControllerHasSensor(ptrGamepad, SDL.SDL_SENSOR_GYRO);
|
||||
this.isRumbleSupported = SDL.SDL_GameControllerHasRumble(ptrGamepad);
|
||||
this.ptrGamepad = SDL_GameControllerOpen(jid);
|
||||
this.guid = SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(ptrGamepad)).toString();
|
||||
this.isGyroSupported = SDL_GameControllerHasSensor(ptrGamepad, SDL_SENSOR_GYRO);
|
||||
this.isRumbleSupported = SDL_GameControllerHasRumble(ptrGamepad);
|
||||
|
||||
if (this.isGyroSupported()) {
|
||||
SDL.SDL_GameControllerSetSensorEnabled(ptrGamepad, SDL.SDL_SENSOR_GYRO, true);
|
||||
SDL_GameControllerSetSensorEnabled(ptrGamepad, SDL_SENSOR_GYRO, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,45 +39,50 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
|
||||
public void update() {
|
||||
if (isGyroSupported()) {
|
||||
float[] gyro = new float[3];
|
||||
if (SDL.SDL_GameControllerGetSensorData(ptrGamepad, SDL.SDL_SENSOR_GYRO, gyro, 3) == 0) {
|
||||
gyroDelta = new GamepadState.GyroState(gyro[0], gyro[1], gyro[2]);
|
||||
if (DebugProperties.PRINT_GYRO) Log.LOGGER.info("Gyro delta: " + gyroDelta);
|
||||
} else {
|
||||
Log.LOGGER.error("Could not get gyro data: " + SDL.SDL_GetError());
|
||||
|
||||
try (Memory memory = new Memory(gyro.length * Float.BYTES)) {
|
||||
if (SDL_GameControllerGetSensorData(ptrGamepad, SDL_SENSOR_GYRO, memory, 3) == 0) {
|
||||
memory.read(0, gyro, 0, gyro.length);
|
||||
|
||||
gyroDelta = new GamepadState.GyroState(gyro[0], gyro[1], gyro[2]);
|
||||
if (DebugProperties.PRINT_GYRO) Log.LOGGER.info("Gyro delta: " + gyroDelta);
|
||||
} else {
|
||||
Log.LOGGER.error("Could not get gyro data: " + SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
SDL.SDL_GameControllerUpdate();
|
||||
SDL_GameControllerUpdate();
|
||||
|
||||
// Axis values are in the range [-32768, 32767] (short)
|
||||
// Triggers are in the range [0, 32767] (thanks SDL!)
|
||||
// https://wiki.libsdl.org/SDL2/SDL_GameControllerGetAxis
|
||||
GamepadState.AxesState axes = new GamepadState.AxesState(
|
||||
Mth.inverseLerp(SDL.SDL_GameControllerGetAxis(ptrGamepad, SDL.SDL_CONTROLLER_AXIS_LEFTX), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
|
||||
Mth.inverseLerp(SDL.SDL_GameControllerGetAxis(ptrGamepad, SDL.SDL_CONTROLLER_AXIS_LEFTY), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
|
||||
Mth.inverseLerp(SDL.SDL_GameControllerGetAxis(ptrGamepad, SDL.SDL_CONTROLLER_AXIS_RIGHTX), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
|
||||
Mth.inverseLerp(SDL.SDL_GameControllerGetAxis(ptrGamepad, SDL.SDL_CONTROLLER_AXIS_RIGHTY), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
|
||||
Mth.inverseLerp(SDL.SDL_GameControllerGetAxis(ptrGamepad, SDL.SDL_CONTROLLER_AXIS_TRIGGERLEFT), 0, Short.MAX_VALUE),
|
||||
Mth.inverseLerp(SDL.SDL_GameControllerGetAxis(ptrGamepad, SDL.SDL_CONTROLLER_AXIS_TRIGGERRIGHT), 0, Short.MAX_VALUE)
|
||||
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_LEFTX), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
|
||||
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_LEFTY), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
|
||||
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_RIGHTX), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
|
||||
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_RIGHTY), Short.MIN_VALUE, Short.MAX_VALUE) * 2f - 1f,
|
||||
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_TRIGGERLEFT), 0, Short.MAX_VALUE),
|
||||
Mth.inverseLerp(SDL_GameControllerGetAxis(ptrGamepad, SDL_CONTROLLER_AXIS_TRIGGERRIGHT), 0, Short.MAX_VALUE)
|
||||
);
|
||||
// Button values return 1 if pressed, 0 if not
|
||||
// https://wiki.libsdl.org/SDL2/SDL_GameControllerGetButton
|
||||
GamepadState.ButtonState buttons = new GamepadState.ButtonState(
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_A) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_B) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_X) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_Y) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_LEFTSHOULDER) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_BACK) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_START) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_GUIDE) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_DPAD_UP) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_DPAD_DOWN) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_DPAD_LEFT) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_DPAD_RIGHT) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_LEFTSTICK) == 1,
|
||||
SDL.SDL_GameControllerGetButton(ptrGamepad, SDL.SDL_CONTROLLER_BUTTON_RIGHTSTICK) == 1
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_A) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_B) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_X) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_Y) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_LEFTSHOULDER) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_BACK) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_START) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_GUIDE) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_DPAD_UP) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_DPAD_DOWN) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_DPAD_LEFT) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_LEFTSTICK) == 1,
|
||||
SDL_GameControllerGetButton(ptrGamepad, SDL_CONTROLLER_BUTTON_RIGHTSTICK) == 1
|
||||
);
|
||||
this.state = new BasicGamepadState(axes, buttons);
|
||||
}
|
||||
@ -80,8 +95,8 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
|
||||
@Override
|
||||
public boolean rumble(float strongMagnitude, float weakMagnitude) {
|
||||
// duration of 0 is infinite
|
||||
if (!SDL.SDL_GameControllerRumble(ptrGamepad, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 0)) {
|
||||
Log.LOGGER.error("Could not rumble controller: " + SDL.SDL_GetError());
|
||||
if (SDL_GameControllerRumble(ptrGamepad, (short)(strongMagnitude * 65535.0F), (short)(weakMagnitude * 65535.0F), 0) != 0) {
|
||||
Log.LOGGER.error("Could not rumble controller: " + SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -94,15 +109,16 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
|
||||
|
||||
@Override
|
||||
public BatteryLevel getBatteryLevel() {
|
||||
return switch (SDL.SDL_JoystickCurrentPowerLevel(ptrGamepad)) {
|
||||
case SDL.SDL_JOYSTICK_POWER_UNKNOWN -> BatteryLevel.UNKNOWN;
|
||||
case SDL.SDL_JOYSTICK_POWER_EMPTY -> BatteryLevel.EMPTY;
|
||||
case SDL.SDL_JOYSTICK_POWER_LOW -> BatteryLevel.LOW;
|
||||
case SDL.SDL_JOYSTICK_POWER_MEDIUM -> BatteryLevel.MEDIUM;
|
||||
case SDL.SDL_JOYSTICK_POWER_FULL -> BatteryLevel.FULL;
|
||||
case SDL.SDL_JOYSTICK_POWER_WIRED -> BatteryLevel.WIRED;
|
||||
case SDL.SDL_JOYSTICK_POWER_MAX -> BatteryLevel.MAX;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + SDL.SDL_JoystickCurrentPowerLevel(ptrGamepad));
|
||||
int powerLevel = SDL_JoystickCurrentPowerLevel(SDL_GameControllerGetJoystick(ptrGamepad));
|
||||
return switch (powerLevel) {
|
||||
case SDL_JOYSTICK_POWER_UNKNOWN -> BatteryLevel.UNKNOWN;
|
||||
case SDL_JOYSTICK_POWER_EMPTY -> BatteryLevel.EMPTY;
|
||||
case SDL_JOYSTICK_POWER_LOW -> BatteryLevel.LOW;
|
||||
case SDL_JOYSTICK_POWER_MEDIUM -> BatteryLevel.MEDIUM;
|
||||
case SDL_JOYSTICK_POWER_FULL -> BatteryLevel.FULL;
|
||||
case SDL_JOYSTICK_POWER_WIRED -> BatteryLevel.WIRED;
|
||||
case SDL_JOYSTICK_POWER_MAX -> BatteryLevel.MAX;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + powerLevel);
|
||||
};
|
||||
}
|
||||
|
||||
@ -123,7 +139,7 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
SDL.SDL_GameControllerClose(ptrGamepad);
|
||||
SDL_GameControllerClose(ptrGamepad);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,15 +1,18 @@
|
||||
package dev.isxander.controlify.driver;
|
||||
|
||||
import dev.isxander.controlify.utils.Log;
|
||||
import org.libsdl.SDL;
|
||||
import io.github.libsdl4j.api.joystick.SDL_Joystick;
|
||||
|
||||
import static io.github.libsdl4j.api.error.SdlError.*;
|
||||
import static io.github.libsdl4j.api.joystick.SdlJoystick.*;
|
||||
|
||||
public class SDL2JoystickDriver implements RumbleDriver {
|
||||
private final long ptrJoystick;
|
||||
private final SDL_Joystick ptrJoystick;
|
||||
private final boolean isRumbleSupported;
|
||||
|
||||
public SDL2JoystickDriver(int jid) {
|
||||
this.ptrJoystick = SDL.SDL_JoystickOpen(jid);
|
||||
this.isRumbleSupported = SDL.SDL_JoystickHasRumble(ptrJoystick);
|
||||
this.ptrJoystick = SDL_JoystickOpen(jid);
|
||||
this.isRumbleSupported = SDL_JoystickHasRumble(ptrJoystick);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -20,8 +23,8 @@ public class SDL2JoystickDriver implements RumbleDriver {
|
||||
@Override
|
||||
public boolean rumble(float strongMagnitude, float weakMagnitude) {
|
||||
// duration of 0 is infinite
|
||||
if (!SDL.SDL_JoystickRumble(ptrJoystick, (int)(strongMagnitude * 65535.0F), (int)(weakMagnitude * 65535.0F), 0)) {
|
||||
Log.LOGGER.error("Could not rumble controller: " + SDL.SDL_GetError());
|
||||
if (SDL_JoystickRumble(ptrJoystick, (short)(strongMagnitude * 65535.0F), (short)(weakMagnitude * 65535.0F), 0) != 0) {
|
||||
Log.LOGGER.error("Could not rumble controller: " + SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -39,6 +42,6 @@ public class SDL2JoystickDriver implements RumbleDriver {
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
SDL.SDL_JoystickClose(ptrJoystick);
|
||||
SDL_JoystickClose(ptrJoystick);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,232 @@
|
||||
package dev.isxander.controlify.driver;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.gui.screen.DownloadingSDLScreen;
|
||||
import dev.isxander.controlify.utils.DebugLog;
|
||||
import dev.isxander.controlify.utils.Log;
|
||||
import dev.isxander.controlify.utils.TrackingBodySubscriber;
|
||||
import dev.isxander.controlify.utils.TrackingConsumer;
|
||||
import io.github.libsdl4j.jna.SdlNativeLibraryLoader;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
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.nio.file.StandardOpenOption;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static io.github.libsdl4j.api.Sdl.*;
|
||||
import static io.github.libsdl4j.api.SdlSubSystemConst.*;
|
||||
import static io.github.libsdl4j.api.error.SdlError.*;
|
||||
import static io.github.libsdl4j.api.hints.SdlHints.*;
|
||||
import static io.github.libsdl4j.api.hints.SdlHintsConst.*;
|
||||
|
||||
public class SDL2NativesManager {
|
||||
private static final String SDL2_VERSION = "<SDL2_VERSION>";
|
||||
private static final Map<Target, NativeFileInfo> NATIVE_LIBRARIES = Map.of(
|
||||
new Target(Util.OS.WINDOWS, true, false), new NativeFileInfo("win32-x86-64", "windows64", "dll"),
|
||||
new Target(Util.OS.WINDOWS, false, false), new NativeFileInfo("win32-x86", "window32", "dll"),
|
||||
new Target(Util.OS.LINUX, true, false), new NativeFileInfo("linux-x86-64", "linux64", "so"),
|
||||
new Target(Util.OS.OSX, true, false), new NativeFileInfo("darwin-x86-64", "macos-x86_64", "dylib"),
|
||||
new Target(Util.OS.OSX, true, true), new NativeFileInfo("darwin-aarch64", "macos-aarch64", "dylib")
|
||||
);
|
||||
private static final String NATIVE_LIBRARY_URL = "https://maven.isxander.dev/releases/dev/isxander/libsdl4j-natives/%s/".formatted(SDL2_VERSION);
|
||||
private static final Path NATIVES_PATH = FabricLoader.getInstance().getGameDir().resolve("controlify-natives");
|
||||
|
||||
private static boolean loaded = false;
|
||||
private static boolean attemptedLoad = false;
|
||||
|
||||
private static CompletableFuture<Boolean> initFuture;
|
||||
|
||||
public static CompletableFuture<Boolean> maybeLoad() {
|
||||
if (initFuture != null)
|
||||
return initFuture;
|
||||
|
||||
if (!Controlify.instance().config().globalSettings().loadVibrationNatives)
|
||||
return initFuture = CompletableFuture.completedFuture(false);
|
||||
|
||||
if (attemptedLoad)
|
||||
return initFuture = CompletableFuture.completedFuture(loaded);
|
||||
|
||||
attemptedLoad = true;
|
||||
|
||||
Path localLibraryPath = NATIVES_PATH.resolve(Target.CURRENT.getArtifactName());
|
||||
|
||||
if (Files.exists(localLibraryPath)) {
|
||||
boolean success = loadAndStart(localLibraryPath);
|
||||
if (success)
|
||||
return initFuture = CompletableFuture.completedFuture(true);
|
||||
|
||||
Log.LOGGER.warn("Failed to load SDL2 from local file, attempting to re-download");
|
||||
}
|
||||
return initFuture = downloadAndStart(localLibraryPath);
|
||||
}
|
||||
|
||||
private static boolean loadAndStart(Path localLibraryPath) {
|
||||
try {
|
||||
SdlNativeLibraryLoader.loadLibSDL2FromFilePathNow(localLibraryPath.toAbsolutePath().toString());
|
||||
|
||||
startSDL2();
|
||||
|
||||
loaded = true;
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
Log.LOGGER.error("Failed to start SDL2", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void startSDL2() {
|
||||
// we have no windows, so all events are background events
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||
// accelerometer as joystick is not good UX. unexpected
|
||||
SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0");
|
||||
// see first hint
|
||||
SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1");
|
||||
// raw input requires controller correlation, which is impossible
|
||||
// without calling JoystickUpdate, which we don't do.
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0");
|
||||
// better rumble
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1");
|
||||
|
||||
// initialise SDL with just joystick and gamecontroller subsystems
|
||||
if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) != 0) {
|
||||
Log.LOGGER.error("Failed to initialise SDL2: " + SDL_GetError());
|
||||
throw new RuntimeException("Failed to initialise SDL2: " + SDL_GetError());
|
||||
}
|
||||
|
||||
DebugLog.log("Initialised SDL2");
|
||||
}
|
||||
|
||||
private static CompletableFuture<Boolean> downloadAndStart(Path localLibraryPath) {
|
||||
return downloadLibrary()
|
||||
.thenCompose(success -> {
|
||||
if (!success) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(loadAndStart(localLibraryPath));
|
||||
})
|
||||
.thenCompose(success -> Minecraft.getInstance().submit(() -> success));
|
||||
}
|
||||
|
||||
private static CompletableFuture<Boolean> downloadLibrary() {
|
||||
Path path = NATIVES_PATH.resolve(Target.CURRENT.getArtifactName());
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.createFile(path);
|
||||
} catch (Exception e) {
|
||||
Log.LOGGER.error("Failed to delete existing SDL2 native library file", e);
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
String url = NATIVE_LIBRARY_URL + Target.CURRENT.getArtifactName();
|
||||
|
||||
var httpClient = HttpClient.newHttpClient();
|
||||
var httpRequest = HttpRequest.newBuilder()
|
||||
.GET()
|
||||
.uri(URI.create(url))
|
||||
.build();
|
||||
|
||||
// send the request asynchronously and track the progress on the download
|
||||
AtomicReference<DownloadingSDLScreen> downloadScreen = new AtomicReference<>();
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
return httpClient.sendAsync(
|
||||
httpRequest,
|
||||
TrackingBodySubscriber.bodyHandler(
|
||||
HttpResponse.BodyHandlers.ofFileDownload(path.getParent(), StandardOpenOption.WRITE),
|
||||
new TrackingConsumer(
|
||||
total -> {
|
||||
DownloadingSDLScreen screen = new DownloadingSDLScreen(minecraft.screen, total, path);
|
||||
downloadScreen.set(screen);
|
||||
minecraft.execute(() -> minecraft.setScreen(screen));
|
||||
},
|
||||
(received, total) -> downloadScreen.get().updateDownloadProgress(received),
|
||||
error -> {
|
||||
if (error.isPresent()) {
|
||||
Log.LOGGER.error("Failed to download SDL2 native library", error.get());
|
||||
minecraft.execute(() -> downloadScreen.get().failDownload(error.get()));
|
||||
} else {
|
||||
Log.LOGGER.debug("Finished downloading SDL2 native library");
|
||||
minecraft.execute(() -> downloadScreen.get().finishDownload());
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
).handle((response, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Log.LOGGER.error("Failed to download SDL2 native library", throwable);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public static boolean hasAttemptedLoad() {
|
||||
return attemptedLoad;
|
||||
}
|
||||
|
||||
public record Target(Util.OS os, boolean is64Bit, boolean isARM) {
|
||||
public static final Target CURRENT = Util.make(() -> {
|
||||
Util.OS os = Util.getPlatform();
|
||||
|
||||
String arch = System.getProperty("os.arch");
|
||||
boolean is64bit = arch.contains("64");
|
||||
boolean isARM = arch.contains("arm") || arch.contains("aarch");
|
||||
|
||||
return new Target(os, is64bit, isARM);
|
||||
});
|
||||
|
||||
public boolean hasNativeLibrary() {
|
||||
return NATIVE_LIBRARIES.containsKey(this);
|
||||
}
|
||||
|
||||
public String getArtifactName() {
|
||||
NativeFileInfo file = NATIVE_LIBRARIES.get(this);
|
||||
return "libsdl4j-natives-" + SDL2_VERSION + "-" + file.downloadSuffix + "." + file.fileExtension;
|
||||
}
|
||||
|
||||
public boolean isMacArm() {
|
||||
return os == Util.OS.OSX && isARM;
|
||||
}
|
||||
}
|
||||
|
||||
public record NativeFileInfo(String folderName, String downloadSuffix, String fileExtension) {
|
||||
public Path getNativePath() {
|
||||
return getSearchPath()
|
||||
.resolve(folderName)
|
||||
.resolve("SDL2." + fileExtension);
|
||||
}
|
||||
|
||||
public Path getSearchPath() {
|
||||
return FabricLoader.getInstance().getGameDir()
|
||||
.resolve("controlify-natives")
|
||||
.resolve(SDL2_VERSION);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package dev.isxander.controlify.fixes.boatfix;
|
||||
|
||||
import dev.isxander.controlify.mixins.feature.patches.boatfix.BoatMixin;
|
||||
|
||||
/**
|
||||
* @see dev.isxander.controlify.mixins.feature.fixes.boatfix.BoatMixin
|
||||
* @see BoatMixin
|
||||
*/
|
||||
public interface AnalogBoatInput {
|
||||
void setAnalogInput(float forward, float right);
|
||||
|
@ -5,6 +5,8 @@ import dev.isxander.controlify.ControllerManager;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||
import dev.isxander.controlify.controller.gamepad.GamepadState;
|
||||
import dev.isxander.controlify.controller.joystick.JoystickController;
|
||||
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
@ -15,11 +17,17 @@ import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ControllerCalibrationScreen extends Screen {
|
||||
/**
|
||||
* Controller calibration screen does a few things:
|
||||
* <ul>
|
||||
* <li>Calculates deadzones</li>
|
||||
* <li>Does gyroscope calibration</li>
|
||||
* <li>Detects triggers on unmapped joysticks</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class ControllerCalibrationScreen extends Screen implements DontInteruptScreen {
|
||||
private static final int CALIBRATION_TIME = 100;
|
||||
private static final ResourceLocation GUI_BARS_LOCATION = new ResourceLocation("textures/gui/bars.png");
|
||||
|
||||
@ -33,7 +41,7 @@ public class ControllerCalibrationScreen extends Screen {
|
||||
protected boolean calibrating = false, calibrated = false;
|
||||
protected int calibrationTicks = 0;
|
||||
|
||||
private final Map<Integer, double[]> deadzoneCalibration = new HashMap<>();
|
||||
private final double[] axisData;
|
||||
private GamepadState.GyroState accumulatedGyroVelocity = new GamepadState.GyroState();
|
||||
|
||||
public ControllerCalibrationScreen(Controller<?, ?> controller, Screen parent) {
|
||||
@ -44,6 +52,7 @@ public class ControllerCalibrationScreen extends Screen {
|
||||
super(Component.translatable("controlify.calibration.title"));
|
||||
this.controller = controller;
|
||||
this.parent = parent;
|
||||
this.axisData = new double[controller.axisCount() * CALIBRATION_TIME];
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -125,17 +134,17 @@ public class ControllerCalibrationScreen extends Screen {
|
||||
|
||||
if (stateChanged()) {
|
||||
calibrationTicks = 0;
|
||||
deadzoneCalibration.clear();
|
||||
Arrays.fill(axisData, 0);
|
||||
accumulatedGyroVelocity = new GamepadState.GyroState();
|
||||
}
|
||||
|
||||
if (calibrationTicks < CALIBRATION_TIME) {
|
||||
processDeadzoneData(calibrationTicks);
|
||||
processAxisData(calibrationTicks);
|
||||
processGyroData();
|
||||
|
||||
calibrationTicks++;
|
||||
} else {
|
||||
applyDeadzones();
|
||||
calibrateAxis();
|
||||
generateGyroCalibration();
|
||||
|
||||
calibrating = false;
|
||||
@ -145,17 +154,16 @@ public class ControllerCalibrationScreen extends Screen {
|
||||
|
||||
controller.config().deadzonesCalibrated = true;
|
||||
controller.config().delayedCalibration = false;
|
||||
Controlify.instance().config().save();
|
||||
// no need to save because of setCurrentController
|
||||
|
||||
Controlify.instance().setCurrentController(controller);
|
||||
}
|
||||
}
|
||||
|
||||
private void processDeadzoneData(int tick) {
|
||||
private void processAxisData(int tick) {
|
||||
var axes = controller.state().rawAxes();
|
||||
|
||||
for (int i = 0; i < axes.size(); i++) {
|
||||
var axis = Math.abs(axes.get(i));
|
||||
deadzoneCalibration.computeIfAbsent(i, k -> new double[CALIBRATION_TIME])[tick] = axis;
|
||||
}
|
||||
System.arraycopy(axes.stream().mapToDouble(a -> a).toArray(), 0, axisData, tick * axes.size(), axes.size());
|
||||
}
|
||||
|
||||
private void processGyroData() {
|
||||
@ -164,11 +172,30 @@ public class ControllerCalibrationScreen extends Screen {
|
||||
}
|
||||
}
|
||||
|
||||
private void applyDeadzones() {
|
||||
deadzoneCalibration.forEach((i, data) -> {
|
||||
var max = Arrays.stream(data).max().orElseThrow();
|
||||
controller.config().setDeadzone(i, (float) max + 0.08f);
|
||||
});
|
||||
private void calibrateAxis() {
|
||||
int axisCount = controller.axisCount();
|
||||
for (int axis = 0; axis < axisCount; axis++) {
|
||||
boolean triggerAxis = true;
|
||||
float maxAbs = 0;
|
||||
|
||||
for (int tick = 0; tick < CALIBRATION_TIME; tick++) {
|
||||
float axisValue = (float) axisData[tick * axisCount + axis];
|
||||
|
||||
if (axisValue != -1) {
|
||||
triggerAxis = false;
|
||||
}
|
||||
|
||||
maxAbs = Math.max(maxAbs, Math.abs(axisValue));
|
||||
}
|
||||
|
||||
if (triggerAxis && controller instanceof JoystickController<?> joystick && joystick.mapping() instanceof UnmappedJoystickMapping mapping) {
|
||||
joystick.config().setDeadzone(axis, 0.0f);
|
||||
joystick.config().setTriggerAxis(axis, true);
|
||||
mapping.setTriggerAxes(axis, true);
|
||||
} else {
|
||||
controller.config().setDeadzone(axis, maxAbs + 0.08f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void generateGyroCalibration() {
|
||||
|
@ -7,7 +7,7 @@ import dev.isxander.controlify.api.buttonguide.ButtonGuideApi;
|
||||
import dev.isxander.controlify.api.buttonguide.ButtonGuidePredicate;
|
||||
import dev.isxander.controlify.api.buttonguide.ButtonRenderPosition;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||
import dev.isxander.controlify.driver.SDL2NativesManager;
|
||||
import dev.isxander.controlify.gui.components.FakePositionPlainTextButton;
|
||||
import dev.isxander.controlify.screenop.ScreenControllerEventListener;
|
||||
import dev.isxander.controlify.utils.Animator;
|
||||
@ -57,23 +57,12 @@ public class ControllerCarouselScreen extends Screen implements ScreenController
|
||||
public static Screen createConfigScreen(Screen parent) {
|
||||
var controlify = Controlify.instance();
|
||||
|
||||
if (!controlify.config().globalSettings().vibrationOnboarded) {
|
||||
return new SDLOnboardingScreen(() -> new ControllerCarouselScreen(parent), yes -> {
|
||||
if (yes) {
|
||||
SDL2NativesManager.initialise();
|
||||
|
||||
if (controlify.config().globalSettings().delegateSetup) {
|
||||
controlify.discoverControllers();
|
||||
controlify.config().globalSettings().delegateSetup = false;
|
||||
controlify.config().save();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (Controlify.instance().config().globalSettings().delegateSetup) {
|
||||
if (controlify.config().globalSettings().delegateSetup) {
|
||||
controlify.discoverControllers();
|
||||
controlify.config().globalSettings().delegateSetup = false;
|
||||
controlify.config().save();
|
||||
}
|
||||
|
||||
return new ControllerCarouselScreen(parent);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package dev.isxander.controlify.gui.screen;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.api.bind.ControllerBinding;
|
||||
import dev.isxander.controlify.bindings.BindContext;
|
||||
|
@ -0,0 +1,4 @@
|
||||
package dev.isxander.controlify.gui.screen;
|
||||
|
||||
public interface DontInteruptScreen {
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
package dev.isxander.controlify.gui.screen;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.driver.SDL2NativesManager;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.MultiLineTextWidget;
|
||||
import net.minecraft.client.gui.components.PlainTextButton;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.ClickEvent;
|
||||
import net.minecraft.network.chat.CommonComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.HoverEvent;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class DownloadingSDLScreen extends Screen implements DontInteruptScreen {
|
||||
private static final ResourceLocation GUI_BARS_LOCATION = new ResourceLocation("textures/gui/bars.png");
|
||||
|
||||
private final Screen screenOnFinish;
|
||||
private final Path nativePath;
|
||||
|
||||
private long receivedBytes;
|
||||
private final long totalBytes;
|
||||
private final DecimalFormat format = new DecimalFormat("0.00 MB");
|
||||
|
||||
public DownloadingSDLScreen(Screen screenOnFinish, long totalBytes, Path nativePath) {
|
||||
super(Component.translatable("controlify.downloading_sdl.title"));
|
||||
this.screenOnFinish = screenOnFinish;
|
||||
this.nativePath = nativePath;
|
||||
this.totalBytes = totalBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
Component filePathText = Component.literal(nativePath.getFileName().toString())
|
||||
.withStyle(ChatFormatting.BLUE);
|
||||
addRenderableWidget(new PlainTextButton(
|
||||
width / 2 - font.width(filePathText) / 2,
|
||||
(int) (30 + 9 * 2.5f + 40 + 5 * 2f + 10),
|
||||
font.width(filePathText),
|
||||
font.lineHeight,
|
||||
filePathText,
|
||||
btn -> Util.getPlatform().openFile(nativePath.toFile()),
|
||||
font
|
||||
));
|
||||
|
||||
addRenderableWidget(new MultiLineTextWidget(
|
||||
width / 2 - (width - 50) / 2,
|
||||
(int) (30 + 9 * 2.5f + 40 + 5 * 2f + 10 + 9*3),
|
||||
Component.translatable("controlify.downloading_sdl.info"),
|
||||
font
|
||||
).setMaxWidth(width - 20).setCentered(true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
|
||||
renderDirtBackground(graphics);
|
||||
|
||||
super.render(graphics, mouseX, mouseY, delta);
|
||||
|
||||
graphics.pose().pushPose();
|
||||
graphics.pose().translate(width / 2f - font.width(this.getTitle()) / 2f * 2.5f, 30, 0);
|
||||
graphics.pose().scale(2.5f, 2.5f, 1f);
|
||||
|
||||
graphics.drawString(font, this.getTitle(), 0, 0, -1);
|
||||
|
||||
graphics.pose().popPose();
|
||||
|
||||
drawProgressBar(graphics, width / 2, (int) (30 + 9 * 2.5f + 40), (float) ((double) receivedBytes / totalBytes));
|
||||
|
||||
String totalString = format.format(totalBytes / 1024f / 1024f);
|
||||
graphics.drawString(
|
||||
font,
|
||||
totalString,
|
||||
(int) (width / 2f + 182 * 2f / 2 - font.width(totalString)),
|
||||
(int) (30 + 9 * 2f + 40 + 5 * 2f + 4),
|
||||
11184810 // light gray
|
||||
);
|
||||
|
||||
String receivedString = format.format(receivedBytes / 1024f / 1024f);
|
||||
graphics.drawString(
|
||||
font,
|
||||
receivedString,
|
||||
(int) (width / 2f - 182 * 2f / 2),
|
||||
(int) (30 + 9 * 2f + 40 + 5 * 2f + 4),
|
||||
11184810 // light gray
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added() {
|
||||
CompletableFuture<Boolean> askNativesFuture = Controlify.instance().askNatives();
|
||||
if (askNativesFuture.isDone()) {
|
||||
minecraft.setScreen(screenOnFinish);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateDownloadProgress(long receivedBytes) {
|
||||
this.receivedBytes = receivedBytes;
|
||||
}
|
||||
|
||||
public void finishDownload() {
|
||||
minecraft.setScreen(screenOnFinish);
|
||||
}
|
||||
|
||||
public void failDownload(Throwable th) {
|
||||
finishDownload();
|
||||
}
|
||||
|
||||
private static void drawProgressBar(GuiGraphics graphics, int centerX, int y, float progress) {
|
||||
int x = (int) (centerX - 182 * 2f / 2);
|
||||
|
||||
graphics.pose().pushPose();
|
||||
graphics.pose().translate(x, y, 0);
|
||||
graphics.pose().scale(2f, 2f, 1f);
|
||||
|
||||
graphics.blit(GUI_BARS_LOCATION, 0, 0, 0, 30, 182, 5);
|
||||
graphics.blit(GUI_BARS_LOCATION, 0, 0, 0, 35, (int)(progress * 182), 5);
|
||||
|
||||
graphics.pose().popPose();
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package dev.isxander.controlify.gui.screen;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
public class ModConfigOpenerScreen extends Screen {
|
||||
private final Screen lastScreen;
|
||||
|
||||
public ModConfigOpenerScreen(Screen lastScreen) {
|
||||
super(Component.empty());
|
||||
this.lastScreen = lastScreen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added() {
|
||||
// need to make sure fabric api has registered all its events
|
||||
// because calling setScreen before this will cause fapi to freak
|
||||
// out that it has no remove event and crash the whole game lol
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
this.init(minecraft, minecraft.getWindow().getGuiScaledWidth(), minecraft.getWindow().getGuiScaledHeight());
|
||||
|
||||
Controlify.instance().askNatives()
|
||||
.whenComplete((result, error) ->
|
||||
minecraft.setScreen(ControllerCarouselScreen.createConfigScreen(lastScreen))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void triggerImmediateNarration(boolean useTranslationsCache) {
|
||||
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package dev.isxander.controlify.gui.screen;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||
import dev.isxander.controlify.driver.SDL2NativesManager;
|
||||
import it.unimi.dsi.fastutil.booleans.BooleanConsumer;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.Util;
|
||||
@ -12,7 +12,7 @@ import net.minecraft.network.chat.Component;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class SDLOnboardingScreen extends ConfirmScreen {
|
||||
public class SDLOnboardingScreen extends ConfirmScreen implements DontInteruptScreen {
|
||||
public SDLOnboardingScreen(Supplier<Screen> lastScreen, BooleanConsumer onAnswered) {
|
||||
super(
|
||||
yes -> {
|
||||
|
@ -22,7 +22,7 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class SubmitUnknownControllerScreen extends Screen {
|
||||
public class SubmitUnknownControllerScreen extends Screen implements DontInteruptScreen {
|
||||
public static final String SUBMISSION_URL = "https://api-controlify.isxander.dev/api/v1/submit";
|
||||
public static final Pattern NAME_PATTERN = Pattern.compile("^[\\w\\- ]{3,32}$");
|
||||
|
||||
|
@ -3,18 +3,19 @@ package dev.isxander.controlify.hid;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.controller.ControllerType;
|
||||
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
|
||||
import dev.isxander.controlify.driver.SDL2NativesManager;
|
||||
import dev.isxander.controlify.debug.DebugProperties;
|
||||
import dev.isxander.controlify.utils.Log;
|
||||
import dev.isxander.controlify.utils.ToastUtils;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.hid4java.*;
|
||||
import org.libsdl.SDL;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
|
||||
import static io.github.libsdl4j.api.joystick.SdlJoystick.*;
|
||||
|
||||
public class ControllerHIDService {
|
||||
private final HidServicesSpecification specification;
|
||||
private HidServices services;
|
||||
@ -155,8 +156,8 @@ public class ControllerHIDService {
|
||||
|
||||
private Optional<ControllerHIDInfo> fetchTypeFromSDL(int jid) {
|
||||
if (SDL2NativesManager.isLoaded()) {
|
||||
int vid = SDL.SDL_JoystickGetDeviceVendor(jid);
|
||||
int pid = SDL.SDL_JoystickGetDeviceProduct(jid);
|
||||
int vid = SDL_JoystickGetDeviceVendor(jid);
|
||||
int pid = SDL_JoystickGetDeviceProduct(jid);
|
||||
String path = GLFW.glfwGetJoystickGUID(jid);
|
||||
|
||||
if (vid != 0 && pid != 0) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package dev.isxander.controlify.mixins.feature.fixes.boatfix;
|
||||
package dev.isxander.controlify.mixins.feature.patches.boatfix;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import dev.isxander.controlify.fixes.boatfix.AnalogBoatInput;
|
@ -1,4 +1,4 @@
|
||||
package dev.isxander.controlify.mixins.feature.fixes.boatfix;
|
||||
package dev.isxander.controlify.mixins.feature.patches.boatfix;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
@ -1,6 +1,7 @@
|
||||
package dev.isxander.controlify.mixins.feature.settingsbutton;
|
||||
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.gui.screen.ControllerCarouselScreen;
|
||||
import net.minecraft.client.Options;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
@ -9,6 +10,7 @@ import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.controls.ControlsScreen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||
@ -22,7 +24,7 @@ public class ControlsScreenMixin extends OptionsSubScreen {
|
||||
|
||||
@Inject(method = "init", at = @At("RETURN"))
|
||||
private void addControllerSettings(CallbackInfo ci, @Local(ordinal = 0) int leftX, @Local(ordinal = 1) int rightX, @Local(ordinal = 2) int currentY) {
|
||||
addRenderableWidget(Button.builder(Component.translatable("controlify.gui.button"), btn -> minecraft.setScreen(ControllerCarouselScreen.createConfigScreen(this)))
|
||||
addRenderableWidget(Button.builder(Component.translatable("controlify.gui.button"), btn -> this.openControllerSettings())
|
||||
.pos(leftX, currentY)
|
||||
.width(150)
|
||||
.build());
|
||||
@ -32,4 +34,12 @@ public class ControlsScreenMixin extends OptionsSubScreen {
|
||||
private int modifyDoneButtonY(int y) {
|
||||
return y + 24;
|
||||
}
|
||||
|
||||
@Unique
|
||||
private void openControllerSettings() {
|
||||
Controlify.instance().askNatives()
|
||||
.whenComplete((result, error) ->
|
||||
minecraft.setScreen(ControllerCarouselScreen.createConfigScreen(lastScreen))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,32 @@
|
||||
package dev.isxander.controlify.utils;
|
||||
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controller.ControllerType;
|
||||
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||
import dev.isxander.controlify.hid.ControllerHIDService;
|
||||
import dev.isxander.controlify.hid.HIDDevice;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
import java.util.HexFormat;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ControllerUtils {
|
||||
public static String createControllerString(Controller<?, ?> controller) {
|
||||
Optional<HIDDevice> hid = controller.hidInfo().flatMap(ControllerHIDService.ControllerHIDInfo::hidDevice);
|
||||
HexFormat hexFormat = HexFormat.of().withPrefix("0x");
|
||||
|
||||
return String.format("'%s'#%s-%s (%s, %s: %s)",
|
||||
controller.name(),
|
||||
controller.joystickId(),
|
||||
controller instanceof GamepadController ? "gamepad" : "joy",
|
||||
hid.map(device -> hexFormat.toHexDigits(device.vendorID())).orElse("?"),
|
||||
hid.map(device -> hexFormat.toHexDigits(device.productID())).orElse("?"),
|
||||
controller.hidInfo().map(ControllerHIDService.ControllerHIDInfo::type)
|
||||
.orElse(ControllerType.UNKNOWN)
|
||||
.friendlyName()
|
||||
);
|
||||
}
|
||||
|
||||
public static float deadzone(float value, float deadzone) {
|
||||
return (value - Math.copySign(Math.min(deadzone, Math.abs(value)), value)) / (1 - deadzone);
|
||||
}
|
||||
|
@ -0,0 +1,60 @@
|
||||
package dev.isxander.controlify.utils;
|
||||
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.concurrent.Flow;
|
||||
|
||||
public class TrackingBodySubscriber<T> implements HttpResponse.BodySubscriber<T> {
|
||||
private final HttpResponse.BodySubscriber<T> delegate;
|
||||
private final TrackingConsumer consumer;
|
||||
|
||||
private long receivedBytes;
|
||||
private final long contentLengthIfKnown;
|
||||
|
||||
public TrackingBodySubscriber(HttpResponse.BodySubscriber<T> delegate, TrackingConsumer consumer, long contentLengthIfKnown) {
|
||||
this.delegate = delegate;
|
||||
this.consumer = consumer;
|
||||
this.contentLengthIfKnown = contentLengthIfKnown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletionStage<T> getBody() {
|
||||
return delegate.getBody();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Flow.Subscription subscription) {
|
||||
consumer.start().accept(contentLengthIfKnown);
|
||||
delegate.onSubscribe(subscription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<ByteBuffer> item) {
|
||||
receivedBytes += countBytes(item);
|
||||
delegate.onNext(item);
|
||||
consumer.progressConsumer().accept(receivedBytes, contentLengthIfKnown);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
consumer.onComplete().accept(Optional.of(throwable));
|
||||
delegate.onError(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
consumer.onComplete().accept(Optional.empty());
|
||||
delegate.onComplete();
|
||||
}
|
||||
|
||||
private long countBytes(List<ByteBuffer> buffers) {
|
||||
return buffers.stream().mapToLong(ByteBuffer::remaining).sum();
|
||||
}
|
||||
|
||||
public static <T> HttpResponse.BodyHandler<T> bodyHandler(HttpResponse.BodyHandler<T> delegate, TrackingConsumer consumer) {
|
||||
return (responseInfo) -> new TrackingBodySubscriber<>(delegate.apply(responseInfo), consumer, responseInfo.headers().firstValueAsLong("Content-Length").orElse(-1L));
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package dev.isxander.controlify.utils;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public record TrackingConsumer(Consumer<Long> start, BiConsumer<Long, Long> progressConsumer, Consumer<Optional<Throwable>> onComplete) {
|
||||
}
|
Reference in New Issue
Block a user