diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java index beea6ae..7af9ab4 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java @@ -118,12 +118,10 @@ public class MainViewController { throwable.printStackTrace(); return new ArrayList<>(); }) - .thenAccept(newServers -> { - Platform.runLater(() -> { - this.servers.clear(); - this.servers.addAll(newServers); - }); - }); + .thenAccept(newServers -> Platform.runLater(() -> { + this.servers.clear(); + this.servers.addAll(newServers); + })); } @FXML @@ -150,7 +148,20 @@ public class MainViewController { @FXML public void play() { - + Profile profile = this.selectedProfile.get(); + Server server = this.selectedServer.get(); + VersionFetcher.INSTANCE.ensureVersionIsDownloaded(profile.getClientVersion()) + .thenAccept(path -> { + try { + Process p = new ProcessBuilder() + .command("java", "-jar", path.toAbsolutePath().toString()) + .directory(Launcher.BASE_DIR.toFile()) + .inheritIO() + .start(); + } catch (IOException e) { + e.printStackTrace(); + } + }); } private void selectProfile(ProfileView view) { diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java new file mode 100644 index 0000000..8517dec --- /dev/null +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java @@ -0,0 +1,35 @@ +package nl.andrewl.aos2_launcher; + +public class SystemVersionValidator { + private static final String os = System.getProperty("os.name").trim().toLowerCase(); + private static final String arch = System.getProperty("os.arch").trim().toLowerCase(); + + private static final boolean OS_WINDOWS = os.contains("win"); + private static final boolean OS_MAC = os.contains("mac"); + private static final boolean OS_LINUX = os.contains("nix") || os.contains("nux") || os.contains("aix"); + + private static final boolean ARCH_X86 = arch.equals("x86"); + private static final boolean ARCH_X86_64 = arch.equals("x86_64"); + private static final boolean ARCH_AMD64 = arch.equals("amd64"); + private static final boolean ARCH_AARCH64 = arch.equals("aarch64"); + private static final boolean ARCH_ARM = arch.equals("arm"); + private static final boolean ARCH_ARM32 = arch.equals("arm32"); + + public static String getPreferredVersionSuffix() { + if (OS_LINUX) { + if (ARCH_AARCH64) return "linux-aarch64"; + if (ARCH_AMD64) return "linux-amd64"; + if (ARCH_ARM) return "linux-arm"; + if (ARCH_ARM32) return "linux-arm32"; + } else if (OS_MAC) { + if (ARCH_AARCH64) return "macos-aarch64"; + if (ARCH_X86_64) return "macos-x86_64"; + } else if (OS_WINDOWS) { + if (ARCH_AARCH64) return "windows-aarch64"; + if (ARCH_AMD64) return "windows-amd64"; + if (ARCH_X86) return "windows-x86"; + } + System.err.println("Couldn't determine the preferred OS/ARCH version. Defaulting to windows-amd64."); + return "windows-amd64"; + } +} diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java new file mode 100644 index 0000000..2152a03 --- /dev/null +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java @@ -0,0 +1,168 @@ +package nl.andrewl.aos2_launcher; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import nl.andrewl.aos2_launcher.model.ClientVersionRelease; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class VersionFetcher { + private static final String BASE_GITHUB_URL = "https://api.github.com/repos/andrewlalis/ace-of-shades-2"; + + public static final VersionFetcher INSTANCE = new VersionFetcher(); + + private final List availableReleases; + + private final HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); + private boolean loaded = false; + private CompletableFuture> activeReleaseFetchFuture; + + public VersionFetcher() { + this.availableReleases = new ArrayList<>(); + System.out.println(System.getProperty("os.name")); + System.out.println(System.getProperty("os.arch")); + } + + public CompletableFuture getRelease(String versionTag) { + return getAvailableReleases().thenApply(releases -> releases.stream() + .filter(r -> r.tag().equals(versionTag)) + .findFirst().orElse(null)); + } + + public CompletableFuture> getAvailableReleases() { + if (loaded) { + return CompletableFuture.completedFuture(Collections.unmodifiableList(availableReleases)); + } + + System.out.println("Fetching the list of available releases..."); + return fetchReleasesFromGitHub(); + } + + private CompletableFuture> fetchReleasesFromGitHub() { + if (activeReleaseFetchFuture != null) return activeReleaseFetchFuture; + HttpRequest req = HttpRequest.newBuilder(URI.create(BASE_GITHUB_URL + "/releases")) + .timeout(Duration.ofSeconds(3)) + .GET() + .build(); + activeReleaseFetchFuture = httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofInputStream()) + .thenApplyAsync(resp -> { + if (resp.statusCode() == 200) { + JsonArray releasesArray = new Gson().fromJson(new InputStreamReader(resp.body()), JsonArray.class); + availableReleases.clear(); + for (var element : releasesArray) { + if (element.isJsonObject()) { + JsonObject obj = element.getAsJsonObject(); + String tag = obj.get("tag_name").getAsString(); + String apiUrl = obj.get("url").getAsString(); + String assetsUrl = obj.get("assets_url").getAsString(); + OffsetDateTime publishedAt = OffsetDateTime.parse(obj.get("published_at").getAsString(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + LocalDateTime localPublishedAt = publishedAt.atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); + availableReleases.add(new ClientVersionRelease(tag, apiUrl, assetsUrl, localPublishedAt)); + } + } + availableReleases.sort(Comparator.comparing(ClientVersionRelease::publishedAt).reversed()); + loaded = true; + return availableReleases; + } else { + throw new RuntimeException("Error while requesting releases."); + } + }); + return activeReleaseFetchFuture; + } + + public List getDownloadedVersions() { + try (var s = Files.list(Launcher.VERSIONS_DIR)) { + return s.filter(this::isVersionFile) + .map(this::extractVersion) + .toList(); + } catch (IOException e) { + e.printStackTrace(); + return Collections.emptyList(); + } + } + + public CompletableFuture ensureVersionIsDownloaded(String versionTag) { + try (var s = Files.list(Launcher.VERSIONS_DIR)) { + Optional optionalFile = s.filter(f -> isVersionFile(f) && versionTag.equals(extractVersion(f))) + .findFirst(); + if (optionalFile.isPresent()) return CompletableFuture.completedFuture(optionalFile.get()); + } catch (IOException e) { + return CompletableFuture.failedFuture(e); + } + return getRelease(versionTag) + .thenComposeAsync(this::downloadVersion); + } + + private CompletableFuture downloadVersion(ClientVersionRelease release) { + System.out.println("Downloading version " + release.tag()); + HttpRequest req = HttpRequest.newBuilder(URI.create(release.assetsUrl())) + .GET().timeout(Duration.ofSeconds(3)).build(); + CompletableFuture downloadUrlFuture = httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofInputStream()) + .thenApplyAsync(resp -> { + if (resp.statusCode() == 200) { + JsonArray assetsArray = new Gson().fromJson(new InputStreamReader(resp.body()), JsonArray.class); + String preferredVersionSuffix = SystemVersionValidator.getPreferredVersionSuffix(); + String regex = "aos2-client-\\d+\\.\\d+\\.\\d+-" + preferredVersionSuffix + "\\.jar"; + for (var asset : assetsArray) { + JsonObject assetObj = asset.getAsJsonObject(); + String name = assetObj.get("name").getAsString(); + if (name.matches(regex)) { + System.out.println("Found matching asset: " + name); + return assetObj; + } + } + throw new RuntimeException("Couldn't find a matching release asset."); + } else { + throw new RuntimeException("Error while requesting release assets."); + } + }); + return downloadUrlFuture.thenComposeAsync(asset -> { + String url = asset.get("browser_download_url").getAsString(); + String fileName = asset.get("name").getAsString(); + HttpRequest downloadRequest = HttpRequest.newBuilder(URI.create(url)) + .GET().timeout(Duration.ofMinutes(5)).build(); + Path file = Launcher.VERSIONS_DIR.resolve(fileName); + System.out.printf("Downloading %s to %s.%n", fileName, file.toAbsolutePath()); + return httpClient.sendAsync(downloadRequest, HttpResponse.BodyHandlers.ofFile(file)) + .thenApplyAsync(resp -> { + System.out.println(resp); + return resp.body(); + }); + }); + } + + private boolean isVersionDownloaded(String versionTag) { + return getDownloadedVersions().contains(versionTag); + } + + private boolean isVersionFile(Path p) { + return Files.isRegularFile(p) && p.getFileName().toString() + .matches("aos2-client-\\d+\\.\\d+\\.\\d+-.+\\.jar"); + } + + private String extractVersion(Path file) { + Pattern pattern = Pattern.compile("\\d+\\.\\d+\\.\\d+"); + Matcher matcher = pattern.matcher(file.getFileName().toString()); + if (matcher.find()) { + return "v" + matcher.group(); + } + throw new IllegalArgumentException("File doesn't contain a valid version pattern."); + } +} diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ClientVersionRelease.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ClientVersionRelease.java new file mode 100644 index 0000000..7ec58f5 --- /dev/null +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ClientVersionRelease.java @@ -0,0 +1,11 @@ +package nl.andrewl.aos2_launcher.model; + +import java.time.LocalDateTime; +import java.time.ZonedDateTime; + +public record ClientVersionRelease ( + String tag, + String apiUrl, + String assetsUrl, + LocalDateTime publishedAt +) {} diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/view/EditProfileDialog.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/EditProfileDialog.java index 7f9da6d..21510b0 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/view/EditProfileDialog.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/EditProfileDialog.java @@ -11,6 +11,8 @@ import javafx.scene.Parent; import javafx.scene.control.*; import javafx.stage.Modality; import javafx.stage.Window; +import nl.andrewl.aos2_launcher.VersionFetcher; +import nl.andrewl.aos2_launcher.model.ClientVersionRelease; import nl.andrewl.aos2_launcher.model.Profile; import java.io.IOException; @@ -41,8 +43,15 @@ public class EditProfileDialog extends Dialog { .or(clientVersionChoiceBox.valueProperty().isNull()); nameField.setText(profile.getName()); descriptionTextArea.setText(profile.getDescription()); - clientVersionChoiceBox.setItems(FXCollections.observableArrayList("v1.2.0", "v1.3.0", "v1.4.0")); - clientVersionChoiceBox.setValue(profile.getClientVersion()); + VersionFetcher.INSTANCE.getAvailableReleases().thenAccept(releases -> { + Platform.runLater(() -> { + clientVersionChoiceBox.setItems(FXCollections.observableArrayList(releases.stream().map(ClientVersionRelease::tag).toList())); + String lastRelease = releases.size() == 0 ? null : releases.get(0).tag(); + if (lastRelease != null) { + clientVersionChoiceBox.setValue(lastRelease); + } + }); + }); DialogPane pane = new DialogPane(); pane.setContent(parent);