1
0
forked from Clones/Controlify

Battery level warning + update SDL with macOS ARM support

This commit is contained in:
isXander
2023-05-11 16:43:13 +01:00
parent 0e8bf0cc9b
commit 71c7e26587
19 changed files with 283 additions and 69 deletions

View File

@ -22,6 +22,7 @@ import dev.isxander.controlify.sound.ControlifySounds;
import dev.isxander.controlify.utils.DebugLog; import dev.isxander.controlify.utils.DebugLog;
import dev.isxander.controlify.utils.ToastUtils; import dev.isxander.controlify.utils.ToastUtils;
import dev.isxander.controlify.virtualmouse.VirtualMouseHandler; 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.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.resource.ResourceManagerHelper; import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
import net.fabricmc.fabric.api.resource.ResourcePackActivationType; import net.fabricmc.fabric.api.resource.ResourcePackActivationType;
@ -57,7 +58,7 @@ public class Controlify implements ControlifyApi {
private InputMode currentInputMode = InputMode.KEYBOARD_MOUSE; private InputMode currentInputMode = InputMode.KEYBOARD_MOUSE;
private ControllerHIDService controllerHIDService; private ControllerHIDService controllerHIDService;
private CompletableFuture<Boolean> vibrationOnboardingFuture = null; private CompletableFuture<Boolean> nativeOnboardingFuture = null;
private final ControlifyConfig config = new ControlifyConfig(this); 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); var controllersConnected = IntStream.range(0, GLFW.GLFW_JOYSTICK_LAST + 1).anyMatch(GLFW::glfwJoystickPresent);
if (controllersConnected) { if (controllersConnected) {
askVibrationNatives().whenComplete((loaded, th) -> discoverControllers()); askNatives().whenComplete((loaded, th) -> discoverControllers());
} }
// listen for new controllers // listen for new controllers
GLFW.glfwSetJoystickCallback((jid, event) -> { GLFW.glfwSetJoystickCallback((jid, event) -> {
try { try {
this.askVibrationNatives().whenComplete((loaded, th) -> { this.askNatives().whenComplete((loaded, th) -> {
if (event == GLFW.GLFW_CONNECTED) { if (event == GLFW.GLFW_CONNECTED) {
this.onControllerHotplugged(jid); this.onControllerHotplugged(jid);
} else if (event == GLFW.GLFW_DISCONNECTED) { } else if (event == GLFW.GLFW_DISCONNECTED) {
@ -96,25 +97,25 @@ public class Controlify implements ControlifyApi {
}); });
} }
private CompletableFuture<Boolean> askVibrationNatives() { private CompletableFuture<Boolean> askNatives() {
if (vibrationOnboardingFuture != null) return vibrationOnboardingFuture; if (nativeOnboardingFuture != null) return nativeOnboardingFuture;
if (config().globalSettings().vibrationOnboarded) { if (config().globalSettings().vibrationOnboarded) {
return CompletableFuture.completedFuture(config().globalSettings().loadVibrationNatives); return CompletableFuture.completedFuture(config().globalSettings().loadVibrationNatives);
} }
vibrationOnboardingFuture = new CompletableFuture<>(); nativeOnboardingFuture = new CompletableFuture<>();
minecraft.setScreen(new VibrationOnboardingScreen( minecraft.setScreen(new SDLOnboardingScreen(
minecraft.screen, minecraft.screen,
answer -> { answer -> {
if (answer) if (answer)
SDL2NativesManager.initialise(); SDL2NativesManager.initialise();
vibrationOnboardingFuture.complete(answer); nativeOnboardingFuture.complete(answer);
} }
)); ));
return vibrationOnboardingFuture; return nativeOnboardingFuture;
} }
private void discoverControllers() { private void discoverControllers() {
@ -229,6 +230,8 @@ public class Controlify implements ControlifyApi {
} }
} }
LowBatteryNotifier.tick();
getCurrentController().ifPresent(currentController -> { getCurrentController().ifPresent(currentController -> {
wrapControllerError( wrapControllerError(
() -> tickController(currentController, outOfFocus), () -> tickController(currentController, outOfFocus),

View File

@ -19,4 +19,5 @@ public class GlobalSettings {
public boolean vibrationOnboarded = false; public boolean vibrationOnboarded = false;
public ReachAroundMode reachAround = ReachAroundMode.OFF; public ReachAroundMode reachAround = ReachAroundMode.OFF;
public boolean uiSounds = false; public boolean uiSounds = false;
public boolean notifyLowBattery = true;
} }

View File

@ -6,6 +6,6 @@ import com.terraformersmc.modmenu.api.ModMenuApi;
public class ModMenuIntegration implements ModMenuApi { public class ModMenuIntegration implements ModMenuApi {
@Override @Override
public ConfigScreenFactory<?> getModConfigScreenFactory() { public ConfigScreenFactory<?> getModConfigScreenFactory() {
return YACLHelper::generateConfigScreen; return YACLHelper::openConfigScreen;
} }
} }

View File

@ -46,7 +46,18 @@ public class YACLHelper {
private static final Function<Float, Component> percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100)); private static final Function<Float, Component> percentFormatter = v -> Component.literal(String.format("%.0f%%", v*100));
private static final Function<Float, Component> percentOrOffFormatter = v -> v == 0 ? CommonComponents.OPTION_OFF : percentFormatter.apply(v); private static final Function<Float, Component> 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 controlify = Controlify.instance();
var yacl = YetAnotherConfigLib.createBuilder() var yacl = YetAnotherConfigLib.createBuilder()
@ -86,6 +97,12 @@ public class YACLHelper {
.binding(GlobalSettings.DEFAULT.uiSounds, () -> globalSettings.uiSounds, v -> globalSettings.uiSounds = v) .binding(GlobalSettings.DEFAULT.uiSounds, () -> globalSettings.uiSounds, v -> globalSettings.uiSounds = v)
.controller(TickBoxController::new) .controller(TickBoxController::new)
.build()) .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) .option(Option.createBuilder(boolean.class)
.name(Component.translatable("controlify.gui.out_of_focus_input")) .name(Component.translatable("controlify.gui.out_of_focus_input"))
.tooltip(Component.translatable("controlify.gui.out_of_focus_input.tooltip")) .tooltip(Component.translatable("controlify.gui.out_of_focus_input.tooltip"))
@ -126,6 +143,10 @@ public class YACLHelper {
category.name(Component.literal(controller.name())); 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 config = controller.config();
var def = controller.defaultConfig(); var def = controller.defaultConfig();

View File

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

View File

@ -2,24 +2,11 @@ package dev.isxander.controlify.controller;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.bindings.ControllerBindings; 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.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.RumbleCapable;
import dev.isxander.controlify.rumble.RumbleManager; import dev.isxander.controlify.rumble.RumbleManager;
import dev.isxander.controlify.rumble.RumbleSource; 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; import java.util.Optional;
public interface Controller<S extends ControllerState, C extends ControllerConfig> { public interface Controller<S extends ControllerState, C extends ControllerConfig> {
@ -48,6 +35,10 @@ public interface Controller<S extends ControllerState, C extends ControllerConfi
RumbleManager rumbleManager(); RumbleManager rumbleManager();
boolean supportsRumble(); boolean supportsRumble();
default BatteryLevel batteryLevel() {
return BatteryLevel.UNKNOWN;
}
Optional<ControllerHIDService.ControllerHIDInfo> hidInfo(); Optional<ControllerHIDService.ControllerHIDInfo> hidInfo();
default boolean canBeUsed() { default boolean canBeUsed() {

View File

@ -17,6 +17,10 @@ public record ControllerType(String friendlyName, String mappingId, String theme
private static Map<HIDIdentifier, ControllerType> typeMap = null; private static Map<HIDIdentifier, ControllerType> typeMap = null;
private static final ResourceLocation hidDbLocation = new ResourceLocation("controlify", "controllers/controller_identification.json5"); 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() { public static void ensureTypeMapFilled() {
if (typeMap != null) return; if (typeMap != null) return;

View File

@ -2,6 +2,7 @@ package dev.isxander.controlify.controller.gamepad;
import dev.isxander.controlify.bindings.ControllerBindings; import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.AbstractController; import dev.isxander.controlify.controller.AbstractController;
import dev.isxander.controlify.controller.BatteryLevel;
import dev.isxander.controlify.controller.hid.ControllerHIDService; import dev.isxander.controlify.controller.hid.ControllerHIDService;
import dev.isxander.controlify.driver.*; import dev.isxander.controlify.driver.*;
import dev.isxander.controlify.rumble.RumbleManager; import dev.isxander.controlify.rumble.RumbleManager;
@ -103,6 +104,11 @@ public class GamepadController extends AbstractController<GamepadState, GamepadC
return this.rumbleManager; return this.rumbleManager;
} }
@Override
public BatteryLevel batteryLevel() {
return drivers.batteryDriver().getBatteryLevel();
}
@Override @Override
public void close() { public void close() {
uniqueDrivers.forEach(Driver::close); uniqueDrivers.forEach(Driver::close);

View File

@ -3,6 +3,9 @@ package dev.isxander.controlify.controller.hid;
import com.mojang.datafixers.util.Pair; import com.mojang.datafixers.util.Pair;
import dev.isxander.controlify.Controlify; import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.ControllerType; import dev.isxander.controlify.controller.ControllerType;
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
import dev.isxander.controlify.utils.ToastUtils;
import net.minecraft.network.chat.Component;
import org.hid4java.*; import org.hid4java.*;
import java.util.*; import java.util.*;
@ -15,6 +18,7 @@ public class ControllerHIDService {
private final Queue<Pair<HidDevice, HIDIdentifier>> unconsumedControllerHIDs; private final Queue<Pair<HidDevice, HIDIdentifier>> unconsumedControllerHIDs;
private final Map<String, HidDevice> attachedDevices = new HashMap<>(); private final Map<String, HidDevice> attachedDevices = new HashMap<>();
private boolean disabled = false; private boolean disabled = false;
private boolean firstFetch = true;
// https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/hid-usages#usage-page // https://learn.microsoft.com/en-us/windows-hardware/drivers/hid/hid-usages#usage-page
private static final Set<Integer> CONTROLLER_USAGE_IDS = Set.of( private static final Set<Integer> CONTROLLER_USAGE_IDS = Set.of(
0x04, // Joystick 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) { if (disabled) {
return new ControllerHIDInfo(ControllerType.UNKNOWN, Optional.empty()); return new ControllerHIDInfo(ControllerType.UNKNOWN, Optional.empty());
} }
@ -52,7 +78,7 @@ public class ControllerHIDService {
return new ControllerHIDInfo(ControllerType.UNKNOWN, Optional.empty()); 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) 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()); Controlify.LOGGER.warn("Controller found via USB hardware scan, but it was not found in the controller identification database! (HID: {})", hid.getSecond());

View File

@ -6,6 +6,7 @@ import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.Util; import net.minecraft.Util;
import org.libsdl.SDL; import org.libsdl.SDL;
import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.net.URL; import java.net.URL;
import java.nio.channels.Channels; import java.nio.channels.Channels;
@ -13,6 +14,7 @@ import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Comparator;
import java.util.Map; import java.util.Map;
import static org.libsdl.SDL_Hints.*; import static org.libsdl.SDL_Hints.*;
@ -20,10 +22,11 @@ import static org.libsdl.SDL_Hints.*;
public class SDL2NativesManager { public class SDL2NativesManager {
private static final String SDL2_VERSION = "<SDL2_VERSION>"; private static final String SDL2_VERSION = "<SDL2_VERSION>";
private static final Map<Target, String> NATIVE_LIBRARIES = Map.of( private static final Map<Target, String> NATIVE_LIBRARIES = Map.of(
new Target(Util.OS.WINDOWS, true), "windows64.dll", new Target(Util.OS.WINDOWS, true, false), "windows64.dll",
new Target(Util.OS.WINDOWS, false), "window32.dll", new Target(Util.OS.WINDOWS, false, false), "window32.dll",
new Target(Util.OS.LINUX, true), "linux64.so" new Target(Util.OS.LINUX, true, false), "linux64.so",
//new Target(Util.OS.OSX, true), "mac64.dylib" 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); 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(); Path localLibraryPath = Target.CURRENT.getLocalNativePath();
if (Files.notExists(localLibraryPath)) { 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()); Controlify.LOGGER.info("Downloading SDL2 native library: " + Target.CURRENT.getArtifactName());
downloadLibrary(localLibraryPath); downloadLibrary(localLibraryPath);
} }
@ -110,12 +123,13 @@ public class SDL2NativesManager {
return loaded; 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(() -> { public static final Target CURRENT = Util.make(() -> {
Util.OS os = Util.getPlatform(); Util.OS os = Util.getPlatform();
boolean is64bit = System.getProperty("os.arch").contains("64"); 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() { public boolean hasNativeLibrary() {

View File

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

View File

@ -1,5 +1,6 @@
package dev.isxander.controlify.driver; package dev.isxander.controlify.driver;
import com.google.common.collect.Sets;
import dev.isxander.controlify.Controlify; import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.sdl2.SDL2NativesManager; import dev.isxander.controlify.controller.sdl2.SDL2NativesManager;
import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.debug.DebugProperties;
@ -7,19 +8,20 @@ import org.hid4java.HidDevice;
import java.util.*; 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<Driver> getUniqueDrivers() { public Set<Driver> getUniqueDrivers() {
Set<Driver> drivers = Collections.newSetFromMap(new IdentityHashMap<>()); Set<Driver> drivers = Sets.newIdentityHashSet();
drivers.addAll(List.of(basicGamepadInputDriver, gyroDriver, rumbleDriver)); drivers.addAll(List.of(basicGamepadInputDriver, gyroDriver, rumbleDriver, batteryDriver));
return drivers; return drivers;
} }
public void printDrivers() { public void printDrivers() {
if (DebugProperties.PRINT_DRIVER) { 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(), basicGamepadInputDriver.getBasicGamepadDetails(),
gyroDriver.getGyroDetails(), gyroDriver.getGyroDetails(),
rumbleDriver.getRumbleDetails() rumbleDriver.getRumbleDetails(),
batteryDriver.getBatteryDriverDetails()
); );
} }
} }
@ -28,11 +30,13 @@ public record GamepadDrivers(BasicGamepadInputDriver basicGamepadInputDriver, Gy
BasicGamepadInputDriver basicGamepadInputDriver = new GLFWGamepadDriver(jid); BasicGamepadInputDriver basicGamepadInputDriver = new GLFWGamepadDriver(jid);
GyroDriver gyroDriver = GyroDriver.UNSUPPORTED; GyroDriver gyroDriver = GyroDriver.UNSUPPORTED;
RumbleDriver rumbleDriver = RumbleDriver.UNSUPPORTED; RumbleDriver rumbleDriver = RumbleDriver.UNSUPPORTED;
BatteryDriver batteryDriver = BatteryDriver.UNSUPPORTED;
if (SDL2NativesManager.isLoaded()) { if (SDL2NativesManager.isLoaded()) {
SDL2GamepadDriver sdl2Driver = new SDL2GamepadDriver(jid); SDL2GamepadDriver sdl2Driver = new SDL2GamepadDriver(jid);
gyroDriver = sdl2Driver; gyroDriver = sdl2Driver;
rumbleDriver = sdl2Driver; rumbleDriver = sdl2Driver;
batteryDriver = sdl2Driver;
} }
// broken // broken
@ -40,6 +44,6 @@ public record GamepadDrivers(BasicGamepadInputDriver basicGamepadInputDriver, Gy
gyroDriver = new SteamDeckDriver(hid.get()); gyroDriver = new SteamDeckDriver(hid.get());
} }
return new GamepadDrivers(basicGamepadInputDriver, gyroDriver, rumbleDriver); return new GamepadDrivers(basicGamepadInputDriver, gyroDriver, rumbleDriver, batteryDriver);
} }
} }

View File

@ -1,11 +1,12 @@
package dev.isxander.controlify.driver; package dev.isxander.controlify.driver;
import dev.isxander.controlify.Controlify; import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.controller.BatteryLevel;
import dev.isxander.controlify.controller.gamepad.GamepadState; import dev.isxander.controlify.controller.gamepad.GamepadState;
import dev.isxander.controlify.debug.DebugProperties; import dev.isxander.controlify.debug.DebugProperties;
import org.libsdl.SDL; import org.libsdl.SDL;
public class SDL2GamepadDriver implements GyroDriver, RumbleDriver { public class SDL2GamepadDriver implements GyroDriver, RumbleDriver, BatteryDriver {
private final long ptrGamepad; private final long ptrGamepad;
private GamepadState.GyroState gyroDelta; private GamepadState.GyroState gyroDelta;
private final boolean isGyroSupported, isRumbleSupported; private final boolean isGyroSupported, isRumbleSupported;
@ -42,6 +43,20 @@ public class SDL2GamepadDriver implements GyroDriver, RumbleDriver {
return gyroDelta; 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 @Override
public boolean isGyroSupported() { public boolean isGyroSupported() {
return isGyroSupported; return isGyroSupported;
@ -66,4 +81,9 @@ public class SDL2GamepadDriver implements GyroDriver, RumbleDriver {
public String getRumbleDetails() { public String getRumbleDetails() {
return "SDL2gp supported=" + isRumbleSupported(); return "SDL2gp supported=" + isRumbleSupported();
} }
@Override
public String getBatteryDriverDetails() {
return "SDL2gp";
}
} }

View File

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

View File

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

View File

@ -22,7 +22,7 @@ public class ControlsScreenMixin extends OptionsSubScreen {
@Inject(method = "init", at = @At("RETURN")) @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) { 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) .pos(leftX, currentY)
.width(150) .width(150)
.build()); .build());

View File

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

View File

@ -2,8 +2,8 @@
"controlify.gui.category.global": "Global", "controlify.gui.category.global": "Global",
"controlify.gui.current_controller": "Current Controller", "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.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": "Load Natives",
"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.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.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": "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.", "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.reach_around.everywhere": "Everywhere",
"controlify.gui.ui_sounds": "UI Sounds", "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.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": "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.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": "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.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.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": "Basic",
"controlify.gui.group.basic.tooltip": "Adjust how your controller behaves.", "controlify.gui.group.basic.tooltip": "Adjust how your controller behaves.",
"controlify.gui.horizontal_look_sensitivity": "Horizontal Look Sensitivity", "controlify.gui.horizontal_look_sensitivity": "Horizontal Look Sensitivity",
@ -121,9 +124,13 @@
"controlify.toast.controller_disconnected.description": "'%s' was disconnected.", "controlify.toast.controller_disconnected.description": "'%s' was disconnected.",
"controlify.toast.faulty_input.title": "Controller disabled", "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.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.sdl2_onboarding.title": "Controlify Native Library",
"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.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.default": "Default",
"controlify.controller_theme.xbox_one": "Xbox", "controlify.controller_theme.xbox_one": "Xbox",
@ -225,6 +232,13 @@
"controlify.error.hid": "Controller Detection Disabled", "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.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.up": "Up",
"controlify.hat_state.down": "Down", "controlify.hat_state.down": "Down",
"controlify.hat_state.left": "Left", "controlify.hat_state.left": "Left",

View File

@ -2,6 +2,9 @@
"package": "dev.isxander.controlify.mixins", "package": "dev.isxander.controlify.mixins",
"required": true, "required": true,
"minVersion": "0.8", "minVersion": "0.8",
"injectors": {
"defaultRequire": 1
},
"compatibilityLevel": "JAVA_17", "compatibilityLevel": "JAVA_17",
"mixins": [ "mixins": [
"compat.iris.BaseOptionElementWidgetMixin", "compat.iris.BaseOptionElementWidgetMixin",