1
0
forked from Clones/Controlify

Custom natives path (closes #114)

This commit is contained in:
isXander
2023-10-25 20:42:52 +01:00
parent 9427520013
commit 3ec8daa125
9 changed files with 142 additions and 38 deletions

View File

@ -56,6 +56,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import static io.github.libsdl4j.api.gamecontroller.SdlGamecontroller.SDL_GameControllerAddMappingsFromRW; 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 io.github.libsdl4j.api.rwops.SdlRWops.SDL_RWFromConstMem;
public class Controlify implements ControlifyApi { public class Controlify implements ControlifyApi {
@ -426,6 +427,8 @@ public class Controlify implements ControlifyApi {
* @param resource the already located `gamecontrollerdb.txt` resource * @param resource the already located `gamecontrollerdb.txt` resource
*/ */
private void loadGamepadMappings(Resource resource) { private void loadGamepadMappings(Resource resource) {
Log.LOGGER.debug("Loading gamepad mappings...");
try (InputStream is = resource.open()) { try (InputStream is = resource.open()) {
byte[] bytes = ByteStreams.toByteArray(is); byte[] bytes = ByteStreams.toByteArray(is);

View File

@ -19,11 +19,11 @@ public class GlobalSettings {
AbstractContainerScreen.class AbstractContainerScreen.class
); );
@SerializedName("keyboardMovement") @SerializedName("keyboardMovement") public boolean alwaysKeyboardMovement = false;
public boolean alwaysKeyboardMovement = false;
public List<String> keyboardMovementWhitelist = new ArrayList<>(); public List<String> keyboardMovementWhitelist = new ArrayList<>();
public boolean outOfFocusInput = false; public boolean outOfFocusInput = false;
public boolean loadVibrationNatives = false; public boolean loadVibrationNatives = false;
public String customVibrationNativesPath = "";
public boolean vibrationOnboarded = false; public boolean vibrationOnboarded = false;
public ReachAroundMode reachAround = ReachAroundMode.OFF; public ReachAroundMode reachAround = ReachAroundMode.OFF;
public boolean allowServerRumble = true; public boolean allowServerRumble = true;

View File

@ -5,6 +5,10 @@ public interface RumbleDriver extends Driver {
boolean isRumbleSupported(); boolean isRumbleSupported();
boolean rumbleTrigger(float left, float right);
boolean isTriggerRumbleSupported();
String getRumbleDetails(); String getRumbleDetails();
RumbleDriver UNSUPPORTED = new RumbleDriver() { RumbleDriver UNSUPPORTED = new RumbleDriver() {
@ -22,6 +26,16 @@ public interface RumbleDriver extends Driver {
return false; return false;
} }
@Override
public boolean rumbleTrigger(float left, float right) {
return false;
}
@Override
public boolean isTriggerRumbleSupported() {
return false;
}
@Override @Override
public String getRumbleDetails() { public String getRumbleDetails() {
return "Unsupported"; return "Unsupported";

View File

@ -8,11 +8,12 @@ import static io.github.libsdl4j.api.joystick.SdlJoystick.*;
public class SDL2JoystickDriver implements RumbleDriver { public class SDL2JoystickDriver implements RumbleDriver {
private final SDL_Joystick ptrJoystick; private final SDL_Joystick ptrJoystick;
private final boolean isRumbleSupported; private final boolean isRumbleSupported, isTriggerRumbleSupported;
public SDL2JoystickDriver(int jid) { public SDL2JoystickDriver(int jid) {
this.ptrJoystick = SDL_JoystickOpen(jid); this.ptrJoystick = SDL_JoystickOpen(jid);
this.isRumbleSupported = SDL_JoystickHasRumble(ptrJoystick); this.isRumbleSupported = SDL_JoystickHasRumble(ptrJoystick);
this.isTriggerRumbleSupported = SDL_JoystickHasRumbleTriggers(ptrJoystick);
} }
@Override @Override
@ -23,21 +24,36 @@ public class SDL2JoystickDriver implements RumbleDriver {
@Override @Override
public boolean rumble(float strongMagnitude, float weakMagnitude) { public boolean rumble(float strongMagnitude, float weakMagnitude) {
// duration of 0 is infinite // duration of 0 is infinite
if (SDL_JoystickRumble(ptrJoystick, (short)(strongMagnitude * 65535.0F), (short)(weakMagnitude * 65535.0F), 0) != 0) { if (SDL_JoystickRumble(ptrJoystick, (short)(strongMagnitude * 0xFFFF), (short)(weakMagnitude * 0xFFFF), 0) != 0) {
Log.LOGGER.error("Could not rumble controller: " + SDL_GetError()); Log.LOGGER.error("Could not rumble controller: " + SDL_GetError());
return false; return false;
} }
return true; return true;
} }
@Override
public boolean rumbleTrigger(float left, float right) {
// duration of 0 is infinite
if (SDL_JoystickRumbleTriggers(ptrJoystick, (short)(left * 0xFFFF), (short)(right * 0xFFFF), 0) != 0) {
Log.LOGGER.error("Could not rumble controller trigger: " + SDL_GetError());
return false;
}
return true;
}
@Override @Override
public boolean isRumbleSupported() { public boolean isRumbleSupported() {
return isRumbleSupported; return isRumbleSupported;
} }
@Override
public boolean isTriggerRumbleSupported() {
return isTriggerRumbleSupported;
}
@Override @Override
public String getRumbleDetails() { public String getRumbleDetails() {
return "SDL2joy supported=" + isRumbleSupported(); return "SDL2joy supported=" + isRumbleSupported() + " trigger=" + isTriggerRumbleSupported();
} }
@Override @Override

View File

@ -1,8 +1,8 @@
package dev.isxander.controlify.driver; package dev.isxander.controlify.driver;
import dev.isxander.controlify.Controlify; import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.config.ControlifyConfig;
import dev.isxander.controlify.gui.screen.DownloadingSDLScreen; import dev.isxander.controlify.gui.screen.DownloadingSDLScreen;
import dev.isxander.controlify.utils.DebugLog;
import dev.isxander.controlify.utils.Log; import dev.isxander.controlify.utils.Log;
import dev.isxander.controlify.utils.TrackingBodySubscriber; import dev.isxander.controlify.utils.TrackingBodySubscriber;
import dev.isxander.controlify.utils.TrackingConsumer; import dev.isxander.controlify.utils.TrackingConsumer;
@ -10,22 +10,12 @@ import io.github.libsdl4j.jna.SdlNativeLibraryLoader;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.Util; import net.minecraft.Util;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import org.apache.commons.lang3.Validate;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URI; import java.net.URI;
import java.net.URL;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.channels.Channels; import java.nio.file.*;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@ -46,7 +36,6 @@ public class SDL2NativesManager {
new Target(Util.OS.OSX, true, true), new NativeFileInfo("darwin-aarch64", "macos-aarch64", "dylib") new Target(Util.OS.OSX, true, true), new NativeFileInfo("darwin-aarch64", "macos-aarch64", "dylib")
); );
private static final String NATIVE_LIBRARY_URL = "https://maven.isxander.dev/releases/dev/isxander/libsdl4j-natives/%s/".formatted(SDL2_VERSION); private static final String NATIVE_LIBRARY_URL = "https://maven.isxander.dev/releases/dev/isxander/libsdl4j-natives/%s/".formatted(SDL2_VERSION);
private static final Path NATIVES_PATH = FabricLoader.getInstance().getGameDir().resolve("controlify-natives");
private static boolean loaded = false; private static boolean loaded = false;
private static boolean attemptedLoad = false; private static boolean attemptedLoad = false;
@ -65,7 +54,7 @@ public class SDL2NativesManager {
attemptedLoad = true; attemptedLoad = true;
Path localLibraryPath = NATIVES_PATH.resolve(Target.CURRENT.getArtifactName()); Path localLibraryPath = getNativesFolderPath().resolve(Target.CURRENT.getArtifactName());
if (Files.exists(localLibraryPath)) { if (Files.exists(localLibraryPath)) {
boolean success = loadAndStart(localLibraryPath); boolean success = loadAndStart(localLibraryPath);
@ -117,7 +106,7 @@ public class SDL2NativesManager {
} }
private static CompletableFuture<Boolean> downloadAndStart(Path localLibraryPath) { private static CompletableFuture<Boolean> downloadAndStart(Path localLibraryPath) {
return downloadLibrary() return downloadLibrary(localLibraryPath)
.thenCompose(success -> { .thenCompose(success -> {
if (!success) { if (!success) {
return CompletableFuture.completedFuture(false); return CompletableFuture.completedFuture(false);
@ -128,9 +117,7 @@ public class SDL2NativesManager {
.thenCompose(success -> Minecraft.getInstance().submit(() -> success)); .thenCompose(success -> Minecraft.getInstance().submit(() -> success));
} }
private static CompletableFuture<Boolean> downloadLibrary() { private static CompletableFuture<Boolean> downloadLibrary(Path path) {
Path path = NATIVES_PATH.resolve(Target.CURRENT.getArtifactName());
try { try {
Files.deleteIfExists(path); Files.deleteIfExists(path);
Files.createDirectories(path.getParent()); Files.createDirectories(path.getParent());
@ -191,6 +178,22 @@ public class SDL2NativesManager {
return attemptedLoad; return attemptedLoad;
} }
private static Path getNativesFolderPath() {
Path nativesFolderPath = FabricLoader.getInstance().getGameDir();
ControlifyConfig config = Controlify.instance().config();
String customPath = config.globalSettings().customVibrationNativesPath;
if (!customPath.isEmpty()) {
try {
nativesFolderPath = Path.of(customPath);
} catch (InvalidPathException e) {
Log.LOGGER.error("Invalid custom SDL2 native library path. Using default and resetting custom path.", e);
config.globalSettings().customVibrationNativesPath = "";
config.save();
}
}
return nativesFolderPath.resolve("controlify-natives");
}
public record Target(Util.OS os, boolean is64Bit, boolean isARM) { public 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();

View File

@ -21,7 +21,7 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
private final SDL_GameController ptrGamepad; private final SDL_GameController ptrGamepad;
private BasicGamepadState state = BasicGamepadState.EMPTY; private BasicGamepadState state = BasicGamepadState.EMPTY;
private GamepadState.GyroState gyroDelta = new GamepadState.GyroState(0, 0, 0); private GamepadState.GyroState gyroDelta = new GamepadState.GyroState(0, 0, 0);
private final boolean isGyroSupported, isRumbleSupported; private final boolean isGyroSupported, isRumbleSupported, isTriggerRumbleSupported;
private final String guid; private final String guid;
public SDL2GamepadDriver(int jid) { public SDL2GamepadDriver(int jid) {
@ -29,6 +29,7 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
this.guid = SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(ptrGamepad)).toString(); this.guid = SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(ptrGamepad)).toString();
this.isGyroSupported = SDL_GameControllerHasSensor(ptrGamepad, SDL_SENSOR_GYRO); this.isGyroSupported = SDL_GameControllerHasSensor(ptrGamepad, SDL_SENSOR_GYRO);
this.isRumbleSupported = SDL_GameControllerHasRumble(ptrGamepad); this.isRumbleSupported = SDL_GameControllerHasRumble(ptrGamepad);
this.isTriggerRumbleSupported = SDL_GameControllerHasRumbleTriggers(ptrGamepad);
if (this.isGyroSupported()) { if (this.isGyroSupported()) {
SDL_GameControllerSetSensorEnabled(ptrGamepad, SDL_SENSOR_GYRO, true); SDL_GameControllerSetSensorEnabled(ptrGamepad, SDL_SENSOR_GYRO, true);
@ -94,14 +95,28 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
@Override @Override
public boolean rumble(float strongMagnitude, float weakMagnitude) { public boolean rumble(float strongMagnitude, float weakMagnitude) {
if (!isRumbleSupported()) return false;
// duration of 0 is infinite // duration of 0 is infinite
if (SDL_GameControllerRumble(ptrGamepad, (short)(strongMagnitude * 65535.0F), (short)(weakMagnitude * 65535.0F), 0) != 0) { if (SDL_GameControllerRumble(ptrGamepad, (short)(strongMagnitude * 0xFFFF), (short)(weakMagnitude * 0xFFFF), 0) != 0) {
Log.LOGGER.error("Could not rumble controller: " + SDL_GetError()); Log.LOGGER.error("Could not rumble controller: " + SDL_GetError());
return false; return false;
} }
return true; return true;
} }
@Override
public boolean rumbleTrigger(float left, float right) {
if (!isTriggerRumbleSupported()) return false;
// duration of 0 is infinite
if (SDL_GameControllerRumbleTriggers(ptrGamepad, (short)(left * 0xFFFF), (short)(right * 0xFFFF), 0) != 0) {
Log.LOGGER.error("Could not rumble controller trigger: " + SDL_GetError());
return false;
}
return true;
}
@Override @Override
public GamepadState.GyroStateC getGyroState() { public GamepadState.GyroStateC getGyroState() {
return gyroDelta; return gyroDelta;
@ -132,6 +147,11 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
return isRumbleSupported; return isRumbleSupported;
} }
@Override
public boolean isTriggerRumbleSupported() {
return isTriggerRumbleSupported;
}
@Override @Override
public String getGUID() { public String getGUID() {
return guid; return guid;
@ -149,7 +169,7 @@ public class SDL2GamepadDriver implements BasicGamepadInputDriver, GyroDriver, R
@Override @Override
public String getRumbleDetails() { public String getRumbleDetails() {
return "SDL2gp supported=" + isRumbleSupported(); return "SDL2gp supported=" + isRumbleSupported() + " trigger=" + isTriggerRumbleSupported();
} }
@Override @Override

View File

@ -0,0 +1,26 @@
package dev.isxander.controlify.gui.controllers;
import dev.isxander.yacl3.api.Option;
import dev.isxander.yacl3.api.controller.ValueFormatter;
import dev.isxander.yacl3.gui.controllers.string.StringController;
import net.minecraft.network.chat.Component;
public class FormattableStringController extends StringController {
private final ValueFormatter<String> formatter;
/**
* Constructs a string controller
*
* @param option bound option
* @param formatter the formatter to use
*/
public FormattableStringController(Option<String> option, ValueFormatter<String> formatter) {
super(option);
this.formatter = formatter;
}
@Override
public Component formatValue() {
return formatter.format(getString());
}
}

View File

@ -3,6 +3,7 @@ package dev.isxander.controlify.gui.screen;
import dev.isxander.controlify.Controlify; import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.api.ControlifyApi; import dev.isxander.controlify.api.ControlifyApi;
import dev.isxander.controlify.config.GlobalSettings; import dev.isxander.controlify.config.GlobalSettings;
import dev.isxander.controlify.gui.controllers.FormattableStringController;
import dev.isxander.controlify.reacharound.ReachAroundMode; import dev.isxander.controlify.reacharound.ReachAroundMode;
import dev.isxander.controlify.server.ServerPolicies; import dev.isxander.controlify.server.ServerPolicies;
import dev.isxander.controlify.server.ServerPolicy; import dev.isxander.controlify.server.ServerPolicy;
@ -27,6 +28,12 @@ public class GlobalSettingsScreenFactory {
.title(Component.translatable("controlify.gui.global_settings.title")) .title(Component.translatable("controlify.gui.global_settings.title"))
.category(ConfigCategory.createBuilder() .category(ConfigCategory.createBuilder()
.name(Component.translatable("controlify.gui.global_settings.title")) .name(Component.translatable("controlify.gui.global_settings.title"))
.option(ButtonOption.createBuilder()
.name(Component.translatable("controlify.gui.open_issue_tracker"))
.action((screen, button) -> Util.getPlatform().openUri("https://github.com/isxander/controlify/issues"))
.build())
.group(OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.natives"))
.option(Option.<Boolean>createBuilder() .option(Option.<Boolean>createBuilder()
.name(Component.translatable("controlify.gui.load_vibration_natives")) .name(Component.translatable("controlify.gui.load_vibration_natives"))
.description(OptionDescription.createBuilder() .description(OptionDescription.createBuilder()
@ -37,9 +44,19 @@ public class GlobalSettingsScreenFactory {
.controller(opt -> BooleanControllerBuilder.create(opt).yesNoFormatter()) .controller(opt -> BooleanControllerBuilder.create(opt).yesNoFormatter())
.flag(OptionFlag.GAME_RESTART) .flag(OptionFlag.GAME_RESTART)
.build()) .build())
.option(ButtonOption.createBuilder() .option(Option.<String>createBuilder()
.name(Component.translatable("controlify.gui.open_issue_tracker")) .name(Component.translatable("controlify.gui.custom_natives_path"))
.action((screen, button) -> Util.getPlatform().openUri("https://github.com/isxander/controlify/issues")) .description(OptionDescription.createBuilder()
.text(Component.translatable("controlify.gui.custom_natives_path.tooltip"))
.text(Component.translatable("controlify.gui.custom_natives_path.tooltip.warning").withStyle(ChatFormatting.RED))
.build())
.binding("", () -> globalSettings.customVibrationNativesPath, v -> globalSettings.customVibrationNativesPath = v)
.customController(opt -> new FormattableStringController(opt, s -> {
if (s.isEmpty())
return Component.translatable("controlify.gui.custom_natives_path.none");
return Component.literal(s);
}))
.build())
.build()) .build())
.group(OptionGroup.createBuilder() .group(OptionGroup.createBuilder()
.name(Component.translatable("controlify.gui.server_options")) .name(Component.translatable("controlify.gui.server_options"))

View File

@ -7,11 +7,16 @@
"controlify.gui.carousel.art_credit": "Controller art by %s.", "controlify.gui.carousel.art_credit": "Controller art by %s.",
"controlify.gui.global_settings.title": "Global Settings", "controlify.gui.global_settings.title": "Global Settings",
"controlify.gui.server_options": "Server Options", "controlify.gui.natives": "Natives",
"controlify.gui.miscellaneous": "Miscellaneous",
"controlify.gui.load_vibration_natives": "Load Natives", "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": "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.custom_natives_path": "Custom Natives Path",
"controlify.gui.custom_natives_path.tooltip": "Specify a custom folder where Controlify will save and load it's native libraries. This is an absolute path and is not relative to .minecraft. If you enter an invalid directory, this will be reset. Leave blank for default.",
"controlify.gui.custom_natives_path.tooltip.warning": "This is an advanced setting. Don't touch it if you don't know what you're doing!",
"controlify.gui.custom_natives_path.none": "Not set",
"controlify.gui.server_options": "Server Options",
"controlify.gui.miscellaneous": "Miscellaneous",
"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.",
"controlify.gui.reach_around.tooltip.parity": "This is parity with bedrock edition where you can also do this.", "controlify.gui.reach_around.tooltip.parity": "This is parity with bedrock edition where you can also do this.",