[BROKEN] Abstract controller manager system
@ -1,13 +1,14 @@
|
||||
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.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.screen.*;
|
||||
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.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;
|
||||
@ -40,30 +40,28 @@ import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.multiplayer.ServerData;
|
||||
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.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
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.gamecontroller.SdlGamecontroller.SDL_GameControllerNumMappings;
|
||||
import static io.github.libsdl4j.api.rwops.SdlRWops.SDL_RWFromConstMem;
|
||||
import static dev.isxander.controlify.utils.ControllerUtils.wrapControllerError;
|
||||
|
||||
public class Controlify implements ControlifyApi {
|
||||
private static Controlify instance = null;
|
||||
|
||||
private final Minecraft minecraft = Minecraft.getInstance();
|
||||
|
||||
private ControllerManager controllerManager;
|
||||
|
||||
private boolean finishedInit = false;
|
||||
private boolean probeMode = false;
|
||||
|
||||
private Controller<?, ?> currentController = null;
|
||||
private InGameInputHandler inGameInputHandler;
|
||||
public InGameButtonGuide inGameButtonGuide;
|
||||
@ -155,18 +153,13 @@ public class Controlify implements ControlifyApi {
|
||||
|
||||
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();
|
||||
boolean controllersConnected = GLFWControllerManager.areControllersConnected();
|
||||
|
||||
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
|
||||
ControlifyEvents.CONTROLLER_CONNECTED.register(this::onControllerAdded);
|
||||
ControlifyEvents.CONTROLLER_DISCONNECTED.register(this::onControllerRemoved);
|
||||
|
||||
if (controllersConnected) {
|
||||
if (config().globalSettings().quietMode) {
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.setup_in_config.title"),
|
||||
Component.translatable(
|
||||
@ -177,33 +170,22 @@ public class Controlify implements ControlifyApi {
|
||||
),
|
||||
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
|
||||
ClientTickEvents.START_CLIENT_TICK.register(this::tick);
|
||||
ClientLifecycleEvents.CLIENT_STOPPING.register(minecraft -> {
|
||||
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
|
||||
notifyOfNewFeatures();
|
||||
@ -217,73 +199,26 @@ public class Controlify implements ControlifyApi {
|
||||
public void discoverControllers() {
|
||||
if (hasDiscoveredControllers) {
|
||||
Log.LOGGER.warn("Attempted to discover controllers twice!");
|
||||
return;
|
||||
}
|
||||
hasDiscoveredControllers = true;
|
||||
|
||||
DebugLog.log("Discovering and initializing controllers...");
|
||||
|
||||
// load gamepad mappings before every
|
||||
minecraft.getResourceManager()
|
||||
.getResource(Controlify.id("controllers/gamecontrollerdb.txt"))
|
||||
.ifPresent(this::loadGamepadMappings);
|
||||
controllerManager.discoverControllers();
|
||||
|
||||
// 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()) {
|
||||
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);
|
||||
Controller<?, ?> controller = controllerManager.getConnectedControllers().stream().findFirst().orElse(null);
|
||||
if (controller != null && (controller.config().delayedCalibration || !controller.config().deadzonesCalibrated)) {
|
||||
controller = null;
|
||||
}
|
||||
|
||||
this.setCurrentController(controller);
|
||||
this.setCurrentController(controller, false);
|
||||
} else {
|
||||
// setCurrentController saves config so there is no need to set dirty to save
|
||||
config().saveIfDirty();
|
||||
@ -298,51 +233,57 @@ public class Controlify implements ControlifyApi {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
public CompletableFuture<Void> finishControlifyInit() {
|
||||
if (finishedInit) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
finishedInit = true;
|
||||
|
||||
var controllerOpt = ControllerManager.createOrGet(jid, controllerHIDService.fetchType(jid));
|
||||
if (controllerOpt.isEmpty()) return;
|
||||
var controller = controllerOpt.get();
|
||||
askNatives().whenComplete((loaded, th) -> {
|
||||
Log.LOGGER.info("Finishing Controlify init...");
|
||||
|
||||
Log.LOGGER.info("Controller connected: " + ControllerUtils.createControllerString(controller));
|
||||
controllerManager = loaded ? new SDLControllerManager() : new GLFWControllerManager();
|
||||
|
||||
config().loadOrCreateControllerData(controller);
|
||||
ClientTickEvents.START_CLIENT_TICK.register(this::tick);
|
||||
ConnectServerEvent.EVENT.register((minecraft, address, data) -> {
|
||||
notifyNewServer(data);
|
||||
});
|
||||
discoverControllers();
|
||||
});
|
||||
|
||||
return askNatives().thenApply(loaded -> null);
|
||||
}
|
||||
|
||||
private void onControllerAdded(Controller<?, ?> controller, boolean hotplugged, boolean newController) {
|
||||
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) {
|
||||
if (controller.config().allowVibrations && !SDL2NativesManager.isLoaded()) {
|
||||
controller.config().allowVibrations = false;
|
||||
config().setDirty();
|
||||
}
|
||||
|
||||
if (ControllerManager.getConnectedControllers().size() == 1 && (controller.config().deadzonesCalibrated || controller.config().delayedCalibration)) {
|
||||
this.setCurrentController(controller);
|
||||
if (hotplugged) {
|
||||
if (controller.config().deadzonesCalibrated) {
|
||||
setCurrentController(controller, hotplugged);
|
||||
} else {
|
||||
calibrationQueue.add(controller);
|
||||
}
|
||||
}
|
||||
|
||||
if (controller instanceof JoystickController<?> joystick && joystick.mapping() instanceof UnmappedJoystickMapping) {
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.default_controller_connected.title"),
|
||||
Component.translatable("controlify.toast.default_controller_connected.description"),
|
||||
false
|
||||
Component.translatable("controlify.toast.unmapped_joystick.title"),
|
||||
Component.translatable("controlify.toast.unmapped_joystick.description", controller.name()),
|
||||
true
|
||||
);
|
||||
} else {
|
||||
this.askToSwitchController(controller);
|
||||
config().saveIfDirty();
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.controller_connected.title"),
|
||||
Component.translatable("controlify.toast.controller_connected.description", controller.name()),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (minecraft.screen instanceof ControllerCarouselScreen controllerListScreen) {
|
||||
@ -350,32 +291,25 @@ public class Controlify implements ControlifyApi {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
private void onControllerRemoved(Controller<?, ?> controller) {
|
||||
this.setCurrentController(
|
||||
controllerManager.getConnectedControllers()
|
||||
.stream()
|
||||
.findFirst()
|
||||
.orElse(null),
|
||||
true);
|
||||
|
||||
controller.hidInfo().ifPresent(controllerHIDService::unconsumeController);
|
||||
this.setInputMode(
|
||||
getCurrentController().isEmpty()
|
||||
? InputMode.KEYBOARD_MOUSE
|
||||
: InputMode.CONTROLLER
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
ToastUtils.sendToast(
|
||||
Component.translatable("controlify.toast.controller_disconnected.title"),
|
||||
Component.translatable("controlify.toast.controller_disconnected.description", controller.name()),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -422,36 +356,6 @@ public class Controlify implements ControlifyApi {
|
||||
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.
|
||||
* 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();
|
||||
|
||||
for (var controller : ControllerManager.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);
|
||||
}
|
||||
controllerManager.tick(outOfFocus);
|
||||
|
||||
if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) {
|
||||
if (switchableController.state().hasAnyInput()) {
|
||||
switchableController.clearState();
|
||||
this.setCurrentController(switchableController); // setCurrentController sets switchableController to null
|
||||
this.setCurrentController(switchableController, true); // setCurrentController sets switchableController to null
|
||||
if (askSwitchToast != null) {
|
||||
askSwitchToast.remove();
|
||||
askSwitchToast = null;
|
||||
@ -533,7 +431,7 @@ public class Controlify implements ControlifyApi {
|
||||
Component.translatable("controlify.toast.faulty_input.description"),
|
||||
true
|
||||
);
|
||||
this.setCurrentController(null);
|
||||
this.setCurrentController(null, true);
|
||||
consecutiveInputSwitches = 0;
|
||||
return;
|
||||
}
|
||||
@ -549,16 +447,12 @@ public class Controlify implements ControlifyApi {
|
||||
ControlifyEvents.ACTIVE_CONTROLLER_TICKED.invoker().onControllerStateUpdate(controller);
|
||||
}
|
||||
|
||||
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);
|
||||
private void probeTick() {
|
||||
if (probeMode) {
|
||||
if (GLFWControllerManager.areControllersConnected()) {
|
||||
probeMode = false;
|
||||
minecraft.execute(this::finishControlifyInit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -566,17 +460,6 @@ public class Controlify implements ControlifyApi {
|
||||
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
|
||||
@Deprecated
|
||||
public @NotNull Controller<?, ?> currentController() {
|
||||
@ -591,7 +474,7 @@ public class Controlify implements ControlifyApi {
|
||||
return Optional.ofNullable(currentController);
|
||||
}
|
||||
|
||||
public void setCurrentController(@Nullable Controller<?, ?> controller) {
|
||||
public void setCurrentController(@Nullable Controller<?, ?> controller, boolean changeInputMode) {
|
||||
if (this.currentController == controller) return;
|
||||
|
||||
this.currentController = controller;
|
||||
@ -617,10 +500,14 @@ public class Controlify implements ControlifyApi {
|
||||
|
||||
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)
|
||||
calibrationQueue.add(controller);
|
||||
public Optional<ControllerManager> getControllerManager() {
|
||||
return Optional.ofNullable(controllerManager);
|
||||
}
|
||||
|
||||
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.controller.Controller;
|
||||
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.EventFactory;
|
||||
|
||||
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.
|
||||
*/
|
||||
@ -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
|
||||
public interface InputModeChanged {
|
||||
void onInputModeChanged(InputMode mode);
|
||||
|
@ -2,7 +2,6 @@ package dev.isxander.controlify.config;
|
||||
|
||||
import com.google.gson.*;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.ControllerManager;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controller.joystick.CompoundJoystickInfo;
|
||||
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
|
||||
|
||||
for (var controller : ControllerManager.getConnectedControllers()) {
|
||||
// `add` replaces if already existing
|
||||
newControllerData.add(controller.uid(), generateControllerConfig(controller));
|
||||
}
|
||||
controlify.getControllerManager().ifPresent(controllerManager -> {
|
||||
for (Controller<?, ?> controller : controllerManager.getConnectedControllers()) {
|
||||
// `add` replaces if already existing
|
||||
newControllerData.add(controller.uid(), generateControllerConfig(controller));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
controllerData = newControllerData;
|
||||
config.addProperty("current_controller", currentControllerUid = controlify.getCurrentController().map(Controller::uid).orElse(null));
|
||||
@ -137,8 +139,10 @@ public class ControlifyConfig {
|
||||
JsonObject controllers = object.getAsJsonObject("controllers");
|
||||
if (controllers != null) {
|
||||
this.controllerData = controllers;
|
||||
for (var controller : ControllerManager.getConnectedControllers()) {
|
||||
loadOrCreateControllerData(controller);
|
||||
if (controlify.getControllerManager().isPresent()) {
|
||||
for (var controller : controlify.getControllerManager().get().getConnectedControllers()) {
|
||||
loadOrCreateControllerData(controller);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setDirty();
|
||||
|
@ -29,7 +29,7 @@ public class GlobalSettings {
|
||||
public boolean allowServerRumble = true;
|
||||
public boolean uiSounds = false;
|
||||
public boolean notifyLowBattery = true;
|
||||
public boolean delegateSetup = false;
|
||||
public boolean quietMode = false;
|
||||
public float ingameButtonGuideScale = 1f;
|
||||
|
||||
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.JsonElement;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.ControllerManager;
|
||||
import dev.isxander.controlify.bindings.ControllerBindings;
|
||||
import dev.isxander.controlify.hid.ControllerHIDService;
|
||||
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) {
|
||||
String uniqueName = name;
|
||||
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++ + ")";
|
||||
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);
|
||||
|
||||
BasicGamepadInputDriver.BasicGamepadState basicState = drivers.basicGamepadInputDriver().getBasicGamepadState();
|
||||
|
||||
if (DebugProperties.PRINT_GAMEPAD_STATE) {
|
||||
Log.LOGGER.info(basicState.toString());
|
||||
}
|
||||
|
||||
GamepadState.AxesState deadzoneAxesState = basicState.axes()
|
||||
.leftJoystickDeadZone(config().getLeftStickDeadzone())
|
||||
.rightJoystickDeadZone(config().getRightStickDeadzone());
|
||||
@ -99,11 +104,6 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
|
||||
this.absoluteGyro.add(gyroState);
|
||||
|
||||
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() {
|
||||
|
@ -1,6 +1,6 @@
|
||||
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.ControllerType;
|
||||
|
||||
@ -14,7 +14,7 @@ public record CompoundJoystickInfo(Collection<String> joystickUids, String frien
|
||||
}
|
||||
|
||||
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()) {
|
||||
return false; // not all controllers are connected
|
||||
}
|
||||
@ -26,13 +26,13 @@ public record CompoundJoystickInfo(Collection<String> joystickUids, String frien
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return ControllerManager.isControllerConnected(createUID(joystickUids));
|
||||
return Controlify.instance().getControllerManager().orElseThrow().isControllerConnected(createUID(joystickUids));
|
||||
}
|
||||
|
||||
public Optional<CompoundJoystickController> attemptCreate() {
|
||||
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()))
|
||||
.map(Controller::joystickId)
|
||||
.toList();
|
||||
|
@ -1,33 +1,46 @@
|
||||
package dev.isxander.controlify;
|
||||
package dev.isxander.controlify.controllermanager;
|
||||
|
||||
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.gamepad.GamepadController;
|
||||
import dev.isxander.controlify.hid.ControllerHIDService;
|
||||
import dev.isxander.controlify.controller.joystick.CompoundJoystickController;
|
||||
import dev.isxander.controlify.controller.joystick.SingleJoystickController;
|
||||
import dev.isxander.controlify.debug.DebugProperties;
|
||||
import dev.isxander.controlify.hid.ControllerHIDService;
|
||||
import dev.isxander.controlify.hid.HIDDevice;
|
||||
import dev.isxander.controlify.utils.ControllerUtils;
|
||||
import dev.isxander.controlify.utils.DebugLog;
|
||||
import dev.isxander.controlify.utils.Log;
|
||||
import net.minecraft.CrashReport;
|
||||
import net.minecraft.CrashReportCategory;
|
||||
import net.minecraft.ReportedException;
|
||||
import org.hid4java.HidDevice;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class ControllerManager {
|
||||
private ControllerManager() {
|
||||
import static dev.isxander.controlify.utils.ControllerUtils.wrapControllerError;
|
||||
|
||||
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 static Optional<Controller<?, ?>> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
|
||||
public Optional<Controller<?, ?>> createOrGet(int joystickId, ControllerHIDService.ControllerHIDInfo hidInfo) {
|
||||
try {
|
||||
Optional<String> uid = hidInfo.createControllerUID();
|
||||
if (uid.isPresent() && CONTROLLERS.containsKey(uid.get())) {
|
||||
@ -39,7 +52,7 @@ public final class ControllerManager {
|
||||
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);
|
||||
CONTROLLERS.put(controller.uid(), controller);
|
||||
checkCompoundJoysticks();
|
||||
@ -57,19 +70,52 @@ public final class ControllerManager {
|
||||
category.setDetail("Controller identification", hidInfo.type());
|
||||
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("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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
CONTROLLERS.remove(controller.uid(), controller);
|
||||
|
||||
checkCompoundJoysticks();
|
||||
}
|
||||
|
||||
public static void disconnect(String uid) {
|
||||
protected void removeController(String uid) {
|
||||
Controller<?, ?> prev = CONTROLLERS.remove(uid);
|
||||
if (prev != null) {
|
||||
prev.close();
|
||||
@ -78,20 +124,31 @@ public final class ControllerManager {
|
||||
checkCompoundJoysticks();
|
||||
}
|
||||
|
||||
public static List<Controller<?, ?>> getConnectedControllers() {
|
||||
@Override
|
||||
public List<Controller<?, ?>> getConnectedControllers() {
|
||||
return ImmutableList.copyOf(CONTROLLERS.values());
|
||||
}
|
||||
|
||||
public static boolean isControllerConnected(String uid) {
|
||||
@Override
|
||||
public boolean isControllerConnected(String 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 -> {
|
||||
try {
|
||||
if (info.isLoaded() && !info.canBeUsed()) {
|
||||
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()) {
|
@ -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 */
|
||||
public static final boolean PRINT_DRIVER = boolProp("controlify.debug.print_driver", true, true);
|
||||
/** 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 */
|
||||
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");
|
||||
|
||||
// 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());
|
||||
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.utils.Log;
|
||||
import io.github.libsdl4j.api.gamecontroller.SDL_GameController;
|
||||
import io.github.libsdl4j.api.joystick.SdlJoystick;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
import static io.github.libsdl4j.api.error.SdlError.*;
|
||||
@ -26,6 +27,9 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
|
||||
|
||||
public SDL2GamepadDriver(int 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.isGyroSupported = SDL_GameControllerHasSensor(ptrGamepad, SDL_SENSOR_GYRO);
|
||||
this.isRumbleSupported = SDL_GameControllerHasRumble(ptrGamepad);
|
||||
|
@ -1,12 +1,12 @@
|
||||
package dev.isxander.controlify.gui.screen;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
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 dev.isxander.controlify.controllermanager.ControllerManager;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
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_FRONT_BAR = new ResourceLocation("boss_bar/green_progress");
|
||||
|
||||
protected final Controlify controlify;
|
||||
protected final ControllerManager controllerManager;
|
||||
protected final Controller<?, ?> controller;
|
||||
private final Supplier<Screen> parent;
|
||||
|
||||
@ -52,6 +54,8 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS
|
||||
|
||||
public ControllerCalibrationScreen(Controller<?, ?> controller, Supplier<Screen> parent) {
|
||||
super(Component.translatable("controlify.calibration.title"));
|
||||
this.controlify = Controlify.instance();
|
||||
this.controllerManager = controlify.getControllerManager().orElseThrow();
|
||||
this.controller = controller;
|
||||
this.parent = parent;
|
||||
this.axisData = new double[controller.axisCount() * CALIBRATION_TIME];
|
||||
@ -129,7 +133,7 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
if (!ControllerManager.isControllerConnected(controller.uid())) {
|
||||
if (!controllerManager.isControllerConnected(controller.uid())) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
@ -161,7 +165,7 @@ public class ControllerCalibrationScreen extends Screen implements DontInteruptS
|
||||
controller.config().delayedCalibration = false;
|
||||
// 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) {
|
||||
controller.config().delayedCalibration = true;
|
||||
Controlify.instance().config().setDirty();
|
||||
Controlify.instance().setCurrentController(null);
|
||||
Controlify.instance().setCurrentController(null, true);
|
||||
}
|
||||
|
||||
onClose();
|
||||
|
@ -2,17 +2,17 @@ package dev.isxander.controlify.gui.screen;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.ControllerManager;
|
||||
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.driver.SDL2NativesManager;
|
||||
import dev.isxander.controlify.controllermanager.ControllerManager;
|
||||
import dev.isxander.controlify.gui.components.FakePositionPlainTextButton;
|
||||
import dev.isxander.controlify.screenop.ScreenControllerEventListener;
|
||||
import dev.isxander.controlify.utils.Animator;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.ComponentPath;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
@ -47,24 +47,27 @@ public class ControllerCarouselScreen extends Screen implements ScreenController
|
||||
private int carouselIndex;
|
||||
private Animator.AnimationInstance carouselAnimation = null;
|
||||
|
||||
private final Controlify controlify;
|
||||
private final ControllerManager controllerManager;
|
||||
|
||||
private Button globalSettingsButton, doneButton;
|
||||
|
||||
private ControllerCarouselScreen(Screen parent) {
|
||||
super(Component.translatable("controlify.gui.carousel.title"));
|
||||
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();
|
||||
|
||||
if (controlify.config().globalSettings().delegateSetup) {
|
||||
controlify.discoverControllers();
|
||||
controlify.config().globalSettings().delegateSetup = false;
|
||||
controlify.config().save();
|
||||
}
|
||||
|
||||
return new ControllerCarouselScreen(parent);
|
||||
controlify.finishControlifyInit().whenComplete((v, th) -> {
|
||||
Minecraft.getInstance().setScreen(new ControllerCarouselScreen(parent));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -109,7 +112,7 @@ public class ControllerCarouselScreen extends Screen implements ScreenController
|
||||
prevSelectedController = null;
|
||||
}
|
||||
|
||||
carouselEntries = ControllerManager.getConnectedControllers().stream()
|
||||
carouselEntries = controllerManager.getConnectedControllers().stream()
|
||||
.map(c -> new CarouselEntry(c, this.width / 3, this.height - 66))
|
||||
.peek(this::addRenderableWidget)
|
||||
.toList();
|
||||
@ -118,7 +121,7 @@ public class ControllerCarouselScreen extends Screen implements ScreenController
|
||||
.findFirst()
|
||||
.map(carouselEntries::indexOf)
|
||||
.orElse(Controlify.instance().getCurrentController()
|
||||
.map(c -> ControllerManager.getConnectedControllers().indexOf(c))
|
||||
.map(c -> controllerManager.getConnectedControllers().indexOf(c))
|
||||
.orElse(0)
|
||||
);
|
||||
if (!carouselEntries.isEmpty())
|
||||
@ -220,7 +223,7 @@ public class ControllerCarouselScreen extends Screen implements ScreenController
|
||||
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.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.prevUse = isCurrentlyUsed();
|
||||
|
@ -1,6 +1,5 @@
|
||||
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;
|
||||
@ -21,10 +20,7 @@ public class ModConfigOpenerScreen extends Screen {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
this.init(minecraft, minecraft.getWindow().getGuiScaledWidth(), minecraft.getWindow().getGuiScaledHeight());
|
||||
|
||||
Controlify.instance().askNatives()
|
||||
.whenComplete((result, error) ->
|
||||
minecraft.setScreen(ControllerCarouselScreen.createConfigScreen(lastScreen))
|
||||
);
|
||||
ControllerCarouselScreen.openConfigScreen(lastScreen);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -2,9 +2,8 @@ package dev.isxander.controlify.mixins.core;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.ControllerManager;
|
||||
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.MouseMinecraftCallNotifier;
|
||||
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"))
|
||||
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"))
|
||||
|
@ -24,10 +24,12 @@ public class ChatComponentMixin {
|
||||
if (!(minecraft.screen instanceof ChatScreen))
|
||||
return;
|
||||
|
||||
Controller<?, ?> controller = Controlify.instance().currentController();
|
||||
graphics.pose().pushPose();
|
||||
if (controller.config().chatKeyboardHeight == 0) return;
|
||||
graphics.pose().translate(0, -controller.config().chatKeyboardHeight * minecraft.getWindow().getGuiScaledHeight(), 0);
|
||||
Controlify.instance().getCurrentController().ifPresent(controller -> {
|
||||
graphics.pose().pushPose();
|
||||
if (controller.config().chatKeyboardHeight == 0) return;
|
||||
graphics.pose().translate(0, -controller.config().chatKeyboardHeight * minecraft.getWindow().getGuiScaledHeight(), 0);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Inject(method = "render", at = @At("TAIL"))
|
||||
|
@ -37,9 +37,6 @@ public class ControlsScreenMixin extends OptionsSubScreen {
|
||||
|
||||
@Unique
|
||||
private void openControllerSettings() {
|
||||
Controlify.instance().askNatives()
|
||||
.whenComplete((result, error) ->
|
||||
minecraft.setScreen(ControllerCarouselScreen.createConfigScreen(lastScreen))
|
||||
);
|
||||
ControllerCarouselScreen.openConfigScreen(this);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ 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.CrashReport;
|
||||
import net.minecraft.CrashReportCategory;
|
||||
import net.minecraft.ReportedException;
|
||||
import net.minecraft.util.Mth;
|
||||
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) {
|
||||
return (value - Math.copySign(Math.min(deadzone, Math.abs(value)), value)) / (1 - deadzone);
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
package dev.isxander.controlify.wireless;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.ControllerManager;
|
||||
import dev.isxander.controlify.controller.BatteryLevel;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controllermanager.ControllerManager;
|
||||
import dev.isxander.controlify.utils.ToastUtils;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
@ -24,7 +24,11 @@ public class LowBatteryNotifier {
|
||||
if (!Controlify.instance().config().globalSettings().notifyLowBattery)
|
||||
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();
|
||||
if (batteryLevel == BatteryLevel.UNKNOWN) {
|
||||
continue;
|
||||
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 281 B After Width: | Height: | Size: 281 B |
Before Width: | Height: | Size: 288 B After Width: | Height: | Size: 288 B |
Before Width: | Height: | Size: 289 B After Width: | Height: | Size: 289 B |
Before Width: | Height: | Size: 298 B After Width: | Height: | Size: 298 B |
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 289 B After Width: | Height: | Size: 289 B |
Before Width: | Height: | Size: 225 B After Width: | Height: | Size: 225 B |
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 361 B |
Before Width: | Height: | Size: 225 B After Width: | Height: | Size: 225 B |
Before Width: | Height: | Size: 241 B After Width: | Height: | Size: 241 B |
Before Width: | Height: | Size: 372 B After Width: | Height: | Size: 372 B |
Before Width: | Height: | Size: 256 B After Width: | Height: | Size: 256 B |
Before Width: | Height: | Size: 297 B After Width: | Height: | Size: 297 B |
Before Width: | Height: | Size: 304 B After Width: | Height: | Size: 304 B |
Before Width: | Height: | Size: 298 B After Width: | Height: | Size: 298 B |
@ -194,11 +194,11 @@ public class FakeController implements JoystickController<JoystickConfig> {
|
||||
}
|
||||
|
||||
public void use() {
|
||||
Controlify.instance().setCurrentController(this);
|
||||
Controlify.instance().setCurrentController(this, true);
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
Controlify.instance().setCurrentController(null);
|
||||
Controlify.instance().setCurrentController(null, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|