forked from Clones/Controlify
Migrate to libsdl4j, SDL download screen, use gamecontrollerdb.txt
, calibration now detects joystick triggers
This commit is contained in:
@ -0,0 +1,232 @@
|
||||
package dev.isxander.controlify.driver;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.gui.screen.DownloadingSDLScreen;
|
||||
import dev.isxander.controlify.utils.DebugLog;
|
||||
import dev.isxander.controlify.utils.Log;
|
||||
import dev.isxander.controlify.utils.TrackingBodySubscriber;
|
||||
import dev.isxander.controlify.utils.TrackingConsumer;
|
||||
import io.github.libsdl4j.jna.SdlNativeLibraryLoader;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.minecraft.Util;
|
||||
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.URL;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.channels.Channels;
|
||||
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.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static io.github.libsdl4j.api.Sdl.*;
|
||||
import static io.github.libsdl4j.api.SdlSubSystemConst.*;
|
||||
import static io.github.libsdl4j.api.error.SdlError.*;
|
||||
import static io.github.libsdl4j.api.hints.SdlHints.*;
|
||||
import static io.github.libsdl4j.api.hints.SdlHintsConst.*;
|
||||
|
||||
public class SDL2NativesManager {
|
||||
private static final String SDL2_VERSION = "<SDL2_VERSION>";
|
||||
private static final Map<Target, NativeFileInfo> NATIVE_LIBRARIES = Map.of(
|
||||
new Target(Util.OS.WINDOWS, true, false), new NativeFileInfo("win32-x86-64", "windows64", "dll"),
|
||||
new Target(Util.OS.WINDOWS, false, false), new NativeFileInfo("win32-x86", "window32", "dll"),
|
||||
new Target(Util.OS.LINUX, true, false), new NativeFileInfo("linux-x86-64", "linux64", "so"),
|
||||
new Target(Util.OS.OSX, true, false), new NativeFileInfo("darwin-x86-64", "macos-x86_64", "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 Path NATIVES_PATH = FabricLoader.getInstance().getGameDir().resolve("controlify-natives");
|
||||
|
||||
private static boolean loaded = false;
|
||||
private static boolean attemptedLoad = false;
|
||||
|
||||
private static CompletableFuture<Boolean> initFuture;
|
||||
|
||||
public static CompletableFuture<Boolean> maybeLoad() {
|
||||
if (initFuture != null)
|
||||
return initFuture;
|
||||
|
||||
if (!Controlify.instance().config().globalSettings().loadVibrationNatives)
|
||||
return initFuture = CompletableFuture.completedFuture(false);
|
||||
|
||||
if (attemptedLoad)
|
||||
return initFuture = CompletableFuture.completedFuture(loaded);
|
||||
|
||||
attemptedLoad = true;
|
||||
|
||||
Path localLibraryPath = NATIVES_PATH.resolve(Target.CURRENT.getArtifactName());
|
||||
|
||||
if (Files.exists(localLibraryPath)) {
|
||||
boolean success = loadAndStart(localLibraryPath);
|
||||
if (success)
|
||||
return initFuture = CompletableFuture.completedFuture(true);
|
||||
|
||||
Log.LOGGER.warn("Failed to load SDL2 from local file, attempting to re-download");
|
||||
}
|
||||
return initFuture = downloadAndStart(localLibraryPath);
|
||||
}
|
||||
|
||||
private static boolean loadAndStart(Path localLibraryPath) {
|
||||
try {
|
||||
SdlNativeLibraryLoader.loadLibSDL2FromFilePathNow(localLibraryPath.toAbsolutePath().toString());
|
||||
|
||||
startSDL2();
|
||||
|
||||
loaded = true;
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
Log.LOGGER.error("Failed to start SDL2", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void startSDL2() {
|
||||
// we have no windows, so all events are background events
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||
// accelerometer as joystick is not good UX. unexpected
|
||||
SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0");
|
||||
// see first hint
|
||||
SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1");
|
||||
// raw input requires controller correlation, which is impossible
|
||||
// without calling JoystickUpdate, which we don't do.
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0");
|
||||
// better rumble
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_STEAM, "1");
|
||||
|
||||
// initialise SDL with just joystick and gamecontroller subsystems
|
||||
if (SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) != 0) {
|
||||
Log.LOGGER.error("Failed to initialise SDL2: " + SDL_GetError());
|
||||
throw new RuntimeException("Failed to initialise SDL2: " + SDL_GetError());
|
||||
}
|
||||
|
||||
DebugLog.log("Initialised SDL2");
|
||||
}
|
||||
|
||||
private static CompletableFuture<Boolean> downloadAndStart(Path localLibraryPath) {
|
||||
return downloadLibrary()
|
||||
.thenCompose(success -> {
|
||||
if (!success) {
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(loadAndStart(localLibraryPath));
|
||||
})
|
||||
.thenCompose(success -> Minecraft.getInstance().submit(() -> success));
|
||||
}
|
||||
|
||||
private static CompletableFuture<Boolean> downloadLibrary() {
|
||||
Path path = NATIVES_PATH.resolve(Target.CURRENT.getArtifactName());
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.createFile(path);
|
||||
} catch (Exception e) {
|
||||
Log.LOGGER.error("Failed to delete existing SDL2 native library file", e);
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
|
||||
String url = NATIVE_LIBRARY_URL + Target.CURRENT.getArtifactName();
|
||||
|
||||
var httpClient = HttpClient.newHttpClient();
|
||||
var httpRequest = HttpRequest.newBuilder()
|
||||
.GET()
|
||||
.uri(URI.create(url))
|
||||
.build();
|
||||
|
||||
// send the request asynchronously and track the progress on the download
|
||||
AtomicReference<DownloadingSDLScreen> downloadScreen = new AtomicReference<>();
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
return httpClient.sendAsync(
|
||||
httpRequest,
|
||||
TrackingBodySubscriber.bodyHandler(
|
||||
HttpResponse.BodyHandlers.ofFileDownload(path.getParent(), StandardOpenOption.WRITE),
|
||||
new TrackingConsumer(
|
||||
total -> {
|
||||
DownloadingSDLScreen screen = new DownloadingSDLScreen(minecraft.screen, total, path);
|
||||
downloadScreen.set(screen);
|
||||
minecraft.execute(() -> minecraft.setScreen(screen));
|
||||
},
|
||||
(received, total) -> downloadScreen.get().updateDownloadProgress(received),
|
||||
error -> {
|
||||
if (error.isPresent()) {
|
||||
Log.LOGGER.error("Failed to download SDL2 native library", error.get());
|
||||
minecraft.execute(() -> downloadScreen.get().failDownload(error.get()));
|
||||
} else {
|
||||
Log.LOGGER.debug("Finished downloading SDL2 native library");
|
||||
minecraft.execute(() -> downloadScreen.get().finishDownload());
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
).handle((response, throwable) -> {
|
||||
if (throwable != null) {
|
||||
Log.LOGGER.error("Failed to download SDL2 native library", throwable);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public static boolean hasAttemptedLoad() {
|
||||
return attemptedLoad;
|
||||
}
|
||||
|
||||
public record Target(Util.OS os, boolean is64Bit, boolean isARM) {
|
||||
public static final Target CURRENT = Util.make(() -> {
|
||||
Util.OS os = Util.getPlatform();
|
||||
|
||||
String arch = System.getProperty("os.arch");
|
||||
boolean is64bit = arch.contains("64");
|
||||
boolean isARM = arch.contains("arm") || arch.contains("aarch");
|
||||
|
||||
return new Target(os, is64bit, isARM);
|
||||
});
|
||||
|
||||
public boolean hasNativeLibrary() {
|
||||
return NATIVE_LIBRARIES.containsKey(this);
|
||||
}
|
||||
|
||||
public String getArtifactName() {
|
||||
NativeFileInfo file = NATIVE_LIBRARIES.get(this);
|
||||
return "libsdl4j-natives-" + SDL2_VERSION + "-" + file.downloadSuffix + "." + file.fileExtension;
|
||||
}
|
||||
|
||||
public boolean isMacArm() {
|
||||
return os == Util.OS.OSX && isARM;
|
||||
}
|
||||
}
|
||||
|
||||
public record NativeFileInfo(String folderName, String downloadSuffix, String fileExtension) {
|
||||
public Path getNativePath() {
|
||||
return getSearchPath()
|
||||
.resolve(folderName)
|
||||
.resolve("SDL2." + fileExtension);
|
||||
}
|
||||
|
||||
public Path getSearchPath() {
|
||||
return FabricLoader.getInstance().getGameDir()
|
||||
.resolve("controlify-natives")
|
||||
.resolve(SDL2_VERSION);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user