1
0
forked from Clones/Controlify
Files
Controlify/src/main/java/dev/isxander/controlify/Controlify.java
2023-04-05 16:36:41 +01:00

400 lines
15 KiB
Java

package dev.isxander.controlify;
import com.mojang.blaze3d.Blaze3D;
import com.mojang.logging.LogUtils;
import dev.isxander.controlify.api.ControlifyApi;
import dev.isxander.controlify.api.entrypoint.ControlifyEntrypoint;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.controller.joystick.CompoundJoystickController;
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
import dev.isxander.controlify.gui.screen.VibrationOnboardingScreen;
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
import dev.isxander.controlify.config.ControlifyConfig;
import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.api.event.ControlifyEvents;
import dev.isxander.controlify.ingame.guide.InGameButtonGuide;
import dev.isxander.controlify.ingame.InGameInputHandler;
import dev.isxander.controlify.mixins.feature.virtualmouse.MouseHandlerAccessor;
import dev.isxander.controlify.utils.ToastUtils;
import dev.isxander.controlify.virtualmouse.VirtualMouseHandler;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import org.jetbrains.annotations.NotNull;
import org.lwjgl.glfw.GLFW;
import org.slf4j.Logger;
import java.util.ArrayDeque;
import java.util.Queue;
public class Controlify implements ControlifyApi {
public static final Logger LOGGER = LogUtils.getLogger();
private static Controlify instance = null;
private final Minecraft minecraft = Minecraft.getInstance();
private Controller<?, ?> currentController = Controller.DUMMY;
private InGameInputHandler inGameInputHandler;
public InGameButtonGuide inGameButtonGuide;
private VirtualMouseHandler virtualMouseHandler;
private InputMode currentInputMode = InputMode.KEYBOARD_MOUSE;
private ControllerHIDService controllerHIDService;
private final ControlifyConfig config = new ControlifyConfig(this);
private final Queue<Controller<?, ?>> calibrationQueue = new ArrayDeque<>();
private int consecutiveInputSwitches = 0;
private double lastInputSwitchTime = 0;
private Controller<?, ?> switchableController = null;
private double askSwitchTime = 0;
private ToastUtils.ControlifyToast askSwitchToast = null;
public void initializeControlify() {
LOGGER.info("Initializing Controlify...");
config().load();
if (!config().globalSettings().vibrationOnboarded) {
minecraft.setScreen(new VibrationOnboardingScreen(
minecraft.screen,
answer -> this.initializeControllers()
));
} else {
this.initializeControllers();
}
}
private void initializeControllers() {
LOGGER.info("Discovering and initializing controllers...");
config().load();
if (config().globalSettings().loadVibrationNatives)
SDL2NativesManager.initialise();
boolean dirtyControllerConfig = false;
// find already connected controllers
for (int jid = 0; jid <= GLFW.GLFW_JOYSTICK_LAST; jid++) {
if (GLFW.glfwJoystickPresent(jid)) {
try {
var controller = Controller.createOrGet(jid, controllerHIDService.fetchType());
LOGGER.info("Controller found: " + controller.name());
config().loadOrCreateControllerData(controller);
if (config().currentControllerUid().equals(controller.uid()))
setCurrentController(controller);
if (controller.config().allowVibrations && !config().globalSettings().loadVibrationNatives) {
controller.config().allowVibrations = false;
dirtyControllerConfig = true;
}
} catch (Exception e) {
LOGGER.error("Failed to initialize controller with jid " + jid, e);
}
}
}
if (dirtyControllerConfig) {
config().save();
}
checkCompoundJoysticks();
if (Controller.CONTROLLERS.isEmpty()) {
LOGGER.info("No controllers found.");
}
if (currentController() == Controller.DUMMY && config().isFirstLaunch()) {
this.setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null));
}
// listen for new controllers
GLFW.glfwSetJoystickCallback((jid, event) -> {
try {
if (event == GLFW.GLFW_CONNECTED) {
this.onControllerHotplugged(jid);
} else if (event == GLFW.GLFW_DISCONNECTED) {
this.onControllerDisconnect(jid);
}
} catch (Exception e) {
e.printStackTrace();
}
});
ClientTickEvents.START_CLIENT_TICK.register(this::tick);
FabricLoader.getInstance().getEntrypoints("controlify", ControlifyEntrypoint.class).forEach(entrypoint -> {
try {
entrypoint.onControllersDiscovered(this);
} catch (Exception e) {
LOGGER.error("Failed to run `onControllersDiscovered` on Controlify entrypoint: " + entrypoint.getClass().getName(), e);
}
});
}
public void preInitialiseControlify() {
LOGGER.info("Pre-initializing Controlify...");
this.inGameInputHandler = new InGameInputHandler(Controller.DUMMY); // initialize with dummy controller before connection in case of no controller
this.virtualMouseHandler = new VirtualMouseHandler();
controllerHIDService = new ControllerHIDService();
controllerHIDService.start();
FabricLoader.getInstance().getEntrypoints("controlify", ControlifyEntrypoint.class).forEach(entrypoint -> {
try {
entrypoint.onControlifyPreInit(this);
} catch (Exception e) {
LOGGER.error("Failed to run `onControlifyPreInit` on Controlify entrypoint: " + entrypoint.getClass().getName(), e);
}
});
}
public void tick(Minecraft client) {
if (minecraft.getOverlay() == null) {
if (!calibrationQueue.isEmpty()) {
Screen screen = minecraft.screen;
while (!calibrationQueue.isEmpty()) {
screen = new ControllerDeadzoneCalibrationScreen(calibrationQueue.poll(), screen);
}
minecraft.setScreen(screen);
}
}
boolean outOfFocus = !config().globalSettings().outOfFocusInput && !client.isWindowActive();
for (var controller : Controller.CONTROLLERS.values()) {
if (!outOfFocus)
controller.updateState();
else {
controller.clearState();
controller.rumbleManager().stopCurrentEffect();
}
controller.rumbleManager().tick();
}
ControllerState state = currentController == null ? ControllerState.EMPTY : currentController.state();
if (switchableController != null && Blaze3D.getTime() - askSwitchTime <= 10000) {
if (switchableController.state().hasAnyInput()) {
this.setCurrentController(switchableController);
if (askSwitchToast != null) {
askSwitchToast.remove();
askSwitchToast = null;
}
switchableController = null;
state = ControllerState.EMPTY;
}
}
if (outOfFocus)
state = ControllerState.EMPTY;
if (state.hasAnyInput())
this.setInputMode(InputMode.CONTROLLER);
if (consecutiveInputSwitches > 500) {
LOGGER.warn("Controlify detected current controller to be constantly giving input and has been disabled.");
ToastUtils.sendToast(
Component.translatable("controlify.toast.faulty_input.title"),
Component.translatable("controlify.toast.faulty_input.description"),
true
);
this.setCurrentController(null);
consecutiveInputSwitches = 0;
}
if (currentController == null) {
this.setInputMode(InputMode.KEYBOARD_MOUSE);
return;
}
if (client.screen != null) {
ScreenProcessorProvider.provide(client.screen).onControllerUpdate(currentController);
}
if (client.level != null) {
this.inGameInputHandler().inputTick();
}
this.virtualMouseHandler().handleControllerInput(currentController);
ControlifyEvents.CONTROLLER_STATE_UPDATED.invoker().onControllerStateUpdate(currentController);
}
public ControlifyConfig config() {
return config;
}
private void onControllerHotplugged(int jid) {
var controller = Controller.createOrGet(jid, controllerHIDService.fetchType());
LOGGER.info("Controller connected: " + controller.name());
config().loadOrCreateControllerData(currentController);
this.askToSwitchController(controller);
checkCompoundJoysticks();
}
private void onControllerDisconnect(int jid) {
Controller.CONTROLLERS.values().stream().filter(controller -> controller.joystickId() == jid).findAny().ifPresent(controller -> {
Controller.remove(controller);
setCurrentController(Controller.CONTROLLERS.values().stream().findFirst().orElse(null));
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
);
});
checkCompoundJoysticks();
}
private void checkCompoundJoysticks() {
config().getCompoundJoysticks().values().forEach(info -> {
try {
if (info.isLoaded() && !info.canBeUsed()) {
LOGGER.warn("Unloading compound joystick " + info.friendlyName() + " due to missing controllers.");
Controller.CONTROLLERS.remove(info.type().identifier());
}
if (!info.isLoaded() && info.canBeUsed()) {
LOGGER.info("Loading compound joystick " + info.type().identifier() + ".");
CompoundJoystickController controller = info.attemptCreate().orElseThrow();
Controller.CONTROLLERS.put(info.type().identifier(), controller);
config().loadOrCreateControllerData(controller);
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
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
public @NotNull Controller<?, ?> currentController() {
if (currentController == null)
return Controller.DUMMY;
return currentController;
}
public void setCurrentController(Controller<?, ?> controller) {
if (controller == null)
controller = Controller.DUMMY;
if (this.currentController == controller) return;
this.currentController = controller;
if (switchableController == controller) {
switchableController = null;
}
LOGGER.info("Updated current controller to " + controller.name() + "(" + controller.uid() + ")");
if (!config().currentControllerUid().equals(controller.uid())) {
config().save();
}
this.inGameInputHandler = new InGameInputHandler(controller);
if (Minecraft.getInstance().player != null) {
this.inGameButtonGuide = new InGameButtonGuide(controller, Minecraft.getInstance().player);
}
if (!controller.config().calibrated && controller != Controller.DUMMY)
calibrationQueue.add(controller);
}
public InGameInputHandler inGameInputHandler() {
return inGameInputHandler;
}
public InGameButtonGuide inGameButtonGuide() {
return inGameButtonGuide;
}
public VirtualMouseHandler virtualMouseHandler() {
return virtualMouseHandler;
}
public ControllerHIDService controllerHIDService() {
return controllerHIDService;
}
public @NotNull InputMode currentInputMode() {
return currentInputMode;
}
@Override
public void setInputMode(@NotNull InputMode currentInputMode) {
if (this.currentInputMode == currentInputMode) return;
this.currentInputMode = currentInputMode;
var minecraft = Minecraft.getInstance();
if (!minecraft.mouseHandler.isMouseGrabbed())
hideMouse(currentInputMode == InputMode.CONTROLLER, true);
if (minecraft.screen != null) {
ScreenProcessorProvider.provide(minecraft.screen).onInputModeChanged(currentInputMode);
}
if (Minecraft.getInstance().player != null) {
if (currentInputMode == InputMode.KEYBOARD_MOUSE)
this.inGameButtonGuide = null;
else
this.inGameButtonGuide = new InGameButtonGuide(this.currentController != null ? currentController : Controller.DUMMY, Minecraft.getInstance().player);
}
if (Blaze3D.getTime() - lastInputSwitchTime < 20) {
consecutiveInputSwitches++;
} else {
consecutiveInputSwitches = 0;
}
lastInputSwitchTime = Blaze3D.getTime();
ControlifyEvents.INPUT_MODE_CHANGED.invoker().onInputModeChanged(currentInputMode);
}
public void hideMouse(boolean hide, boolean moveMouse) {
var minecraft = Minecraft.getInstance();
GLFW.glfwSetInputMode(
minecraft.getWindow().getWindow(),
GLFW.GLFW_CURSOR,
hide
? GLFW.GLFW_CURSOR_HIDDEN
: GLFW.GLFW_CURSOR_NORMAL
);
if (minecraft.screen != null) {
var mouseHandlerAccessor = (MouseHandlerAccessor) minecraft.mouseHandler;
if (hide && !virtualMouseHandler().isVirtualMouseEnabled() && moveMouse) {
// stop mouse hovering over last element before hiding cursor but don't actually move it
// so when the user switches back to mouse it will be in the same place
mouseHandlerAccessor.invokeOnMove(minecraft.getWindow().getWindow(), -50, -50);
}
}
}
public static Controlify instance() {
if (instance == null) instance = new Controlify();
return instance;
}
}