From 71c7e2658710d30a2156f6d6e677e9dc8a6b768f Mon Sep 17 00:00:00 2001 From: isXander Date: Thu, 11 May 2023 16:43:13 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9E=95=20Battery=20level=20warning=20+=20upd?= =?UTF-8?q?ate=20SDL=20with=20macOS=20ARM=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/isxander/controlify/Controlify.java | 21 ++++---- .../controlify/config/GlobalSettings.java | 1 + .../config/gui/ModMenuIntegration.java | 2 +- .../controlify/config/gui/YACLHelper.java | 23 ++++++++- .../controlify/controller/BatteryLevel.java | 21 ++++++++ .../controlify/controller/Controller.java | 17 ++----- .../controlify/controller/ControllerType.java | 4 ++ .../controller/gamepad/GamepadController.java | 6 +++ .../controller/hid/ControllerHIDService.java | 30 ++++++++++- .../controller/sdl2/SDL2NativesManager.java | 26 +++++++--- .../controlify/driver/BatteryDriver.java | 25 ++++++++++ .../controlify/driver/GamepadDrivers.java | 16 +++--- .../controlify/driver/SDL2GamepadDriver.java | 22 +++++++- .../gui/screen/SDLOnboardingScreen.java | 36 +++++++++++++ .../gui/screen/VibrationOnboardingScreen.java | 25 ---------- .../settingsbutton/ControlsScreenMixin.java | 2 +- .../wireless/LowBatteryNotifier.java | 50 +++++++++++++++++++ .../assets/controlify/lang/en_us.json | 22 ++++++-- src/main/resources/controlify.mixins.json | 3 ++ 19 files changed, 283 insertions(+), 69 deletions(-) create mode 100644 src/main/java/dev/isxander/controlify/controller/BatteryLevel.java create mode 100644 src/main/java/dev/isxander/controlify/driver/BatteryDriver.java create mode 100644 src/main/java/dev/isxander/controlify/gui/screen/SDLOnboardingScreen.java delete mode 100644 src/main/java/dev/isxander/controlify/gui/screen/VibrationOnboardingScreen.java create mode 100644 src/main/java/dev/isxander/controlify/wireless/LowBatteryNotifier.java diff --git a/src/main/java/dev/isxander/controlify/Controlify.java b/src/main/java/dev/isxander/controlify/Controlify.java index 4b0893a..a0349c6 100644 --- a/src/main/java/dev/isxander/controlify/Controlify.java +++ b/src/main/java/dev/isxander/controlify/Controlify.java @@ -22,6 +22,7 @@ import dev.isxander.controlify.sound.ControlifySounds; import dev.isxander.controlify.utils.DebugLog; import dev.isxander.controlify.utils.ToastUtils; import dev.isxander.controlify.virtualmouse.VirtualMouseHandler; +import dev.isxander.controlify.wireless.LowBatteryNotifier; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.resource.ResourceManagerHelper; import net.fabricmc.fabric.api.resource.ResourcePackActivationType; @@ -57,7 +58,7 @@ public class Controlify implements ControlifyApi { private InputMode currentInputMode = InputMode.KEYBOARD_MOUSE; private ControllerHIDService controllerHIDService; - private CompletableFuture vibrationOnboardingFuture = null; + private CompletableFuture nativeOnboardingFuture = null; private final ControlifyConfig config = new ControlifyConfig(this); @@ -77,13 +78,13 @@ public class Controlify implements ControlifyApi { var controllersConnected = IntStream.range(0, GLFW.GLFW_JOYSTICK_LAST + 1).anyMatch(GLFW::glfwJoystickPresent); if (controllersConnected) { - askVibrationNatives().whenComplete((loaded, th) -> discoverControllers()); + askNatives().whenComplete((loaded, th) -> discoverControllers()); } // listen for new controllers GLFW.glfwSetJoystickCallback((jid, event) -> { try { - this.askVibrationNatives().whenComplete((loaded, th) -> { + this.askNatives().whenComplete((loaded, th) -> { if (event == GLFW.GLFW_CONNECTED) { this.onControllerHotplugged(jid); } else if (event == GLFW.GLFW_DISCONNECTED) { @@ -96,25 +97,25 @@ public class Controlify implements ControlifyApi { }); } - private CompletableFuture askVibrationNatives() { - if (vibrationOnboardingFuture != null) return vibrationOnboardingFuture; + private CompletableFuture askNatives() { + if (nativeOnboardingFuture != null) return nativeOnboardingFuture; if (config().globalSettings().vibrationOnboarded) { return CompletableFuture.completedFuture(config().globalSettings().loadVibrationNatives); } - vibrationOnboardingFuture = new CompletableFuture<>(); + nativeOnboardingFuture = new CompletableFuture<>(); - minecraft.setScreen(new VibrationOnboardingScreen( + minecraft.setScreen(new SDLOnboardingScreen( minecraft.screen, answer -> { if (answer) SDL2NativesManager.initialise(); - vibrationOnboardingFuture.complete(answer); + nativeOnboardingFuture.complete(answer); } )); - return vibrationOnboardingFuture; + return nativeOnboardingFuture; } private void discoverControllers() { @@ -229,6 +230,8 @@ public class Controlify implements ControlifyApi { } } + LowBatteryNotifier.tick(); + getCurrentController().ifPresent(currentController -> { wrapControllerError( () -> tickController(currentController, outOfFocus), diff --git a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java index a45d488..8ea623c 100644 --- a/src/main/java/dev/isxander/controlify/config/GlobalSettings.java +++ b/src/main/java/dev/isxander/controlify/config/GlobalSettings.java @@ -19,4 +19,5 @@ public class GlobalSettings { public boolean vibrationOnboarded = false; public ReachAroundMode reachAround = ReachAroundMode.OFF; public boolean uiSounds = false; + public boolean notifyLowBattery = true; } diff --git a/src/main/java/dev/isxander/controlify/config/gui/ModMenuIntegration.java b/src/main/java/dev/isxander/controlify/config/gui/ModMenuIntegration.java index 2b8f762..262be5c 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/ModMenuIntegration.java +++ b/src/main/java/dev/isxander/controlify/config/gui/ModMenuIntegration.java @@ -6,6 +6,6 @@ import com.terraformersmc.modmenu.api.ModMenuApi; public class ModMenuIntegration implements ModMenuApi { @Override public ConfigScreenFactory getModConfigScreenFactory() { - return YACLHelper::generateConfigScreen; + return YACLHelper::openConfigScreen; } } diff --git a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java index 59d5a0c..2545d63 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java +++ b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java @@ -46,7 +46,18 @@ public class YACLHelper { private static final Function percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100)); private static final Function percentOrOffFormatter = v -> v == 0 ? CommonComponents.OPTION_OFF : percentFormatter.apply(v); - public static Screen generateConfigScreen(Screen parent) { + public static Screen openConfigScreen(Screen parent) { + Screen configScreen = generateConfigScreen(parent); + if (!Controlify.instance().config().globalSettings().vibrationOnboarded) + configScreen = new SDLOnboardingScreen(configScreen, yes -> { + if (yes) { + SDL2NativesManager.initialise(); + } + }); + return configScreen; + } + + private static Screen generateConfigScreen(Screen parent) { var controlify = Controlify.instance(); var yacl = YetAnotherConfigLib.createBuilder() @@ -86,6 +97,12 @@ public class YACLHelper { .binding(GlobalSettings.DEFAULT.uiSounds, () -> globalSettings.uiSounds, v -> globalSettings.uiSounds = v) .controller(TickBoxController::new) .build()) + .option(Option.createBuilder(boolean.class) + .name(Component.translatable("controlify.gui.notify_low_battery")) + .tooltip(Component.translatable("controlify.gui.notify_low_battery.tooltip")) + .binding(GlobalSettings.DEFAULT.notifyLowBattery, () -> globalSettings.notifyLowBattery, v -> globalSettings.notifyLowBattery = v) + .controller(TickBoxController::new) + .build()) .option(Option.createBuilder(boolean.class) .name(Component.translatable("controlify.gui.out_of_focus_input")) .tooltip(Component.translatable("controlify.gui.out_of_focus_input.tooltip")) @@ -126,6 +143,10 @@ public class YACLHelper { category.name(Component.literal(controller.name())); + if (controller.batteryLevel() != BatteryLevel.UNKNOWN) { + category.option(LabelOption.create(Component.translatable("controlify.gui.battery_level", controller.batteryLevel().getFriendlyName()))); + } + var config = controller.config(); var def = controller.defaultConfig(); diff --git a/src/main/java/dev/isxander/controlify/controller/BatteryLevel.java b/src/main/java/dev/isxander/controlify/controller/BatteryLevel.java new file mode 100644 index 0000000..7a3a4dc --- /dev/null +++ b/src/main/java/dev/isxander/controlify/controller/BatteryLevel.java @@ -0,0 +1,21 @@ +package dev.isxander.controlify.controller; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public enum BatteryLevel { + EMPTY, LOW, MEDIUM, FULL, MAX, + WIRED, UNKNOWN; + + public MutableComponent getFriendlyName() { + return Component.translatable("controlify.battery_level." + name().toLowerCase()).withStyle( + switch (this) { + case EMPTY, LOW -> ChatFormatting.RED; + case MEDIUM -> ChatFormatting.YELLOW; + case FULL, MAX -> ChatFormatting.GREEN; + default -> ChatFormatting.WHITE; + } + ); + } +} diff --git a/src/main/java/dev/isxander/controlify/controller/Controller.java b/src/main/java/dev/isxander/controlify/controller/Controller.java index 1cef661..39773f2 100644 --- a/src/main/java/dev/isxander/controlify/controller/Controller.java +++ b/src/main/java/dev/isxander/controlify/controller/Controller.java @@ -2,24 +2,11 @@ package dev.isxander.controlify.controller; import com.google.gson.Gson; import com.google.gson.JsonElement; -import dev.isxander.controlify.Controlify; import dev.isxander.controlify.bindings.ControllerBindings; -import dev.isxander.controlify.controller.gamepad.GamepadController; import dev.isxander.controlify.controller.hid.ControllerHIDService; -import dev.isxander.controlify.controller.joystick.SingleJoystickController; -import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.rumble.RumbleCapable; import dev.isxander.controlify.rumble.RumbleManager; import dev.isxander.controlify.rumble.RumbleSource; -import dev.isxander.controlify.utils.DebugLog; -import net.minecraft.CrashReport; -import net.minecraft.CrashReportCategory; -import net.minecraft.ReportedException; -import org.hid4java.HidDevice; -import org.lwjgl.glfw.GLFW; - -import java.util.HashMap; -import java.util.Map; import java.util.Optional; public interface Controller { @@ -48,6 +35,10 @@ public interface Controller hidInfo(); default boolean canBeUsed() { diff --git a/src/main/java/dev/isxander/controlify/controller/ControllerType.java b/src/main/java/dev/isxander/controlify/controller/ControllerType.java index e03814d..9af3cf1 100644 --- a/src/main/java/dev/isxander/controlify/controller/ControllerType.java +++ b/src/main/java/dev/isxander/controlify/controller/ControllerType.java @@ -17,6 +17,10 @@ public record ControllerType(String friendlyName, String mappingId, String theme private static Map typeMap = null; private static final ResourceLocation hidDbLocation = new ResourceLocation("controlify", "controllers/controller_identification.json5"); + public static ControllerType getTypeForHID(HIDIdentifier hid) { + return getTypeMap().getOrDefault(hid, ControllerType.UNKNOWN); + } + public static void ensureTypeMapFilled() { if (typeMap != null) return; diff --git a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java index cba4861..3288056 100644 --- a/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java +++ b/src/main/java/dev/isxander/controlify/controller/gamepad/GamepadController.java @@ -2,6 +2,7 @@ package dev.isxander.controlify.controller.gamepad; import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.controller.AbstractController; +import dev.isxander.controlify.controller.BatteryLevel; import dev.isxander.controlify.controller.hid.ControllerHIDService; import dev.isxander.controlify.driver.*; import dev.isxander.controlify.rumble.RumbleManager; @@ -103,6 +104,11 @@ public class GamepadController extends AbstractController> unconsumedControllerHIDs; private final Map attachedDevices = new HashMap<>(); private boolean disabled = false; + private boolean firstFetch = true; // https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/hid-usages#usage-page private static final Set CONTROLLER_USAGE_IDS = Set.of( 0x04, // Joystick @@ -39,7 +43,29 @@ public class ControllerHIDService { } } - public ControllerHIDInfo fetchType() { + public ControllerHIDInfo fetchType(int jid) { + if (firstFetch) { + firstFetch = false; + if (isDisabled() && !SDL2NativesManager.isLoaded()) { + if (Controlify.instance().controllerHIDService().isDisabled() && !SDL2NativesManager.isLoaded()) { + ToastUtils.sendToast( + Component.translatable("controlify.error.hid"), + Component.translatable("controlify.error.hid.desc"), + true + ); + } + } + } + +// if (SDL2NativesManager.isLoaded()) { +// int vid = SDL.SDL_JoystickGetDeviceVendor(jid); +// int pid = SDL.SDL_JoystickGetDeviceProduct(jid); +// +// if (vid != 0 && pid != 0) { +// return new ControllerHIDInfo(ControllerType.getTypeForHID(new HIDIdentifier(vid, pid)), Optional.empty()); +// } +// } + if (disabled) { return new ControllerHIDInfo(ControllerType.UNKNOWN, Optional.empty()); } @@ -52,7 +78,7 @@ public class ControllerHIDService { return new ControllerHIDInfo(ControllerType.UNKNOWN, Optional.empty()); } - ControllerType type = ControllerType.getTypeMap().getOrDefault(hid.getSecond(), ControllerType.UNKNOWN); + ControllerType type = ControllerType.getTypeForHID(hid.getSecond()); if (type == ControllerType.UNKNOWN) Controlify.LOGGER.warn("Controller found via USB hardware scan, but it was not found in the controller identification database! (HID: {})", hid.getSecond()); diff --git a/src/main/java/dev/isxander/controlify/controller/sdl2/SDL2NativesManager.java b/src/main/java/dev/isxander/controlify/controller/sdl2/SDL2NativesManager.java index 2c78b91..63b208b 100644 --- a/src/main/java/dev/isxander/controlify/controller/sdl2/SDL2NativesManager.java +++ b/src/main/java/dev/isxander/controlify/controller/sdl2/SDL2NativesManager.java @@ -6,6 +6,7 @@ import net.fabricmc.loader.api.FabricLoader; import net.minecraft.Util; import org.libsdl.SDL; +import java.io.File; import java.io.FileOutputStream; import java.net.URL; import java.nio.channels.Channels; @@ -13,6 +14,7 @@ import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Comparator; import java.util.Map; import static org.libsdl.SDL_Hints.*; @@ -20,10 +22,11 @@ import static org.libsdl.SDL_Hints.*; public class SDL2NativesManager { private static final String SDL2_VERSION = ""; private static final Map NATIVE_LIBRARIES = Map.of( - new Target(Util.OS.WINDOWS, true), "windows64.dll", - new Target(Util.OS.WINDOWS, false), "window32.dll", - new Target(Util.OS.LINUX, true), "linux64.so" - //new Target(Util.OS.OSX, true), "mac64.dylib" + new Target(Util.OS.WINDOWS, true, false), "windows64.dll", + new Target(Util.OS.WINDOWS, false, false), "window32.dll", + new Target(Util.OS.LINUX, true, false), "linux64.so", + new Target(Util.OS.OSX, true, false), "macosx64.dylib", + new Target(Util.OS.OSX, true, true), "macosxarm64.dylib" ); private static final String NATIVE_LIBRARY_URL = "https://maven.isxander.dev/releases/dev/isxander/sdl2-jni-natives/%s/".formatted(SDL2_VERSION); @@ -41,6 +44,16 @@ public class SDL2NativesManager { Path localLibraryPath = Target.CURRENT.getLocalNativePath(); if (Files.notExists(localLibraryPath)) { + if (Files.exists(localLibraryPath.getParent())) { + try(var walk = Files.walk(localLibraryPath.getParent())) { + walk.sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEachOrdered(File::delete); + } catch (Exception e) { + Controlify.LOGGER.error("Failed to delete old SDL2 native library", e); + } + } + Controlify.LOGGER.info("Downloading SDL2 native library: " + Target.CURRENT.getArtifactName()); downloadLibrary(localLibraryPath); } @@ -110,12 +123,13 @@ public class SDL2NativesManager { return loaded; } - private record Target(Util.OS os, boolean is64Bit) { + private record Target(Util.OS os, boolean is64Bit, boolean isARM) { public static final Target CURRENT = Util.make(() -> { Util.OS os = Util.getPlatform(); boolean is64bit = System.getProperty("os.arch").contains("64"); + boolean isARM = System.getProperty("os.arch").contains("arm"); - return new Target(os, is64bit); + return new Target(os, is64bit, isARM); }); public boolean hasNativeLibrary() { diff --git a/src/main/java/dev/isxander/controlify/driver/BatteryDriver.java b/src/main/java/dev/isxander/controlify/driver/BatteryDriver.java new file mode 100644 index 0000000..df08bcb --- /dev/null +++ b/src/main/java/dev/isxander/controlify/driver/BatteryDriver.java @@ -0,0 +1,25 @@ +package dev.isxander.controlify.driver; + +import dev.isxander.controlify.controller.BatteryLevel; + +public interface BatteryDriver extends Driver { + BatteryLevel getBatteryLevel(); + + String getBatteryDriverDetails(); + + BatteryDriver UNSUPPORTED = new BatteryDriver() { + @Override + public void update() { + } + + @Override + public BatteryLevel getBatteryLevel() { + return BatteryLevel.UNKNOWN; + } + + @Override + public String getBatteryDriverDetails() { + return "Unsupported"; + } + }; +} diff --git a/src/main/java/dev/isxander/controlify/driver/GamepadDrivers.java b/src/main/java/dev/isxander/controlify/driver/GamepadDrivers.java index babc224..01237ab 100644 --- a/src/main/java/dev/isxander/controlify/driver/GamepadDrivers.java +++ b/src/main/java/dev/isxander/controlify/driver/GamepadDrivers.java @@ -1,5 +1,6 @@ package dev.isxander.controlify.driver; +import com.google.common.collect.Sets; import dev.isxander.controlify.Controlify; import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; import dev.isxander.controlify.debug.DebugProperties; @@ -7,19 +8,20 @@ import org.hid4java.HidDevice; import java.util.*; -public record GamepadDrivers(BasicGamepadInputDriver basicGamepadInputDriver, GyroDriver gyroDriver, RumbleDriver rumbleDriver) { +public record GamepadDrivers(BasicGamepadInputDriver basicGamepadInputDriver, GyroDriver gyroDriver, RumbleDriver rumbleDriver, BatteryDriver batteryDriver) { public Set getUniqueDrivers() { - Set drivers = Collections.newSetFromMap(new IdentityHashMap<>()); - drivers.addAll(List.of(basicGamepadInputDriver, gyroDriver, rumbleDriver)); + Set drivers = Sets.newIdentityHashSet(); + drivers.addAll(List.of(basicGamepadInputDriver, gyroDriver, rumbleDriver, batteryDriver)); return drivers; } public void printDrivers() { if (DebugProperties.PRINT_DRIVER) { - Controlify.LOGGER.info("Drivers in use: Basic Input = '{}', Gyro = '{}', Rumble = '{}'", + Controlify.LOGGER.info("Drivers in use: Basic Input = '{}', Gyro = '{}', Rumble = '{}', Battery = '{}'", basicGamepadInputDriver.getBasicGamepadDetails(), gyroDriver.getGyroDetails(), - rumbleDriver.getRumbleDetails() + rumbleDriver.getRumbleDetails(), + batteryDriver.getBatteryDriverDetails() ); } } @@ -28,11 +30,13 @@ public record GamepadDrivers(BasicGamepadInputDriver basicGamepadInputDriver, Gy BasicGamepadInputDriver basicGamepadInputDriver = new GLFWGamepadDriver(jid); GyroDriver gyroDriver = GyroDriver.UNSUPPORTED; RumbleDriver rumbleDriver = RumbleDriver.UNSUPPORTED; + BatteryDriver batteryDriver = BatteryDriver.UNSUPPORTED; if (SDL2NativesManager.isLoaded()) { SDL2GamepadDriver sdl2Driver = new SDL2GamepadDriver(jid); gyroDriver = sdl2Driver; rumbleDriver = sdl2Driver; + batteryDriver = sdl2Driver; } // broken @@ -40,6 +44,6 @@ public record GamepadDrivers(BasicGamepadInputDriver basicGamepadInputDriver, Gy gyroDriver = new SteamDeckDriver(hid.get()); } - return new GamepadDrivers(basicGamepadInputDriver, gyroDriver, rumbleDriver); + return new GamepadDrivers(basicGamepadInputDriver, gyroDriver, rumbleDriver, batteryDriver); } } diff --git a/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java b/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java index be69983..c3d5592 100644 --- a/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java +++ b/src/main/java/dev/isxander/controlify/driver/SDL2GamepadDriver.java @@ -1,11 +1,12 @@ package dev.isxander.controlify.driver; import dev.isxander.controlify.Controlify; +import dev.isxander.controlify.controller.BatteryLevel; import dev.isxander.controlify.controller.gamepad.GamepadState; import dev.isxander.controlify.debug.DebugProperties; import org.libsdl.SDL; -public class SDL2GamepadDriver implements GyroDriver, RumbleDriver { +public class SDL2GamepadDriver implements GyroDriver, RumbleDriver, BatteryDriver { private final long ptrGamepad; private GamepadState.GyroState gyroDelta; private final boolean isGyroSupported, isRumbleSupported; @@ -42,6 +43,20 @@ public class SDL2GamepadDriver implements GyroDriver, RumbleDriver { return gyroDelta; } + @Override + public BatteryLevel getBatteryLevel() { + return switch (SDL.SDL_JoystickCurrentPowerLevel(ptrGamepad)) { + case SDL.SDL_JOYSTICK_POWER_UNKNOWN -> BatteryLevel.UNKNOWN; + case SDL.SDL_JOYSTICK_POWER_EMPTY -> BatteryLevel.EMPTY; + case SDL.SDL_JOYSTICK_POWER_LOW -> BatteryLevel.LOW; + case SDL.SDL_JOYSTICK_POWER_MEDIUM -> BatteryLevel.MEDIUM; + case SDL.SDL_JOYSTICK_POWER_FULL -> BatteryLevel.FULL; + case SDL.SDL_JOYSTICK_POWER_WIRED -> BatteryLevel.WIRED; + case SDL.SDL_JOYSTICK_POWER_MAX -> BatteryLevel.MAX; + default -> throw new IllegalStateException("Unexpected value: " + SDL.SDL_JoystickCurrentPowerLevel(ptrGamepad)); + }; + } + @Override public boolean isGyroSupported() { return isGyroSupported; @@ -66,4 +81,9 @@ public class SDL2GamepadDriver implements GyroDriver, RumbleDriver { public String getRumbleDetails() { return "SDL2gp supported=" + isRumbleSupported(); } + + @Override + public String getBatteryDriverDetails() { + return "SDL2gp"; + } } diff --git a/src/main/java/dev/isxander/controlify/gui/screen/SDLOnboardingScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/SDLOnboardingScreen.java new file mode 100644 index 0000000..96f8a76 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/gui/screen/SDLOnboardingScreen.java @@ -0,0 +1,36 @@ +package dev.isxander.controlify.gui.screen; + +import dev.isxander.controlify.Controlify; +import it.unimi.dsi.fastutil.booleans.BooleanConsumer; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class SDLOnboardingScreen extends ConfirmScreen { + public SDLOnboardingScreen(Screen lastScreen, BooleanConsumer onAnswered) { + super( + yes -> { + Controlify.instance().config().globalSettings().loadVibrationNatives = yes; + Controlify.instance().config().globalSettings().vibrationOnboarded = true; + Controlify.instance().config().save(); + Minecraft.getInstance().setScreen(lastScreen); + onAnswered.accept(yes); + }, + Component.translatable("controlify.sdl2_onboarding.title").withStyle(ChatFormatting.BOLD), + Util.make(() -> { + var message = Component.translatable("controlify.sdl2_onboarding.message"); + +// if (Util.getPlatform() == Util.OS.OSX) { +// message.append("\n").append(Component.translatable("controlify.sdl2_onboarding.message_mac").withStyle(ChatFormatting.RED)); +// } + + message.append("\n\n").append(Component.translatable("controlify.sdl2_onboarding.question")); + + return message; + }) + ); + } +} diff --git a/src/main/java/dev/isxander/controlify/gui/screen/VibrationOnboardingScreen.java b/src/main/java/dev/isxander/controlify/gui/screen/VibrationOnboardingScreen.java deleted file mode 100644 index c68434c..0000000 --- a/src/main/java/dev/isxander/controlify/gui/screen/VibrationOnboardingScreen.java +++ /dev/null @@ -1,25 +0,0 @@ -package dev.isxander.controlify.gui.screen; - -import dev.isxander.controlify.Controlify; -import it.unimi.dsi.fastutil.booleans.BooleanConsumer; -import net.minecraft.ChatFormatting; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.ConfirmScreen; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; - -public class VibrationOnboardingScreen extends ConfirmScreen { - public VibrationOnboardingScreen(Screen lastScreen, BooleanConsumer onAnswered) { - super( - yes -> { - Controlify.instance().config().globalSettings().loadVibrationNatives = yes; - Controlify.instance().config().globalSettings().vibrationOnboarded = true; - Controlify.instance().config().save(); - Minecraft.getInstance().setScreen(lastScreen); - onAnswered.accept(yes); - }, - Component.translatable("controlify.vibration_onboarding.title").withStyle(ChatFormatting.BOLD), - Component.translatable("controlify.vibration_onboarding.message") - ); - } -} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java index 14e5560..11f5af0 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/settingsbutton/ControlsScreenMixin.java @@ -22,7 +22,7 @@ public class ControlsScreenMixin extends OptionsSubScreen { @Inject(method = "init", at = @At("RETURN")) private void addControllerSettings(CallbackInfo ci, @Local(ordinal = 0) int leftX, @Local(ordinal = 1) int rightX, @Local(ordinal = 2) int currentY) { - addRenderableWidget(Button.builder(Component.translatable("controlify.gui.button"), btn -> minecraft.setScreen(YACLHelper.generateConfigScreen(this))) + addRenderableWidget(Button.builder(Component.translatable("controlify.gui.button"), btn -> minecraft.setScreen(YACLHelper.openConfigScreen(this))) .pos(leftX, currentY) .width(150) .build()); diff --git a/src/main/java/dev/isxander/controlify/wireless/LowBatteryNotifier.java b/src/main/java/dev/isxander/controlify/wireless/LowBatteryNotifier.java new file mode 100644 index 0000000..5acf108 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/wireless/LowBatteryNotifier.java @@ -0,0 +1,50 @@ +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.utils.ToastUtils; +import net.minecraft.network.chat.Component; + +import java.util.HashMap; +import java.util.Map; + +public class LowBatteryNotifier { + private static final Map previousBatteryLevels = new HashMap<>(); + private static int interval; + + public static void tick() { + if (interval > 0) { + interval--; + return; + } + interval = 20 * 60; // 1 minute + + if (!Controlify.instance().config().globalSettings().notifyLowBattery) + return; + + for (Controller controller : ControllerManager.getConnectedControllers()) { + BatteryLevel batteryLevel = controller.batteryLevel(); + if (batteryLevel == BatteryLevel.UNKNOWN) { + continue; + } + + String uid = controller.uid(); + if (previousBatteryLevels.containsKey(uid)) { + BatteryLevel previousBatteryLevel = previousBatteryLevels.get(uid); + if (batteryLevel.ordinal() < previousBatteryLevel.ordinal()) { + if (batteryLevel == BatteryLevel.LOW) { + ToastUtils.sendToast( + Component.translatable("controlify.toast.low_battery.title"), + Component.translatable("controlify.toast.low_battery.message", controller.name()), + true + ); + } + } + } + + previousBatteryLevels.put(uid, batteryLevel); + } + } +} diff --git a/src/main/resources/assets/controlify/lang/en_us.json b/src/main/resources/assets/controlify/lang/en_us.json index a71aee7..10a42c7 100644 --- a/src/main/resources/assets/controlify/lang/en_us.json +++ b/src/main/resources/assets/controlify/lang/en_us.json @@ -2,8 +2,8 @@ "controlify.gui.category.global": "Global", "controlify.gui.current_controller": "Current Controller", "controlify.gui.current_controller.tooltip": "In Controlify's infancy, only one controller can be used at a time, this selects which one you want to use.", - "controlify.gui.load_vibration_natives": "Vibration Support", - "controlify.gui.load_vibration_natives.tooltip": "If enabled, Controlify will download and load native libraries on launch to enable vibration support. The download process only happens once and only downloads for your specific OS. Disabling this will not delete the natives, it just won't load them.", + "controlify.gui.load_vibration_natives": "Load Natives", + "controlify.gui.load_vibration_natives.tooltip": "If enabled, Controlify will download and load native libraries on launch to enable support for enhanced features such as vibration and gyro. The download process only happens once and only downloads for your specific OS. Disabling this will not delete the natives, it just won't load them.", "controlify.gui.load_vibration_natives.tooltip.warning": "You must enable vibration support per-controller as well as this setting.", "controlify.gui.reach_around": "Block Reach Around", "controlify.gui.reach_around.tooltip": "If enabled, you can interact with the block you are standing on in the direction you are looking.", @@ -15,12 +15,15 @@ "controlify.reach_around.everywhere": "Everywhere", "controlify.gui.ui_sounds": "UI Sounds", "controlify.gui.ui_sounds.tooltip": "If enabled, Controlify will play UI sounds when you interact with the UI, like in legacy console editions of Minecraft.", + "controlify.gui.notify_low_battery": "Notify Low Battery", + "controlify.gui.notify_low_battery.tooltip": "A toast will appear when your wireless controller's battery becomes low. (EXPERIMENTAL)", "controlify.gui.out_of_focus_input": "Out of Focus Input", "controlify.gui.out_of_focus_input.tooltip": "If enabled, Controlify will still receive input even if the game window is not focused.", "controlify.gui.keyboard_movement": "Keyboard-like Movement", "controlify.gui.keyboard_movement.tooltip": "Makes movement either on or off rather than being smooth with a thumbstick, this may help in cases where server anti-cheats are harsh.", "controlify.gui.open_issue_tracker": "Open Issue Tracker", + "controlify.gui.battery_level": "Your controller battery is currently %s.", "controlify.gui.group.basic": "Basic", "controlify.gui.group.basic.tooltip": "Adjust how your controller behaves.", "controlify.gui.horizontal_look_sensitivity": "Horizontal Look Sensitivity", @@ -121,9 +124,13 @@ "controlify.toast.controller_disconnected.description": "'%s' was disconnected.", "controlify.toast.faulty_input.title": "Controller disabled", "controlify.toast.faulty_input.description": "The controller was found to conflict with the keyboard and mouse and is now disabled. Increase deadzone values or check joystick mapping to fix.", + "controlify.toast.low_battery.title": "Low Battery", + "controlify.toast.low_battery.message": "Your controller '%s' is low on battery. Please charge it soon.", - "controlify.vibration_onboarding.title": "Controlify Extra Features", - "controlify.vibration_onboarding.message": "To enable vibration and gyro support, a native library must be downloaded that Controlify loads automatically. This is a seamless process and will only take a few seconds. If you choose no, you may change your mind later in Controlify settings, but you won't have access to these important features.\n\nWould you like to download them?", + "controlify.sdl2_onboarding.title": "Controlify Native Library", + "controlify.sdl2_onboarding.message": "Many features in Controlify require an extra library that needs to be downloaded for your system. If you do not download this library, you will lose access to many features such as: controller vibration, gyroscope control, better controller identification. This is a seamless process and will only take a few seconds. If you choose no, you may change your mind later in Controlify settings, but you won't have access to these features in the meantime.", + "controlify.sdl2_onboarding.message_mac": "Because you are on macOS, this library is required for any sort of controller identification. Without it, all controllers will be unidentified. This will severely impact on user experience if you choose not to download them.", + "controlify.sdl2_onboarding.question": "Would you like to download them?", "controlify.controller_theme.default": "Default", "controlify.controller_theme.xbox_one": "Xbox", @@ -225,6 +232,13 @@ "controlify.error.hid": "Controller Detection Disabled", "controlify.error.hid.desc": "Controller identification failed, so any controller config changes will not persist. Check logs for more info.", + "controlify.battery_level.unknown": "Unknown", + "controlify.battery_level.empty": "Empty", + "controlify.battery_level.low": "Low", + "controlify.battery_level.medium": "Medium", + "controlify.battery_level.high": "High", + "controlify.battery_level.full": "Full", + "controlify.hat_state.up": "Up", "controlify.hat_state.down": "Down", "controlify.hat_state.left": "Left", diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index 5654cc5..2343b23 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -2,6 +2,9 @@ "package": "dev.isxander.controlify.mixins", "required": true, "minVersion": "0.8", + "injectors": { + "defaultRequire": 1 + }, "compatibilityLevel": "JAVA_17", "mixins": [ "compat.iris.BaseOptionElementWidgetMixin",