1
0
forked from Clones/Controlify

[BROKEN] Abstract controller manager system

This commit is contained in:
isXander
2023-11-03 17:17:50 +00:00
parent 9960fedad1
commit b07066e097
39 changed files with 531 additions and 286 deletions

View File

@ -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() {

View File

@ -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);

View File

@ -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();

View File

@ -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<>();

View File

@ -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)");
}

View File

@ -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() {

View File

@ -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();

View File

@ -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()) {

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);

View File

@ -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());
}

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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

View File

@ -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"))

View File

@ -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"))

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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