forked from Clones/Controlify
[BROKEN] Abstract controller manager system
This commit is contained in:
1 parent
9960fedad1
commit
b07066e097
39 files changed
+521
-276
No files matched your search
@@ -1,13 +1,14 @@
|
|||||||
package dev.isxander.controlify;
|
package dev.isxander.controlify;
|
||||||
|
|
||||||
import com.google.common.io.ByteStreams;
|
|
||||||
import com.mojang.blaze3d.Blaze3D;
|
import com.mojang.blaze3d.Blaze3D;
|
||||||
import com.sun.jna.Memory;
|
|
||||||
import dev.isxander.controlify.api.ControlifyApi;
|
import dev.isxander.controlify.api.ControlifyApi;
|
||||||
import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint;
|
import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint;
|
||||||
import dev.isxander.controlify.compatibility.ControlifyCompat;
|
import dev.isxander.controlify.compatibility.ControlifyCompat;
|
||||||
import dev.isxander.controlify.controller.joystick.JoystickController;
|
import dev.isxander.controlify.controller.joystick.JoystickController;
|
||||||
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
|
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
|
||||||
|
import dev.isxander.controlify.controllermanager.ControllerManager;
|
||||||
|
import dev.isxander.controlify.controllermanager.GLFWControllerManager;
|
||||||
|
import dev.isxander.controlify.controllermanager.SDLControllerManager;
|
||||||
import dev.isxander.controlify.gui.controllers.ControllerBindHandler;
|
import dev.isxander.controlify.gui.controllers.ControllerBindHandler;
|
||||||
import dev.isxander.controlify.gui.screen.*;
|
import dev.isxander.controlify.gui.screen.*;
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
@@ -26,7 +27,6 @@ import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor;
|
|||||||
import dev.isxander.controlify.utils.*;
|
import dev.isxander.controlify.utils.*;
|
||||||
import dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
|
import dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
|
||||||
import dev.isxander.controlify.wireless.LowBatteryNotifier;
|
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.ClientLifecycleEvents;
|
||||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||||
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||||
@@ -40,30 +40,28 @@ import net.minecraft.client.gui.screens.Screen;
|
|||||||
import net.minecraft.client.multiplayer.ServerData;
|
import net.minecraft.client.multiplayer.ServerData;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.server.packs.resources.Resource;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
import org.lwjgl.system.MemoryUtil;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
import static io.github.libsdl4j.api.gamecontroller.SdlGamecontroller.SDL_GameControllerAddMappingsFromRW;
|
import static dev.isxander.controlify.utils.ControllerUtils.wrapControllerError;
|
||||||
import static io.github.libsdl4j.api.gamecontroller.SdlGamecontroller.SDL_GameControllerNumMappings;
|
|
||||||
import static io.github.libsdl4j.api.rwops.SdlRWops.SDL_RWFromConstMem;
|
|
||||||
|
|
||||||
public class Controlify implements ControlifyApi {
|
public class Controlify implements ControlifyApi {
|
||||||
private static Controlify instance = null;
|
private static Controlify instance = null;
|
||||||
|
|
||||||
private final Minecraft minecraft = Minecraft.getInstance();
|
private final Minecraft minecraft = Minecraft.getInstance();
|
||||||
|
|
||||||
|
private ControllerManager controllerManager;
|
||||||
|
|
||||||
|
private boolean finishedInit = false;
|
||||||
|
private boolean probeMode = false;
|
||||||
|
|
||||||
private Controller<?, ?> currentController = null;
|
private Controller<?, ?> currentController = null;
|
||||||
private InGameInputHandler inGameInputHandler;
|
private InGameInputHandler inGameInputHandler;
|
||||||
public InGameButtonGuide inGameButtonGuide;
|
public InGameButtonGuide inGameButtonGuide;
|
||||||
@@ -155,18 +153,13 @@ public class Controlify implements ControlifyApi {
|
|||||||
|
|
||||||
config().load();
|
config().load();
|
||||||
|
|
||||||
// initialise and compatability modules that controlify implements itself
|
boolean controllersConnected = GLFWControllerManager.areControllersConnected();
|
||||||
// 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)
|
ControlifyEvents.CONTROLLER_CONNECTED.register(this::onControllerAdded);
|
||||||
.anyMatch(GLFW::glfwJoystickPresent);
|
ControlifyEvents.CONTROLLER_DISCONNECTED.register(this::onControllerRemoved);
|
||||||
if (controllersConnected) { // only initialise Controlify if controllers are detected
|
|
||||||
if (!config().globalSettings().delegateSetup) {
|
if (controllersConnected) {
|
||||||
// check native onboarding then discover controllers
|
if (config().globalSettings().quietMode) {
|
||||||
askNatives().whenComplete((loaded, th) -> discoverControllers());
|
|
||||||
} else {
|
|
||||||
// delegate setup: don't auto set up controllers, require the user to open config screen
|
|
||||||
ToastUtils.sendToast(
|
ToastUtils.sendToast(
|
||||||
Component.translatable("controlify.toast.setup_in_config.title"),
|
Component.translatable("controlify.toast.setup_in_config.title"),
|
||||||
Component.translatable(
|
Component.translatable(
|
||||||
@@ -177,33 +170,22 @@ public class Controlify implements ControlifyApi {
|
|||||||
),
|
),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
finishControlifyInit();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
probeMode = true;
|
||||||
|
ClientTickEvents.END_CLIENT_TICK.register(client -> this.probeTick());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initialise and compatability modules that controlify implements itself
|
||||||
|
// this does NOT invoke any entrypoints. this is done in the pre-initialisation phase
|
||||||
|
ControlifyCompat.init();
|
||||||
|
|
||||||
// register events
|
// register events
|
||||||
ClientTickEvents.START_CLIENT_TICK.register(this::tick);
|
|
||||||
ClientLifecycleEvents.CLIENT_STOPPING.register(minecraft -> {
|
ClientLifecycleEvents.CLIENT_STOPPING.register(minecraft -> {
|
||||||
controllerHIDService().stop();
|
controllerHIDService().stop();
|
||||||
});
|
});
|
||||||
ConnectServerEvent.EVENT.register((minecraft, address, data) -> {
|
|
||||||
notifyNewServer(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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
|
// sends toasts of new features
|
||||||
notifyOfNewFeatures();
|
notifyOfNewFeatures();
|
||||||
@@ -217,73 +199,26 @@ public class Controlify implements ControlifyApi {
|
|||||||
public void discoverControllers() {
|
public void discoverControllers() {
|
||||||
if (hasDiscoveredControllers) {
|
if (hasDiscoveredControllers) {
|
||||||
Log.LOGGER.warn("Attempted to discover controllers twice!");
|
Log.LOGGER.warn("Attempted to discover controllers twice!");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
hasDiscoveredControllers = true;
|
hasDiscoveredControllers = true;
|
||||||
|
|
||||||
DebugLog.log("Discovering and initializing controllers...");
|
DebugLog.log("Discovering and initializing controllers...");
|
||||||
|
|
||||||
// load gamepad mappings before every
|
controllerManager.discoverControllers();
|
||||||
minecraft.getResourceManager()
|
|
||||||
.getResource(Controlify.id("controllers/gamecontrollerdb.txt"))
|
|
||||||
.ifPresent(this::loadGamepadMappings);
|
|
||||||
|
|
||||||
// find already connected controllers
|
if (controllerManager.getConnectedControllers().isEmpty()) {
|
||||||
// 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.");
|
Log.LOGGER.info("No controllers found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no controller is currently selected, select the first one
|
// if no controller is currently selected, select the first one
|
||||||
if (getCurrentController().isEmpty()) {
|
if (getCurrentController().isEmpty()) {
|
||||||
var controller = ControllerManager.getConnectedControllers().stream().findFirst().orElse(null);
|
Controller<?, ?> controller = controllerManager.getConnectedControllers().stream().findFirst().orElse(null);
|
||||||
if (controller != null && (controller.config().delayedCalibration || !controller.config().deadzonesCalibrated)) {
|
if (controller != null && (controller.config().delayedCalibration || !controller.config().deadzonesCalibrated)) {
|
||||||
controller = null;
|
controller = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setCurrentController(controller);
|
this.setCurrentController(controller, false);
|
||||||
} else {
|
} else {
|
||||||
// setCurrentController saves config so there is no need to set dirty to save
|
// setCurrentController saves config so there is no need to set dirty to save
|
||||||
config().saveIfDirty();
|
config().saveIfDirty();
|
||||||
@@ -298,51 +233,57 @@ public class Controlify implements ControlifyApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public CompletableFuture<Void> finishControlifyInit() {
|
||||||
* Called when a controller has been connected after mod initialisation.
|
if (finishedInit) {
|
||||||
* If this is the first controller to be connected in the game's lifecycle,
|
return CompletableFuture.completedFuture(null);
|
||||||
* 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.
|
finishedInit = true;
|
||||||
*/
|
|
||||||
private void onControllerHotplugged(int jid) {
|
askNatives().whenComplete((loaded, th) -> {
|
||||||
if (!hasDiscoveredControllers) {
|
Log.LOGGER.info("Finishing Controlify init...");
|
||||||
|
|
||||||
|
controllerManager = loaded ? new SDLControllerManager() : new GLFWControllerManager();
|
||||||
|
|
||||||
|
ClientTickEvents.START_CLIENT_TICK.register(this::tick);
|
||||||
|
ConnectServerEvent.EVENT.register((minecraft, address, data) -> {
|
||||||
|
notifyNewServer(data);
|
||||||
|
});
|
||||||
discoverControllers();
|
discoverControllers();
|
||||||
return;
|
});
|
||||||
|
|
||||||
|
return askNatives().thenApply(loaded -> null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var controllerOpt = ControllerManager.createOrGet(jid, controllerHIDService.fetchType(jid));
|
private void onControllerAdded(Controller<?, ?> controller, boolean hotplugged, boolean newController) {
|
||||||
if (controllerOpt.isEmpty()) return;
|
|
||||||
var controller = controllerOpt.get();
|
|
||||||
|
|
||||||
Log.LOGGER.info("Controller connected: " + ControllerUtils.createControllerString(controller));
|
|
||||||
|
|
||||||
config().loadOrCreateControllerData(controller);
|
|
||||||
|
|
||||||
if (SubmitUnknownControllerScreen.canSubmit(controller)) {
|
if (SubmitUnknownControllerScreen.canSubmit(controller)) {
|
||||||
minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen));
|
minecraft.setScreen(new SubmitUnknownControllerScreen(controller, minecraft.screen));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config().globalSettings().delegateSetup) {
|
if (controller.config().allowVibrations && !SDL2NativesManager.isLoaded()) {
|
||||||
config().globalSettings().delegateSetup = false;
|
|
||||||
config().setDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
|
|
||||||
controller.config().allowVibrations = false;
|
controller.config().allowVibrations = false;
|
||||||
config().setDirty();
|
config().setDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ControllerManager.getConnectedControllers().size() == 1 && (controller.config().deadzonesCalibrated || controller.config().delayedCalibration)) {
|
if (hotplugged) {
|
||||||
this.setCurrentController(controller);
|
if (controller.config().deadzonesCalibrated) {
|
||||||
|
setCurrentController(controller, hotplugged);
|
||||||
|
} else {
|
||||||
|
calibrationQueue.add(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller instanceof JoystickController<?> joystick && joystick.mapping() instanceof UnmappedJoystickMapping) {
|
||||||
ToastUtils.sendToast(
|
ToastUtils.sendToast(
|
||||||
Component.translatable("controlify.toast.default_controller_connected.title"),
|
Component.translatable("controlify.toast.unmapped_joystick.title"),
|
||||||
Component.translatable("controlify.toast.default_controller_connected.description"),
|
Component.translatable("controlify.toast.unmapped_joystick.description", controller.name()),
|
||||||
false
|
true
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.askToSwitchController(controller);
|
ToastUtils.sendToast(
|
||||||
config().saveIfDirty();
|
Component.translatable("controlify.toast.controller_connected.title"),
|
||||||
|
Component.translatable("controlify.toast.controller_connected.description", controller.name()),
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
|
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
|
||||||
@@ -350,32 +291,25 @@ public class Controlify implements ControlifyApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void onControllerRemoved(Controller<?, ?> controller) {
|
||||||
* Called when a controller is disconnected.
|
this.setCurrentController(
|
||||||
* Equips another controller if available.
|
controllerManager.getConnectedControllers()
|
||||||
*
|
.stream()
|
||||||
* @param jid the joystick id of the disconnected controller
|
.findFirst()
|
||||||
*/
|
.orElse(null),
|
||||||
private void onControllerDisconnect(int jid) {
|
true);
|
||||||
ControllerManager.getConnectedControllers().stream().filter(controller -> controller.joystickId() == jid).findAny().ifPresent(controller -> {
|
|
||||||
ControllerManager.disconnect(controller);
|
|
||||||
|
|
||||||
controller.hidInfo().ifPresent(controllerHIDService::unconsumeController);
|
this.setInputMode(
|
||||||
|
getCurrentController().isEmpty()
|
||||||
setCurrentController(ControllerManager.getConnectedControllers().stream().findFirst().orElse(null));
|
? InputMode.KEYBOARD_MOUSE
|
||||||
Log.LOGGER.info("Controller disconnected: " + controller.name());
|
: InputMode.CONTROLLER
|
||||||
this.setInputMode(currentController == null ? InputMode.KEYBOARD_MOUSE : InputMode.CONTROLLER);
|
);
|
||||||
|
|
||||||
ToastUtils.sendToast(
|
ToastUtils.sendToast(
|
||||||
Component.translatable("controlify.toast.controller_disconnected.title"),
|
Component.translatable("controlify.toast.controller_disconnected.title"),
|
||||||
Component.translatable("controlify.toast.controller_disconnected.description", controller.name()),
|
Component.translatable("controlify.toast.controller_disconnected.description", controller.name()),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
|
|
||||||
controllerListScreen.refreshControllers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -422,36 +356,6 @@ public class Controlify implements ControlifyApi {
|
|||||||
return nativeOnboardingFuture;
|
return nativeOnboardingFuture;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the gamepad mappings for both GLFW and SDL2.
|
|
||||||
* @param resource the already located `gamecontrollerdb.txt` resource
|
|
||||||
*/
|
|
||||||
private void loadGamepadMappings(Resource resource) {
|
|
||||||
Log.LOGGER.debug("Loading gamepad mappings...");
|
|
||||||
|
|
||||||
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!");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SDL2NativesManager.isLoaded()) {
|
|
||||||
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 (Throwable e) {
|
|
||||||
Log.LOGGER.error("Failed to load gamecontrollerdb.txt", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main loop of Controlify.
|
* The main loop of Controlify.
|
||||||
* In Controlify's current state, only the current controller is ticked.
|
* In Controlify's current state, only the current controller is ticked.
|
||||||
@@ -469,18 +373,12 @@ public class Controlify implements ControlifyApi {
|
|||||||
|
|
||||||
boolean outOfFocus = !config().globalSettings().outOfFocusInput && !client.isWindowActive();
|
boolean outOfFocus = !config().globalSettings().outOfFocusInput && !client.isWindowActive();
|
||||||
|
|
||||||
for (var controller : ControllerManager.getConnectedControllers()) {
|
controllerManager.tick(outOfFocus);
|
||||||
if (!outOfFocus)
|
|
||||||
wrapControllerError(controller::updateState, "Updating controller state", controller);
|
|
||||||
else
|
|
||||||
wrapControllerError(controller::clearState, "Clearing controller state", controller);
|
|
||||||
ControlifyEvents.CONTROLLER_STATE_UPDATE.invoker().onControllerStateUpdate(controller);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) {
|
if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) {
|
||||||
if (switchableController.state().hasAnyInput()) {
|
if (switchableController.state().hasAnyInput()) {
|
||||||
switchableController.clearState();
|
switchableController.clearState();
|
||||||
this.setCurrentController(switchableController); // setCurrentController sets switchableController to null
|
this.setCurrentController(switchableController, true); // setCurrentController sets switchableController to null
|
||||||
if (askSwitchToast != null) {
|
if (askSwitchToast != null) {
|
||||||
askSwitchToast.remove();
|
askSwitchToast.remove();
|
||||||
askSwitchToast = null;
|
askSwitchToast = null;
|
||||||
@@ -533,7 +431,7 @@ public class Controlify implements ControlifyApi {
|
|||||||
Component.translatable("controlify.toast.faulty_input.description"),
|
Component.translatable("controlify.toast.faulty_input.description"),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
this.setCurrentController(null);
|
this.setCurrentController(null, true);
|
||||||
consecutiveInputSwitches = 0;
|
consecutiveInputSwitches = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -549,16 +447,12 @@ public class Controlify implements ControlifyApi {
|
|||||||
ControlifyEvents.ACTIVE_CONTROLLER_TICKED.invoker().onControllerStateUpdate(controller);
|
ControlifyEvents.ACTIVE_CONTROLLER_TICKED.invoker().onControllerStateUpdate(controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void wrapControllerError(Runnable runnable, String errorTitle, Controller<?, ?> controller) {
|
private void probeTick() {
|
||||||
try {
|
if (probeMode) {
|
||||||
runnable.run();
|
if (GLFWControllerManager.areControllersConnected()) {
|
||||||
} catch (Throwable e) {
|
probeMode = false;
|
||||||
CrashReport crashReport = CrashReport.forThrowable(e, errorTitle);
|
minecraft.execute(this::finishControlifyInit);
|
||||||
CrashReportCategory category = crashReport.addCategory("Affected controller");
|
}
|
||||||
category.setDetail("Controller name", controller.name());
|
|
||||||
category.setDetail("Controller identification", controller.type().toString());
|
|
||||||
category.setDetail("Controller type", controller.getClass().getCanonicalName());
|
|
||||||
throw new ReportedException(crashReport);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,17 +460,6 @@ public class Controlify implements ControlifyApi {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void askToSwitchController(Controller<?, ?> controller) {
|
|
||||||
this.switchableController = controller;
|
|
||||||
this.askSwitchTime = Blaze3D.getTime();
|
|
||||||
|
|
||||||
askSwitchToast = ToastUtils.sendToast(
|
|
||||||
Component.translatable("controlify.toast.ask_to_switch.title"),
|
|
||||||
Component.translatable("controlify.toast.ask_to_switch.description", controller.name()),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public @NotNull Controller<?, ?> currentController() {
|
public @NotNull Controller<?, ?> currentController() {
|
||||||
@@ -591,7 +474,7 @@ public class Controlify implements ControlifyApi {
|
|||||||
return Optional.ofNullable(currentController);
|
return Optional.ofNullable(currentController);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCurrentController(@Nullable Controller<?, ?> controller) {
|
public void setCurrentController(@Nullable Controller<?, ?> controller, boolean changeInputMode) {
|
||||||
if (this.currentController == controller) return;
|
if (this.currentController == controller) return;
|
||||||
|
|
||||||
this.currentController = controller;
|
this.currentController = controller;
|
||||||
@@ -617,10 +500,14 @@ public class Controlify implements ControlifyApi {
|
|||||||
|
|
||||||
this.inGameInputHandler = new InGameInputHandler(controller);
|
this.inGameInputHandler = new InGameInputHandler(controller);
|
||||||
|
|
||||||
setInputMode(controller.config().mixedInput ? InputMode.MIXED : InputMode.CONTROLLER);
|
if (controller.config().mixedInput)
|
||||||
|
setInputMode(InputMode.MIXED);
|
||||||
|
else if (changeInputMode)
|
||||||
|
setInputMode(InputMode.CONTROLLER);
|
||||||
|
}
|
||||||
|
|
||||||
if (!controller.config().deadzonesCalibrated)
|
public Optional<ControllerManager> getControllerManager() {
|
||||||
calibrationQueue.add(controller);
|
return Optional.ofNullable(controllerManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<InGameInputHandler> inGameInputHandler() {
|
public Optional<InGameInputHandler> inGameInputHandler() {
|
||||||
|
|||||||
@@ -5,10 +5,23 @@ import dev.isxander.controlify.api.ingameinput.LookInputModifier;
|
|||||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
import dev.isxander.controlify.bindings.ControllerBindings;
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
import dev.isxander.controlify.api.ingameguide.IngameGuideRegistry;
|
import dev.isxander.controlify.api.ingameguide.IngameGuideRegistry;
|
||||||
|
import dev.isxander.controlify.controllermanager.ControllerManager;
|
||||||
import net.fabricmc.fabric.api.event.Event;
|
import net.fabricmc.fabric.api.event.Event;
|
||||||
import net.fabricmc.fabric.api.event.EventFactory;
|
import net.fabricmc.fabric.api.event.EventFactory;
|
||||||
|
|
||||||
public final class ControlifyEvents {
|
public final class ControlifyEvents {
|
||||||
|
public static final Event<ControllerConnected> CONTROLLER_CONNECTED = EventFactory.createArrayBacked(ControllerConnected.class, callbacks -> (controller, hotplugged, newController) -> {
|
||||||
|
for (ControllerConnected callback : callbacks) {
|
||||||
|
callback.onControllerConnected(controller, hotplugged, newController);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
public static final Event<ControllerDisconnected> CONTROLLER_DISCONNECTED = EventFactory.createArrayBacked(ControllerDisconnected.class, callbacks -> controller -> {
|
||||||
|
for (ControllerDisconnected callback : callbacks) {
|
||||||
|
callback.onControllerDisconnected(controller);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers when the input mode is changed from keyboard to controller or vice versa.
|
* Triggers when the input mode is changed from keyboard to controller or vice versa.
|
||||||
*/
|
*/
|
||||||
@@ -82,6 +95,16 @@ public final class ControlifyEvents {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ControllerConnected {
|
||||||
|
void onControllerConnected(Controller<?, ?> controller, boolean hotplugged, boolean newController);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ControllerDisconnected {
|
||||||
|
void onControllerDisconnected(Controller<?, ?> controller);
|
||||||
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface InputModeChanged {
|
public interface InputModeChanged {
|
||||||
void onInputModeChanged(InputMode mode);
|
void onInputModeChanged(InputMode mode);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package dev.isxander.controlify.config;
|
|||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
import dev.isxander.controlify.Controlify;
|
import dev.isxander.controlify.Controlify;
|
||||||
import dev.isxander.controlify.ControllerManager;
|
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
import dev.isxander.controlify.controller.joystick.CompoundJoystickInfo;
|
import dev.isxander.controlify.controller.joystick.CompoundJoystickInfo;
|
||||||
import dev.isxander.controlify.utils.DebugLog;
|
import dev.isxander.controlify.utils.DebugLog;
|
||||||
@@ -96,10 +95,13 @@ public class ControlifyConfig {
|
|||||||
|
|
||||||
JsonObject newControllerData = controllerData.deepCopy(); // we use the old config, so we don't lose disconnected controller data
|
JsonObject newControllerData = controllerData.deepCopy(); // we use the old config, so we don't lose disconnected controller data
|
||||||
|
|
||||||
for (var controller : ControllerManager.getConnectedControllers()) {
|
controlify.getControllerManager().ifPresent(controllerManager -> {
|
||||||
|
for (Controller<?, ?> controller : controllerManager.getConnectedControllers()) {
|
||||||
// `add` replaces if already existing
|
// `add` replaces if already existing
|
||||||
newControllerData.add(controller.uid(), generateControllerConfig(controller));
|
newControllerData.add(controller.uid(), generateControllerConfig(controller));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
controllerData = newControllerData;
|
controllerData = newControllerData;
|
||||||
config.addProperty("current_controller", currentControllerUid = controlify.getCurrentController().map(Controller::uid).orElse(null));
|
config.addProperty("current_controller", currentControllerUid = controlify.getCurrentController().map(Controller::uid).orElse(null));
|
||||||
@@ -137,9 +139,11 @@ public class ControlifyConfig {
|
|||||||
JsonObject controllers = object.getAsJsonObject("controllers");
|
JsonObject controllers = object.getAsJsonObject("controllers");
|
||||||
if (controllers != null) {
|
if (controllers != null) {
|
||||||
this.controllerData = controllers;
|
this.controllerData = controllers;
|
||||||
for (var controller : ControllerManager.getConnectedControllers()) {
|
if (controlify.getControllerManager().isPresent()) {
|
||||||
|
for (var controller : controlify.getControllerManager().get().getConnectedControllers()) {
|
||||||
loadOrCreateControllerData(controller);
|
loadOrCreateControllerData(controller);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setDirty();
|
setDirty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class GlobalSettings {
|
|||||||
public boolean allowServerRumble = true;
|
public boolean allowServerRumble = true;
|
||||||
public boolean uiSounds = false;
|
public boolean uiSounds = false;
|
||||||
public boolean notifyLowBattery = true;
|
public boolean notifyLowBattery = true;
|
||||||
public boolean delegateSetup = false;
|
public boolean quietMode = false;
|
||||||
public float ingameButtonGuideScale = 1f;
|
public float ingameButtonGuideScale = 1f;
|
||||||
|
|
||||||
public Set<String> seenServers = new HashSet<>();
|
public Set<String> seenServers = new HashSet<>();
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.google.common.reflect.TypeToken;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import dev.isxander.controlify.Controlify;
|
import dev.isxander.controlify.Controlify;
|
||||||
import dev.isxander.controlify.ControllerManager;
|
|
||||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
import dev.isxander.controlify.bindings.ControllerBindings;
|
||||||
import dev.isxander.controlify.hid.ControllerHIDService;
|
import dev.isxander.controlify.hid.ControllerHIDService;
|
||||||
import dev.isxander.controlify.rumble.RumbleCapable;
|
import dev.isxander.controlify.rumble.RumbleCapable;
|
||||||
@@ -62,7 +61,7 @@ public abstract class AbstractController<S extends ControllerState, C extends Co
|
|||||||
protected void setName(String name) {
|
protected void setName(String name) {
|
||||||
String uniqueName = name;
|
String uniqueName = name;
|
||||||
int i = 1;
|
int i = 1;
|
||||||
while (ControllerManager.getConnectedControllers().stream().map(Controller::name).anyMatch(uniqueName::equalsIgnoreCase)) {
|
while (Controlify.instance().getControllerManager().orElseThrow().getConnectedControllers().stream().map(Controller::name).anyMatch(uniqueName::equalsIgnoreCase)) {
|
||||||
uniqueName = name + " (" + i++ + ")";
|
uniqueName = name + " (" + i++ + ")";
|
||||||
if (i > 1000) throw new IllegalStateException("Could not find a unique name for controller " + name + " (" + uid() + ")! (tried " + i + " times)");
|
if (i > 1000) throw new IllegalStateException("Could not find a unique name for controller " + name + " (" + uid() + ")! (tried " + i + " times)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
|||||||
uniqueDrivers.forEach(Driver::update);
|
uniqueDrivers.forEach(Driver::update);
|
||||||
|
|
||||||
BasicGamepadInputDriver.BasicGamepadState basicState = drivers.basicGamepadInputDriver().getBasicGamepadState();
|
BasicGamepadInputDriver.BasicGamepadState basicState = drivers.basicGamepadInputDriver().getBasicGamepadState();
|
||||||
|
|
||||||
|
if (DebugProperties.PRINT_GAMEPAD_STATE) {
|
||||||
|
Log.LOGGER.info(basicState.toString());
|
||||||
|
}
|
||||||
|
|
||||||
GamepadState.AxesState deadzoneAxesState = basicState.axes()
|
GamepadState.AxesState deadzoneAxesState = basicState.axes()
|
||||||
.leftJoystickDeadZone(config().getLeftStickDeadzone())
|
.leftJoystickDeadZone(config().getLeftStickDeadzone())
|
||||||
.rightJoystickDeadZone(config().getRightStickDeadzone());
|
.rightJoystickDeadZone(config().getRightStickDeadzone());
|
||||||
@@ -99,11 +104,6 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
|||||||
this.absoluteGyro.add(gyroState);
|
this.absoluteGyro.add(gyroState);
|
||||||
|
|
||||||
state = new GamepadState(deadzoneAxesState, basicState.axes(), basicState.buttons(), gyroState, absoluteGyro);
|
state = new GamepadState(deadzoneAxesState, basicState.axes(), basicState.buttons(), gyroState, absoluteGyro);
|
||||||
|
|
||||||
if (DebugProperties.PRINT_TRIGGER_STATE) {
|
|
||||||
Log.LOGGER.info("Left Trigger: " + state.gamepadAxes().leftTrigger());
|
|
||||||
Log.LOGGER.info("Right Trigger: " + state.gamepadAxes().rightTrigger());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GamepadState.GyroState absoluteGyroState() {
|
public GamepadState.GyroState absoluteGyroState() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package dev.isxander.controlify.controller.joystick;
|
package dev.isxander.controlify.controller.joystick;
|
||||||
|
|
||||||
import dev.isxander.controlify.ControllerManager;
|
import dev.isxander.controlify.Controlify;
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
import dev.isxander.controlify.controller.ControllerType;
|
import dev.isxander.controlify.controller.ControllerType;
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ public record CompoundJoystickInfo(Collection<String> joystickUids, String frien
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean canBeUsed() {
|
public boolean canBeUsed() {
|
||||||
List<Controller<?, ?>> joysticks = ControllerManager.getConnectedControllers().stream().filter(c -> joystickUids.contains(c.uid())).toList();
|
List<Controller<?, ?>> joysticks = Controlify.instance().getControllerManager().orElseThrow().getConnectedControllers().stream().filter(c -> joystickUids.contains(c.uid())).toList();
|
||||||
if (joysticks.size() != joystickUids().size()) {
|
if (joysticks.size() != joystickUids().size()) {
|
||||||
return false; // not all controllers are connected
|
return false; // not all controllers are connected
|
||||||
}
|
}
|
||||||
@@ -26,13 +26,13 @@ public record CompoundJoystickInfo(Collection<String> joystickUids, String frien
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLoaded() {
|
public boolean isLoaded() {
|
||||||
return ControllerManager.isControllerConnected(createUID(joystickUids));
|
return Controlify.instance().getControllerManager().orElseThrow().isControllerConnected(createUID(joystickUids));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<CompoundJoystickController> attemptCreate() {
|
public Optional<CompoundJoystickController> attemptCreate() {
|
||||||
if (!canBeUsed()) return Optional.empty();
|
if (!canBeUsed()) return Optional.empty();
|
||||||
|
|
||||||
List<Integer> joystickIDs = ControllerManager.getConnectedControllers().stream()
|
List<Integer> joystickIDs = Controlify.instance().getControllerManager().orElseThrow().getConnectedControllers().stream()
|
||||||
.filter(c -> joystickUids.contains(c.uid()))
|
.filter(c -> joystickUids.contains(c.uid()))
|
||||||
.map(Controller::joystickId)
|
.map(Controller::joystickId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
+74
-17
@@ -1,33 +1,46 @@
|
|||||||
package dev.isxander.controlify;
|
package dev.isxander.controlify.controllermanager;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import dev.isxander.controlify.Controlify;
|
||||||
|
import dev.isxander.controlify.api.event.ControlifyEvents;
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||||
import dev.isxander.controlify.hid.ControllerHIDService;
|
|
||||||
import dev.isxander.controlify.controller.joystick.CompoundJoystickController;
|
import dev.isxander.controlify.controller.joystick.CompoundJoystickController;
|
||||||
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
|
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
|
||||||
import dev.isxander.controlify.debug.DebugProperties;
|
import dev.isxander.controlify.debug.DebugProperties;
|
||||||
|
import dev.isxander.controlify.hid.ControllerHIDService;
|
||||||
import dev.isxander.controlify.hid.HIDDevice;
|
import dev.isxander.controlify.hid.HIDDevice;
|
||||||
|
import dev.isxander.controlify.utils.ControllerUtils;
|
||||||
import dev.isxander.controlify.utils.DebugLog;
|
import dev.isxander.controlify.utils.DebugLog;
|
||||||
import dev.isxander.controlify.utils.Log;
|
import dev.isxander.controlify.utils.Log;
|
||||||
import net.minecraft.CrashReport;
|
import net.minecraft.CrashReport;
|
||||||
import net.minecraft.CrashReportCategory;
|
import net.minecraft.CrashReportCategory;
|
||||||
import net.minecraft.ReportedException;
|
import net.minecraft.ReportedException;
|
||||||
import org.hid4java.HidDevice;
|
import net.minecraft.client.Minecraft;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import net.minecraft.server.packs.resources.Resource;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public final class ControllerManager {
|
import static dev.isxander.controlify.utils.ControllerUtils.wrapControllerError;
|
||||||
private ControllerManager() {
|
|
||||||
|
public abstract class AbstractControllerManager implements ControllerManager {
|
||||||
|
protected final Controlify controlify;
|
||||||
|
protected final Minecraft minecraft;
|
||||||
|
private final Map<String, Controller<?, ?>> CONTROLLERS = new HashMap<>();
|
||||||
|
|
||||||
|
public AbstractControllerManager() {
|
||||||
|
this.controlify = Controlify.instance();
|
||||||
|
this.minecraft = Minecraft.getInstance();
|
||||||
|
|
||||||
|
minecraft.getResourceManager()
|
||||||
|
.getResource(Controlify.id("controllers/gamecontrollerdb.txt"))
|
||||||
|
.ifPresent(this::loadGamepadMappings);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static Map<String, Controller<?, ?>> CONTROLLERS = new HashMap<>();
|
public Optional<Controller<?, ?>> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
|
||||||
|
|
||||||
public static Optional<Controller<?, ?>> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
|
|
||||||
try {
|
try {
|
||||||
Optional<String> uid = hidInfo.createControllerUID();
|
Optional<String> uid = hidInfo.createControllerUID();
|
||||||
if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) {
|
if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) {
|
||||||
@@ -39,7 +52,7 @@ public final class ControllerManager {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GLFW.glfwJoystickIsGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK && !hidInfo.type().forceJoystick()) {
|
if (this.isControllerGamepad(joystickId) && !DebugProperties.FORCE_JOYSTICK && !hidInfo.type().forceJoystick()) {
|
||||||
GamepadController controller = new GamepadController(joystickId, hidInfo);
|
GamepadController controller = new GamepadController(joystickId, hidInfo);
|
||||||
CONTROLLERS.put(controller.uid(), controller);
|
CONTROLLERS.put(controller.uid(), controller);
|
||||||
checkCompoundJoysticks();
|
checkCompoundJoysticks();
|
||||||
@@ -57,19 +70,52 @@ public final class ControllerManager {
|
|||||||
category.setDetail("Controller identification", hidInfo.type());
|
category.setDetail("Controller identification", hidInfo.type());
|
||||||
category.setDetail("HID path", hidInfo.hidDevice().map(HIDDevice::path).orElse("N/A"));
|
category.setDetail("HID path", hidInfo.hidDevice().map(HIDDevice::path).orElse("N/A"));
|
||||||
category.setDetail("HID service status", Controlify.instance().controllerHIDService().isDisabled() ? "Disabled" : "Enabled");
|
category.setDetail("HID service status", Controlify.instance().controllerHIDService().isDisabled() ? "Disabled" : "Enabled");
|
||||||
category.setDetail("GLFW name", Optional.ofNullable(GLFW.glfwGetJoystickName(joystickId)).orElse("N/A"));
|
category.setDetail("GLFW name", Optional.ofNullable(getControllerSystemName(joystickId)).orElse("N/A"));
|
||||||
throw new ReportedException(crashReport);
|
throw new ReportedException(crashReport);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void disconnect(Controller<?, ?> controller) {
|
@Override
|
||||||
|
public void tick(boolean outOfFocus) {
|
||||||
|
for (var controller : this.getConnectedControllers()) {
|
||||||
|
if (!outOfFocus)
|
||||||
|
wrapControllerError(controller::updateState, "Updating controller state", controller);
|
||||||
|
else
|
||||||
|
wrapControllerError(controller::clearState, "Clearing controller state", controller);
|
||||||
|
ControlifyEvents.CONTROLLER_STATE_UPDATE.invoker().onControllerStateUpdate(controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Controller<?, ?>> getController(int joystickId) {
|
||||||
|
return CONTROLLERS.values().stream().filter(controller -> controller.joystickId() == joystickId).findAny();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onControllerConnected(Controller<?, ?> controller, boolean hotplug) {
|
||||||
|
Log.LOGGER.info("Controller connected: {}", ControllerUtils.createControllerString(controller));
|
||||||
|
|
||||||
|
boolean newController = controlify.config().loadOrCreateControllerData(controller);
|
||||||
|
|
||||||
|
ControlifyEvents.CONTROLLER_CONNECTED.invoker().onControllerConnected(controller, hotplug, newController);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onControllerRemoved(Controller<?, ?> controller) {
|
||||||
|
Log.LOGGER.info("Controller disconnected: {}", ControllerUtils.createControllerString(controller));
|
||||||
|
|
||||||
|
controller.hidInfo().ifPresent(controlify.controllerHIDService()::unconsumeController);
|
||||||
|
removeController(controller);
|
||||||
|
|
||||||
|
ControlifyEvents.CONTROLLER_DISCONNECTED.invoker().onControllerDisconnected(controller);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void removeController(Controller<?, ?> controller) {
|
||||||
controller.close();
|
controller.close();
|
||||||
CONTROLLERS.remove(controller.uid(), controller);
|
CONTROLLERS.remove(controller.uid(), controller);
|
||||||
|
|
||||||
checkCompoundJoysticks();
|
checkCompoundJoysticks();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void disconnect(String uid) {
|
protected void removeController(String uid) {
|
||||||
Controller<?, ?> prev = CONTROLLERS.remove(uid);
|
Controller<?, ?> prev = CONTROLLERS.remove(uid);
|
||||||
if (prev != null) {
|
if (prev != null) {
|
||||||
prev.close();
|
prev.close();
|
||||||
@@ -78,20 +124,31 @@ public final class ControllerManager {
|
|||||||
checkCompoundJoysticks();
|
checkCompoundJoysticks();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Controller<?, ?>> getConnectedControllers() {
|
@Override
|
||||||
|
public List<Controller<?, ?>> getConnectedControllers() {
|
||||||
return ImmutableList.copyOf(CONTROLLERS.values());
|
return ImmutableList.copyOf(CONTROLLERS.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isControllerConnected(String uid) {
|
@Override
|
||||||
|
public boolean isControllerConnected(String uid) {
|
||||||
return CONTROLLERS.containsKey(uid);
|
return CONTROLLERS.containsKey(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void checkCompoundJoysticks() {
|
@Override
|
||||||
|
public void close() {
|
||||||
|
CONTROLLERS.values().forEach(Controller::close);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void loadGamepadMappings(Resource resource);
|
||||||
|
|
||||||
|
protected abstract String getControllerSystemName(int joystickId);
|
||||||
|
|
||||||
|
private void checkCompoundJoysticks() {
|
||||||
Controlify.instance().config().getCompoundJoysticks().values().forEach(info -> {
|
Controlify.instance().config().getCompoundJoysticks().values().forEach(info -> {
|
||||||
try {
|
try {
|
||||||
if (info.isLoaded() && !info.canBeUsed()) {
|
if (info.isLoaded() && !info.canBeUsed()) {
|
||||||
Log.LOGGER.warn("Unloading compound joystick " + info.friendlyName() + " due to missing controllers.");
|
Log.LOGGER.warn("Unloading compound joystick " + info.friendlyName() + " due to missing controllers.");
|
||||||
disconnect(info.type().mappingId());
|
removeController(info.type().mappingId());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info.isLoaded() && info.canBeUsed()) {
|
if (!info.isLoaded() && info.canBeUsed()) {
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package dev.isxander.controlify.controllermanager;
|
||||||
|
|
||||||
|
import dev.isxander.controlify.controller.Controller;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface ControllerManager {
|
||||||
|
void discoverControllers();
|
||||||
|
|
||||||
|
void tick(boolean outOfFocus);
|
||||||
|
|
||||||
|
boolean probeConnectedControllers();
|
||||||
|
|
||||||
|
List<Controller<?, ?>> getConnectedControllers();
|
||||||
|
|
||||||
|
Optional<Controller<?, ?>> getController(int jid);
|
||||||
|
|
||||||
|
boolean isControllerConnected(String uid);
|
||||||
|
|
||||||
|
boolean isControllerGamepad(int jid);
|
||||||
|
|
||||||
|
void close();
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package dev.isxander.controlify.controllermanager;
|
||||||
|
|
||||||
|
import com.google.common.io.ByteStreams;
|
||||||
|
import dev.isxander.controlify.Controlify;
|
||||||
|
import dev.isxander.controlify.controller.Controller;
|
||||||
|
import dev.isxander.controlify.utils.Log;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.server.packs.resources.Resource;
|
||||||
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
public class GLFWControllerManager extends AbstractControllerManager {
|
||||||
|
private final Minecraft minecraft;
|
||||||
|
|
||||||
|
public GLFWControllerManager() {
|
||||||
|
this.minecraft = Minecraft.getInstance();
|
||||||
|
|
||||||
|
minecraft.getResourceManager()
|
||||||
|
.getResource(Controlify.id("controllers/gamecontrollerdb.txt"))
|
||||||
|
.ifPresent(this::loadGamepadMappings);
|
||||||
|
|
||||||
|
this.setupCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupCallbacks() {
|
||||||
|
GLFW.glfwSetJoystickCallback((jid, event) -> {
|
||||||
|
try {
|
||||||
|
if (event == GLFW.GLFW_CONNECTED) {
|
||||||
|
createOrGet(jid, controlify.controllerHIDService().fetchType(jid))
|
||||||
|
.ifPresent(controller -> onControllerConnected(controller, true));
|
||||||
|
} else if (event == GLFW.GLFW_DISCONNECTED) {
|
||||||
|
getController(jid).ifPresent(this::onControllerRemoved);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Log.LOGGER.error("Failed to handle controller connect/disconnect event", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void discoverControllers() {
|
||||||
|
for (int i = 0; i < GLFW.GLFW_JOYSTICK_LAST; i++) {
|
||||||
|
if (!GLFW.glfwJoystickPresent(i))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Optional<Controller<?, ?>> controllerOpt = createOrGet(i, controlify.controllerHIDService().fetchType(i));
|
||||||
|
controllerOpt.ifPresent(controller -> onControllerConnected(controller, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean probeConnectedControllers() {
|
||||||
|
return areControllersConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick(boolean outOfFocus) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void loadGamepadMappings(Resource resource) {
|
||||||
|
Log.LOGGER.debug("Loading gamepad mappings...");
|
||||||
|
|
||||||
|
try (InputStream is = resource.open()) {
|
||||||
|
byte[] bytes = ByteStreams.toByteArray(is);
|
||||||
|
ByteBuffer buffer = MemoryUtil.memASCIISafe(new String(bytes));
|
||||||
|
|
||||||
|
if (!GLFW.glfwUpdateGamepadMappings(buffer)) {
|
||||||
|
Log.LOGGER.error("Failed to load gamepad mappings: {}", GLFW.glfwGetError(null));
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Log.LOGGER.error("Failed to load gamepad mappings: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isControllerGamepad(int jid) {
|
||||||
|
return GLFW.glfwJoystickIsGamepad(jid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getControllerSystemName(int joystickId) {
|
||||||
|
return isControllerGamepad(joystickId) ? GLFW.glfwGetGamepadName(joystickId) : GLFW.glfwGetJoystickName(joystickId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean areControllersConnected() {
|
||||||
|
return IntStream.range(0, GLFW.GLFW_JOYSTICK_LAST + 1)
|
||||||
|
.anyMatch(GLFW::glfwJoystickPresent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package dev.isxander.controlify.controllermanager;
|
||||||
|
|
||||||
|
import com.google.common.io.ByteStreams;
|
||||||
|
import com.sun.jna.Memory;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
import dev.isxander.controlify.Controlify;
|
||||||
|
import dev.isxander.controlify.controller.Controller;
|
||||||
|
import dev.isxander.controlify.driver.SDL2NativesManager;
|
||||||
|
import dev.isxander.controlify.utils.Log;
|
||||||
|
import io.github.libsdl4j.api.event.SDL_Event;
|
||||||
|
import io.github.libsdl4j.api.event.SDL_EventFilter;
|
||||||
|
import io.github.libsdl4j.api.rwops.SDL_RWops;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.server.packs.resources.Resource;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static io.github.libsdl4j.api.error.SdlError.SDL_GetError;
|
||||||
|
import static io.github.libsdl4j.api.event.SDL_EventType.*;
|
||||||
|
import static io.github.libsdl4j.api.event.SdlEvents.*;
|
||||||
|
import static io.github.libsdl4j.api.gamecontroller.SdlGamecontroller.*;
|
||||||
|
import static io.github.libsdl4j.api.joystick.SdlJoystick.*;
|
||||||
|
import static io.github.libsdl4j.api.rwops.SdlRWops.SDL_RWFromConstMem;
|
||||||
|
|
||||||
|
public class SDLControllerManager extends AbstractControllerManager {
|
||||||
|
private final Controlify controlify;
|
||||||
|
private final Minecraft minecraft;
|
||||||
|
|
||||||
|
private final SDL_Event event = new SDL_Event();
|
||||||
|
|
||||||
|
// must keep a reference to prevent GC from collecting it and the callback failing
|
||||||
|
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||||
|
private final EventFilter eventFilter;
|
||||||
|
|
||||||
|
public SDLControllerManager() {
|
||||||
|
Validate.isTrue(SDL2NativesManager.isLoaded(), "SDL2 natives must be loaded before creating SDLControllerManager");
|
||||||
|
|
||||||
|
this.controlify = Controlify.instance();
|
||||||
|
this.minecraft = Minecraft.getInstance();
|
||||||
|
|
||||||
|
SDL_SetEventFilter(eventFilter = new EventFilter(), Pointer.NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick(boolean outOfFocus) {
|
||||||
|
while (SDL_PollEvent(event) == 1) {
|
||||||
|
switch (event.type) {
|
||||||
|
case SDL_JOYDEVICEADDED -> {
|
||||||
|
int jid = event.jdevice.which;
|
||||||
|
Optional<Controller<?, ?>> controllerOpt = createOrGet(jid, controlify.controllerHIDService().fetchType(jid));
|
||||||
|
controllerOpt.ifPresent(controller -> onControllerConnected(controller, true));
|
||||||
|
}
|
||||||
|
case SDL_CONTROLLERDEVICEADDED -> {
|
||||||
|
int jid = event.cdevice.which;
|
||||||
|
Optional<Controller<?, ?>> controllerOpt = createOrGet(jid, controlify.controllerHIDService().fetchType(jid));
|
||||||
|
controllerOpt.ifPresent(controller -> onControllerConnected(controller, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_JOYDEVICEREMOVED -> {
|
||||||
|
int jid = event.jdevice.which;
|
||||||
|
getController(jid).ifPresent(this::onControllerRemoved);
|
||||||
|
}
|
||||||
|
case SDL_CONTROLLERDEVICEREMOVED -> {
|
||||||
|
int jid = event.cdevice.which;
|
||||||
|
getController(jid).ifPresent(this::onControllerRemoved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void discoverControllers() {
|
||||||
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
|
Optional<Controller<?, ?>> controllerOpt = createOrGet(i, controlify.controllerHIDService().fetchType(i));
|
||||||
|
controllerOpt.ifPresent(controller -> onControllerConnected(controller, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean probeConnectedControllers() {
|
||||||
|
return SDL_NumJoysticks() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isControllerGamepad(int jid) {
|
||||||
|
return SDL_IsGameController(jid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getControllerSystemName(int joystickId) {
|
||||||
|
return isControllerGamepad(joystickId) ? SDL_GameControllerNameForIndex(joystickId) : SDL_JoystickNameForIndex(joystickId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void loadGamepadMappings(Resource resource) {
|
||||||
|
Log.LOGGER.debug("Loading gamepad mappings...");
|
||||||
|
|
||||||
|
try (InputStream is = resource.open()) {
|
||||||
|
byte[] bytes = ByteStreams.toByteArray(is);
|
||||||
|
|
||||||
|
try (Memory memory = new Memory(bytes.length)) {
|
||||||
|
memory.write(0, bytes, 0, bytes.length);
|
||||||
|
SDL_RWops rw = SDL_RWFromConstMem(memory, bytes.length);
|
||||||
|
int count = SDL_GameControllerAddMappingsFromRW(rw, 1);
|
||||||
|
if (count < 1) {
|
||||||
|
Log.LOGGER.error("Failed to load gamepad mappings: {}", SDL_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
Log.LOGGER.error("Failed to load gamepad mappings", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class EventFilter implements SDL_EventFilter {
|
||||||
|
@Override
|
||||||
|
public int filterEvent(Pointer userdata, SDL_Event event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case SDL_JOYDEVICEADDED:
|
||||||
|
case SDL_JOYDEVICEREMOVED:
|
||||||
|
case SDL_CONTROLLERDEVICEADDED:
|
||||||
|
case SDL_CONTROLLERDEVICEREMOVED:
|
||||||
|
return 1;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ public class DebugProperties {
|
|||||||
/** 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);
|
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);
|
public static final boolean PRINT_GAMEPAD_STATE = boolProp("controlify.debug.print_gamepad_state", false, true);
|
||||||
/** Use experimental anti-snapback */
|
/** Use experimental anti-snapback */
|
||||||
public static final boolean USE_SNAPBACK = boolProp("controlify.debug.use_snapback", false, false);
|
public static final boolean USE_SNAPBACK = boolProp("controlify.debug.use_snapback", false, false);
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ public class SDL2NativesManager {
|
|||||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1");
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1");
|
||||||
|
|
||||||
// initialise SDL with just joystick and gamecontroller subsystems
|
// initialise SDL with just joystick and gamecontroller subsystems
|
||||||
if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) != 0) {
|
if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER | SDL_INIT_EVENTS) != 0) {
|
||||||
Log.LOGGER.error("Failed to initialise SDL2: " + SDL_GetError());
|
Log.LOGGER.error("Failed to initialise SDL2: " + SDL_GetError());
|
||||||
throw new RuntimeException("Failed to initialise SDL2: " + SDL_GetError());
|
throw new RuntimeException("Failed to initialise SDL2: " + SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import dev.isxander.controlify.debug.DebugProperties;
|
|||||||
import dev.isxander.controlify.driver.*;
|
import dev.isxander.controlify.driver.*;
|
||||||
import dev.isxander.controlify.utils.Log;
|
import dev.isxander.controlify.utils.Log;
|
||||||
import io.github.libsdl4j.api.gamecontroller.SDL_GameController;
|
import io.github.libsdl4j.api.gamecontroller.SDL_GameController;
|
||||||
|
import io.github.libsdl4j.api.joystick.SdlJoystick;
|
||||||
import net.minecraft.util.Mth;
|
import net.minecraft.util.Mth;
|
||||||
|
|
||||||
import static io.github.libsdl4j.api.error.SdlError.*;
|
import static io.github.libsdl4j.api.error.SdlError.*;
|
||||||
@@ -26,6 +27,9 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
|
|||||||
|
|
||||||
public SDL2GamepadDriver(int jid) {
|
public SDL2GamepadDriver(int jid) {
|
||||||
this.ptrGamepad = SDL_GameControllerOpen(jid);
|
this.ptrGamepad = SDL_GameControllerOpen(jid);
|
||||||
|
if (ptrGamepad == null)
|
||||||
|
throw new IllegalStateException("Could not open gamepad: " + SDL_GetError());
|
||||||
|
|
||||||
this.guid = SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(ptrGamepad)).toString();
|
this.guid = SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(ptrGamepad)).toString();
|
||||||
this.isGyroSupported = SDL_GameControllerHasSensor(ptrGamepad, SDL_SENSOR_GYRO);
|
this.isGyroSupported = SDL_GameControllerHasSensor(ptrGamepad, SDL_SENSOR_GYRO);
|
||||||
this.isRumbleSupported = SDL_GameControllerHasRumble(ptrGamepad);
|
this.isRumbleSupported = SDL_GameControllerHasRumble(ptrGamepad);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package dev.isxander.controlify.gui.screen;
|
package dev.isxander.controlify.gui.screen;
|
||||||
|
|
||||||
import dev.isxander.controlify.Controlify;
|
import dev.isxander.controlify.Controlify;
|
||||||
import dev.isxander.controlify.ControllerManager;
|
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||||
import dev.isxander.controlify.controller.gamepad.GamepadState;
|
import dev.isxander.controlify.controller.gamepad.GamepadState;
|
||||||
import dev.isxander.controlify.controller.joystick.JoystickController;
|
import dev.isxander.controlify.controller.joystick.JoystickController;
|
||||||
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
|
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
|
||||||
|
import dev.isxander.controlify.controllermanager.ControllerManager;
|
||||||
import net.minecraft.ChatFormatting;
|
import net.minecraft.ChatFormatting;
|
||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.client.gui.components.Button;
|
import net.minecraft.client.gui.components.Button;
|
||||||
@@ -33,6 +33,8 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS
|
|||||||
private static final ResourceLocation GREEN_BACK_BAR = new ResourceLocation("boss_bar/green_background");
|
private static final ResourceLocation GREEN_BACK_BAR = new ResourceLocation("boss_bar/green_background");
|
||||||
private static final ResourceLocation GREEN_FRONT_BAR = new ResourceLocation("boss_bar/green_progress");
|
private static final ResourceLocation GREEN_FRONT_BAR = new ResourceLocation("boss_bar/green_progress");
|
||||||
|
|
||||||
|
protected final Controlify controlify;
|
||||||
|
protected final ControllerManager controllerManager;
|
||||||
protected final Controller<?, ?> controller;
|
protected final Controller<?, ?> controller;
|
||||||
private final Supplier<Screen> parent;
|
private final Supplier<Screen> parent;
|
||||||
|
|
||||||
@@ -52,6 +54,8 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS
|
|||||||
|
|
||||||
public ControllerCalibrationScreen(Controller<?, ?> controller, Supplier<Screen> parent) {
|
public ControllerCalibrationScreen(Controller<?, ?> controller, Supplier<Screen> parent) {
|
||||||
super(Component.translatable("controlify.calibration.title"));
|
super(Component.translatable("controlify.calibration.title"));
|
||||||
|
this.controlify = Controlify.instance();
|
||||||
|
this.controllerManager = controlify.getControllerManager().orElseThrow();
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.axisData = new double[controller.axisCount() * CALIBRATION_TIME];
|
this.axisData = new double[controller.axisCount() * CALIBRATION_TIME];
|
||||||
@@ -129,7 +133,7 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void tick() {
|
public void tick() {
|
||||||
if (!ControllerManager.isControllerConnected(controller.uid())) {
|
if (!controllerManager.isControllerConnected(controller.uid())) {
|
||||||
onClose();
|
onClose();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -161,7 +165,7 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS
|
|||||||
controller.config().delayedCalibration = false;
|
controller.config().delayedCalibration = false;
|
||||||
// no need to save because of setCurrentController
|
// no need to save because of setCurrentController
|
||||||
|
|
||||||
Controlify.instance().setCurrentController(controller);
|
Controlify.instance().setCurrentController(controller, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +235,7 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS
|
|||||||
if (!controller.config().deadzonesCalibrated) {
|
if (!controller.config().deadzonesCalibrated) {
|
||||||
controller.config().delayedCalibration = true;
|
controller.config().delayedCalibration = true;
|
||||||
Controlify.instance().config().setDirty();
|
Controlify.instance().config().setDirty();
|
||||||
Controlify.instance().setCurrentController(null);
|
Controlify.instance().setCurrentController(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ package dev.isxander.controlify.gui.screen;
|
|||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import dev.isxander.controlify.Controlify;
|
import dev.isxander.controlify.Controlify;
|
||||||
import dev.isxander.controlify.ControllerManager;
|
|
||||||
import dev.isxander.controlify.api.buttonguide.ButtonGuideApi;
|
import dev.isxander.controlify.api.buttonguide.ButtonGuideApi;
|
||||||
import dev.isxander.controlify.api.buttonguide.ButtonGuidePredicate;
|
import dev.isxander.controlify.api.buttonguide.ButtonGuidePredicate;
|
||||||
import dev.isxander.controlify.api.buttonguide.ButtonRenderPosition;
|
import dev.isxander.controlify.api.buttonguide.ButtonRenderPosition;
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
import dev.isxander.controlify.driver.SDL2NativesManager;
|
import dev.isxander.controlify.controllermanager.ControllerManager;
|
||||||
import dev.isxander.controlify.gui.components.FakePositionPlainTextButton;
|
import dev.isxander.controlify.gui.components.FakePositionPlainTextButton;
|
||||||
import dev.isxander.controlify.screenop.ScreenControllerEventListener;
|
import dev.isxander.controlify.screenop.ScreenControllerEventListener;
|
||||||
import dev.isxander.controlify.utils.Animator;
|
import dev.isxander.controlify.utils.Animator;
|
||||||
import net.minecraft.ChatFormatting;
|
import net.minecraft.ChatFormatting;
|
||||||
import net.minecraft.Util;
|
import net.minecraft.Util;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.gui.ComponentPath;
|
import net.minecraft.client.gui.ComponentPath;
|
||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.client.gui.components.Button;
|
import net.minecraft.client.gui.components.Button;
|
||||||
@@ -47,24 +47,27 @@ public class ControllerCarouselScreen extends Screen implements ScreenController
|
|||||||
private int carouselIndex;
|
private int carouselIndex;
|
||||||
private Animator.AnimationInstance carouselAnimation = null;
|
private Animator.AnimationInstance carouselAnimation = null;
|
||||||
|
|
||||||
|
private final Controlify controlify;
|
||||||
|
private final ControllerManager controllerManager;
|
||||||
|
|
||||||
private Button globalSettingsButton, doneButton;
|
private Button globalSettingsButton, doneButton;
|
||||||
|
|
||||||
private ControllerCarouselScreen(Screen parent) {
|
private ControllerCarouselScreen(Screen parent) {
|
||||||
super(Component.translatable("controlify.gui.carousel.title"));
|
super(Component.translatable("controlify.gui.carousel.title"));
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.carouselIndex = Controlify.instance().getCurrentController().map(c -> ControllerManager.getConnectedControllers().indexOf(c)).orElse(0);
|
|
||||||
|
this.controlify = Controlify.instance();
|
||||||
|
this.controllerManager = controlify.getControllerManager().orElseThrow();
|
||||||
|
|
||||||
|
this.carouselIndex = controlify.getCurrentController().map(c -> controllerManager.getConnectedControllers().indexOf(c)).orElse(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Screen createConfigScreen(Screen parent) {
|
public static void openConfigScreen(Screen parent) {
|
||||||
var controlify = Controlify.instance();
|
var controlify = Controlify.instance();
|
||||||
|
|
||||||
if (controlify.config().globalSettings().delegateSetup) {
|
controlify.finishControlifyInit().whenComplete((v, th) -> {
|
||||||
controlify.discoverControllers();
|
Minecraft.getInstance().setScreen(new ControllerCarouselScreen(parent));
|
||||||
controlify.config().globalSettings().delegateSetup = false;
|
});
|
||||||
controlify.config().save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ControllerCarouselScreen(parent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -109,7 +112,7 @@ public class ControllerCarouselScreen extends Screen implements ScreenController
|
|||||||
prevSelectedController = null;
|
prevSelectedController = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
carouselEntries = ControllerManager.getConnectedControllers().stream()
|
carouselEntries = controllerManager.getConnectedControllers().stream()
|
||||||
.map(c -> new CarouselEntry(c, this.width / 3, this.height - 66))
|
.map(c -> new CarouselEntry(c, this.width / 3, this.height - 66))
|
||||||
.peek(this::addRenderableWidget)
|
.peek(this::addRenderableWidget)
|
||||||
.toList();
|
.toList();
|
||||||
@@ -118,7 +121,7 @@ public class ControllerCarouselScreen extends Screen implements ScreenController
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
.map(carouselEntries::indexOf)
|
.map(carouselEntries::indexOf)
|
||||||
.orElse(Controlify.instance().getCurrentController()
|
.orElse(Controlify.instance().getCurrentController()
|
||||||
.map(c -> ControllerManager.getConnectedControllers().indexOf(c))
|
.map(c -> controllerManager.getConnectedControllers().indexOf(c))
|
||||||
.orElse(0)
|
.orElse(0)
|
||||||
);
|
);
|
||||||
if (!carouselEntries.isEmpty())
|
if (!carouselEntries.isEmpty())
|
||||||
@@ -220,7 +223,7 @@ public class ControllerCarouselScreen extends Screen implements ScreenController
|
|||||||
this.hasNickname = this.controller.config().customName != null;
|
this.hasNickname = this.controller.config().customName != null;
|
||||||
|
|
||||||
this.settingsButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.settings"), btn -> minecraft.setScreen(ControllerConfigScreenFactory.generateConfigScreen(ControllerCarouselScreen.this, controller))).width((getWidth() - 2) / 2 - 2).build();
|
this.settingsButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.settings"), btn -> minecraft.setScreen(ControllerConfigScreenFactory.generateConfigScreen(ControllerCarouselScreen.this, controller))).width((getWidth() - 2) / 2 - 2).build();
|
||||||
this.useControllerButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.use"), btn -> Controlify.instance().setCurrentController(controller)).width(settingsButton.getWidth()).build();
|
this.useControllerButton = Button.builder(Component.translatable("controlify.gui.carousel.entry.use"), btn -> Controlify.instance().setCurrentController(controller, true)).width(settingsButton.getWidth()).build();
|
||||||
this.children = ImmutableList.of(settingsButton, useControllerButton);
|
this.children = ImmutableList.of(settingsButton, useControllerButton);
|
||||||
|
|
||||||
this.prevUse = isCurrentlyUsed();
|
this.prevUse = isCurrentlyUsed();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package dev.isxander.controlify.gui.screen;
|
package dev.isxander.controlify.gui.screen;
|
||||||
|
|
||||||
import dev.isxander.controlify.Controlify;
|
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.gui.screens.Screen;
|
import net.minecraft.client.gui.screens.Screen;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
@@ -21,10 +20,7 @@ public class ModConfigOpenerScreen extends Screen {
|
|||||||
Minecraft minecraft = Minecraft.getInstance();
|
Minecraft minecraft = Minecraft.getInstance();
|
||||||
this.init(minecraft, minecraft.getWindow().getGuiScaledWidth(), minecraft.getWindow().getGuiScaledHeight());
|
this.init(minecraft, minecraft.getWindow().getGuiScaledWidth(), minecraft.getWindow().getGuiScaledHeight());
|
||||||
|
|
||||||
Controlify.instance().askNatives()
|
ControllerCarouselScreen.openConfigScreen(lastScreen);
|
||||||
.whenComplete((result, error) ->
|
|
||||||
minecraft.setScreen(ControllerCarouselScreen.createConfigScreen(lastScreen))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ package dev.isxander.controlify.mixins.core;
|
|||||||
|
|
||||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||||
import dev.isxander.controlify.Controlify;
|
import dev.isxander.controlify.Controlify;
|
||||||
import dev.isxander.controlify.ControllerManager;
|
|
||||||
import dev.isxander.controlify.api.ControlifyApi;
|
import dev.isxander.controlify.api.ControlifyApi;
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controllermanager.ControllerManager;
|
||||||
import dev.isxander.controlify.utils.Animator;
|
import dev.isxander.controlify.utils.Animator;
|
||||||
import dev.isxander.controlify.utils.MouseMinecraftCallNotifier;
|
import dev.isxander.controlify.utils.MouseMinecraftCallNotifier;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
@@ -72,7 +71,7 @@ public abstract class MinecraftMixin {
|
|||||||
|
|
||||||
@Inject(method = "close", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/telemetry/ClientTelemetryManager;close()V"))
|
@Inject(method = "close", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/telemetry/ClientTelemetryManager;close()V"))
|
||||||
private void onMinecraftClose(CallbackInfo ci) {
|
private void onMinecraftClose(CallbackInfo ci) {
|
||||||
ControllerManager.getConnectedControllers().forEach(Controller::close);
|
Controlify.instance().getControllerManager().ifPresent(ControllerManager::close);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;render(FJZ)V"))
|
@Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;render(FJZ)V"))
|
||||||
|
|||||||
+3
-1
@@ -24,10 +24,12 @@ public class ChatComponentMixin {
|
|||||||
if (!(minecraft.screen instanceof ChatScreen))
|
if (!(minecraft.screen instanceof ChatScreen))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Controller<?, ?> controller = Controlify.instance().currentController();
|
Controlify.instance().getCurrentController().ifPresent(controller -> {
|
||||||
graphics.pose().pushPose();
|
graphics.pose().pushPose();
|
||||||
if (controller.config().chatKeyboardHeight == 0) return;
|
if (controller.config().chatKeyboardHeight == 0) return;
|
||||||
graphics.pose().translate(0, -controller.config().chatKeyboardHeight * minecraft.getWindow().getGuiScaledHeight(), 0);
|
graphics.pose().translate(0, -controller.config().chatKeyboardHeight * minecraft.getWindow().getGuiScaledHeight(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject(method = "render", at = @At("TAIL"))
|
@Inject(method = "render", at = @At("TAIL"))
|
||||||
|
|||||||
+1
-4
@@ -37,9 +37,6 @@ public class ControlsScreenMixin extends OptionsSubScreen {
|
|||||||
|
|
||||||
@Unique
|
@Unique
|
||||||
private void openControllerSettings() {
|
private void openControllerSettings() {
|
||||||
Controlify.instance().askNatives()
|
ControllerCarouselScreen.openConfigScreen(this);
|
||||||
.whenComplete((result, error) ->
|
|
||||||
minecraft.setScreen(ControllerCarouselScreen.createConfigScreen(lastScreen))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,9 @@ import dev.isxander.controlify.controller.ControllerType;
|
|||||||
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
import dev.isxander.controlify.controller.gamepad.GamepadController;
|
||||||
import dev.isxander.controlify.hid.ControllerHIDService;
|
import dev.isxander.controlify.hid.ControllerHIDService;
|
||||||
import dev.isxander.controlify.hid.HIDDevice;
|
import dev.isxander.controlify.hid.HIDDevice;
|
||||||
|
import net.minecraft.CrashReport;
|
||||||
|
import net.minecraft.CrashReportCategory;
|
||||||
|
import net.minecraft.ReportedException;
|
||||||
import net.minecraft.util.Mth;
|
import net.minecraft.util.Mth;
|
||||||
import org.joml.Vector2f;
|
import org.joml.Vector2f;
|
||||||
|
|
||||||
@@ -29,6 +32,19 @@ public class ControllerUtils {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void wrapControllerError(Runnable runnable, String errorTitle, Controller<?, ?> controller) {
|
||||||
|
try {
|
||||||
|
runnable.run();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
CrashReport crashReport = CrashReport.forThrowable(e, errorTitle);
|
||||||
|
CrashReportCategory category = crashReport.addCategory("Affected controller");
|
||||||
|
category.setDetail("Controller name", controller.name());
|
||||||
|
category.setDetail("Controller identification", controller.type().toString());
|
||||||
|
category.setDetail("Controller type", controller.getClass().getCanonicalName());
|
||||||
|
throw new ReportedException(crashReport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static float deadzone(float value, float deadzone) {
|
public static float deadzone(float value, float deadzone) {
|
||||||
return (value - Math.copySign(Math.min(deadzone, Math.abs(value)), value)) / (1 - deadzone);
|
return (value - Math.copySign(Math.min(deadzone, Math.abs(value)), value)) / (1 - deadzone);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package dev.isxander.controlify.wireless;
|
package dev.isxander.controlify.wireless;
|
||||||
|
|
||||||
import dev.isxander.controlify.Controlify;
|
import dev.isxander.controlify.Controlify;
|
||||||
import dev.isxander.controlify.ControllerManager;
|
|
||||||
import dev.isxander.controlify.controller.BatteryLevel;
|
import dev.isxander.controlify.controller.BatteryLevel;
|
||||||
import dev.isxander.controlify.controller.Controller;
|
import dev.isxander.controlify.controller.Controller;
|
||||||
|
import dev.isxander.controlify.controllermanager.ControllerManager;
|
||||||
import dev.isxander.controlify.utils.ToastUtils;
|
import dev.isxander.controlify.utils.ToastUtils;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
@@ -24,7 +24,11 @@ public class LowBatteryNotifier {
|
|||||||
if (!Controlify.instance().config().globalSettings().notifyLowBattery)
|
if (!Controlify.instance().config().globalSettings().notifyLowBattery)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (Controller<?, ?> controller : ControllerManager.getConnectedControllers()) {
|
ControllerManager controllerManager = Controlify.instance().getControllerManager().orElse(null);
|
||||||
|
if (controllerManager == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (Controller<?, ?> controller : controllerManager.getConnectedControllers()) {
|
||||||
BatteryLevel batteryLevel = controller.batteryLevel();
|
BatteryLevel batteryLevel = controller.batteryLevel();
|
||||||
if (batteryLevel == BatteryLevel.UNKNOWN) {
|
if (batteryLevel == BatteryLevel.UNKNOWN) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -194,11 +194,11 @@ public class FakeController implements JoystickController<JoystickConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void use() {
|
public void use() {
|
||||||
Controlify.instance().setCurrentController(this);
|
Controlify.instance().setCurrentController(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void finish() {
|
public void finish() {
|
||||||
Controlify.instance().setCurrentController(null);
|
Controlify.instance().setCurrentController(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in new issue
Block a user