From f7959796b4138e458fa3e469249ac1842f4f329c Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sun, 7 Aug 2022 12:48:24 +0200 Subject: [PATCH] Added better client command-line config, and set up simple application packaging pipeline. --- .../java/nl/andrewl/aos2_client/Client.java | 25 ++- .../aos2_client/CommunicationHandler.java | 6 +- .../aos2_client/config/ClientConfig.java | 3 - client/src/main/resources/default-config.yaml | 4 - launcher/package_linux.sh | 30 ++++ launcher/pom.xml | 43 +++++- .../nl/andrewl/aos2_launcher/GameRunner.java | 70 +++++++++ .../nl/andrewl/aos2_launcher/Launcher.java | 12 +- .../aos2_launcher/MainViewController.java | 145 ++++-------------- .../aos2_launcher/SystemVersionValidator.java | 6 +- .../andrewl/aos2_launcher/VersionFetcher.java | 10 +- .../andrewl/aos2_launcher/model/Profile.java | 40 +++-- .../aos2_launcher/model/ProfileSet.java | 40 +++-- .../aos2_launcher/view/EditProfileDialog.java | 37 +++-- .../aos2_launcher/view/ElementList.java | 111 ++++++++++++++ .../aos2_launcher/view/ProfileView.java | 31 ++-- .../main/resources/dialog/edit_profile.fxml | 10 +- launcher/src/main/resources/main_view.fxml | 50 +++--- launcher/src/main/resources/profile_view.fxml | 31 ++++ launcher/src/main/resources/styles.css | 2 +- 20 files changed, 471 insertions(+), 235 deletions(-) create mode 100755 launcher/package_linux.sh create mode 100644 launcher/src/main/java/nl/andrewl/aos2_launcher/GameRunner.java create mode 100644 launcher/src/main/java/nl/andrewl/aos2_launcher/view/ElementList.java create mode 100644 launcher/src/main/resources/profile_view.fxml diff --git a/client/src/main/java/nl/andrewl/aos2_client/Client.java b/client/src/main/java/nl/andrewl/aos2_client/Client.java index 13814e5..70f4d38 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -26,7 +26,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; public class Client implements Runnable { + private final String host; + private final int port; + private final String username; private final ClientConfig config; + private final CommunicationHandler communicationHandler; private final InputHandler inputHandler; private final Camera camera; @@ -42,7 +46,10 @@ public class Client implements Runnable { private final Chat chat; private final Queue mainThreadActions; - public Client(ClientConfig config) { + public Client(ClientConfig config, String host, int port, String username) { + this.host = host; + this.port = port; + this.username = username; this.config = config; this.camera = new Camera(); this.players = new ConcurrentHashMap<>(); @@ -74,7 +81,7 @@ public class Client implements Runnable { @Override public void run() { try { - communicationHandler.establishConnection(); + communicationHandler.establishConnection(host, port, username); } catch (IOException e) { System.err.println("Couldn't connect to the server: " + e.getMessage()); return; @@ -265,13 +272,21 @@ public class Client implements Runnable { } public static void main(String[] args) throws IOException { + if (args.length < 3) { + System.err.println("Missing required host, port, username args."); + System.exit(1); + } + String host = args[0].trim(); + int port = Integer.parseInt(args[1]); + String username = args[2].trim(); + List configPaths = Config.getCommonConfigPaths(); configPaths.add(0, Path.of("client.yaml")); // Add this first so we create client.yaml if needed. - if (args.length > 0) { - configPaths.add(Path.of(args[0].trim())); + if (args.length > 3) { + configPaths.add(Path.of(args[3].trim())); } ClientConfig clientConfig = Config.loadConfig(ClientConfig.class, configPaths, new ClientConfig(), "default-config.yaml"); - Client client = new Client(clientConfig); + Client client = new Client(clientConfig, host, port, username); client.run(); } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java b/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java index 9c80b54..5942aee 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java +++ b/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java @@ -42,13 +42,11 @@ public class CommunicationHandler { this.client = client; } - public void establishConnection() throws IOException { + public void establishConnection(String host, int port, String username) throws IOException { if (socket != null && !socket.isClosed()) { socket.close(); } - InetAddress address = InetAddress.getByName(client.getConfig().serverHost); - int port = client.getConfig().serverPort; - String username = client.getConfig().username; + InetAddress address = InetAddress.getByName(host); System.out.printf("Connecting to server at %s, port %d, with username \"%s\"...%n", address, port, username); socket = new Socket(address, port); diff --git a/client/src/main/java/nl/andrewl/aos2_client/config/ClientConfig.java b/client/src/main/java/nl/andrewl/aos2_client/config/ClientConfig.java index 16b1b0b..b35e471 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/config/ClientConfig.java +++ b/client/src/main/java/nl/andrewl/aos2_client/config/ClientConfig.java @@ -1,9 +1,6 @@ package nl.andrewl.aos2_client.config; public class ClientConfig { - public String serverHost = "localhost"; - public int serverPort = 25565; - public String username = "player"; public InputConfig input = new InputConfig(); public DisplayConfig display = new DisplayConfig(); diff --git a/client/src/main/resources/default-config.yaml b/client/src/main/resources/default-config.yaml index 27086af..f5260ca 100644 --- a/client/src/main/resources/default-config.yaml +++ b/client/src/main/resources/default-config.yaml @@ -1,8 +1,4 @@ # Ace of Shades 2 Client Configuration -# Set these properties to connect to a server. -serverHost: localhost -serverPort: 25565 -username: player # Settings for input. input: diff --git a/launcher/package_linux.sh b/launcher/package_linux.sh new file mode 100755 index 0000000..45a15db --- /dev/null +++ b/launcher/package_linux.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +function join_by { + local d=${1-} f=${2-} + if shift 2; then + printf %s "$f" "${@/#/$d}" + fi +} + +mvn clean package javafx:jlink -DskipDebug=true -DstripJavaDebugAttributes=true -DnoHeaderFiles=true -DnoManPages=true + +cd target +module_jars=(lib/*) +eligible_main_jars=("*jar-with-dependencies.jar") +main_jar=(${eligible_main_jars[0]}) +module_path=$(join_by ";" ${module_jars[@]}) +module_path="$main_jar;$module_path" + +jpackage \ + --name "Ace of Shades Launcher" \ + --app-version "1.0.0" \ + --description "Launcher app for Ace of Shades, a voxel-based first-person shooter." \ + --linux-shortcut \ + --linux-deb-maintainer "andrewlalisofficial@gmail.com" \ + --linux-menu-group "Game" \ + --runtime-image image \ + --main-jar $main_jar \ + --main-class nl.andrewl.aos2_launcher.Launcher \ + --input . + diff --git a/launcher/pom.xml b/launcher/pom.xml index 00d1d5a..ddf40d6 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -11,8 +11,9 @@ 18 18 - 18.0.1 + 18.0.2 0.0.8 + UTF-8 @@ -53,6 +54,46 @@ 17 + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + + nl.andrewl.aos2_launcher.Launcher + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.8 + + + package + + copy-dependencies + + + ${project.build.directory}/lib + + + + diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/GameRunner.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/GameRunner.java new file mode 100644 index 0000000..0b5ec08 --- /dev/null +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/GameRunner.java @@ -0,0 +1,70 @@ +package nl.andrewl.aos2_launcher; + +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.stage.Window; +import nl.andrewl.aos2_launcher.model.Profile; +import nl.andrewl.aos2_launcher.model.ProgressReporter; +import nl.andrewl.aos2_launcher.model.Server; + +import java.io.IOException; +import java.nio.file.Path; + +public class GameRunner { + public void run(Profile profile, Server server, ProgressReporter progressReporter, Window owner) { + SystemVersionValidator.getJreExecutablePath(progressReporter) + .whenCompleteAsync((jrePath, throwable) -> { + if (throwable != null) { + showPopup( + owner, + Alert.AlertType.ERROR, + "An error occurred while ensuring that you've got the latest Java runtime: " + throwable.getMessage() + ); + } else { + VersionFetcher.INSTANCE.ensureVersionIsDownloaded(profile.getClientVersion(), progressReporter) + .whenCompleteAsync((clientJarPath, throwable2) -> { + progressReporter.disableProgress(); + if (throwable2 != null) { + showPopup( + owner, + Alert.AlertType.ERROR, + "An error occurred while ensuring you've got the correct client version: " + throwable2.getMessage() + ); + } else { + startGame(owner, profile, server, jrePath, clientJarPath); + } + }); + } + }); + } + + private void startGame(Window owner, Profile profile, Server server, Path jrePath, Path clientJarPath) { + try { + Process p = new ProcessBuilder() + .command( + jrePath.toAbsolutePath().toString(), + "-jar", clientJarPath.toAbsolutePath().toString(), + server.getHost(), + Integer.toString(server.getPort()), + profile.getUsername() + ) + .directory(profile.getDir().toFile()) + .inheritIO() + .start(); + p.wait(); + } catch (IOException e) { + showPopup(owner, Alert.AlertType.ERROR, "An error occurred while starting the game: " + e.getMessage()); + } catch (InterruptedException e) { + showPopup(owner, Alert.AlertType.ERROR, "The game was interrupted: " + e.getMessage()); + } + } + + private void showPopup(Window owner, Alert.AlertType type, String text) { + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.initOwner(owner); + alert.setContentText(text); + alert.show(); + }); + } +} diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java index 2106a3d..9aeb87b 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/Launcher.java @@ -17,23 +17,21 @@ public class Launcher extends Application { public static final Path BASE_DIR = Path.of(System.getProperty("user.home"), ".ace-of-shades"); public static final Path VERSIONS_DIR = BASE_DIR.resolve("versions"); public static final Path PROFILES_FILE = BASE_DIR.resolve("profiles.json"); + public static final Path PROFILES_DIR = BASE_DIR.resolve("profiles"); public static final Path JRE_PATH = BASE_DIR.resolve("jre"); @Override public void start(Stage stage) throws IOException { - if (!Files.exists(BASE_DIR)) { - Files.createDirectory(BASE_DIR); - } - if (!Files.exists(VERSIONS_DIR)) { - Files.createDirectory(VERSIONS_DIR); - } + if (!Files.exists(BASE_DIR)) Files.createDirectory(BASE_DIR); + if (!Files.exists(VERSIONS_DIR)) Files.createDirectory(VERSIONS_DIR); + if (!Files.exists(PROFILES_DIR)) Files.createDirectory(PROFILES_DIR); FXMLLoader loader = new FXMLLoader(Launcher.class.getResource("/main_view.fxml")); Parent rootNode = loader.load(); Scene scene = new Scene(rootNode); addStylesheet(scene, "/font/fonts.css"); addStylesheet(scene, "/styles.css"); stage.setScene(scene); - stage.setTitle("Ace of Shades 2 - Launcher"); + stage.setTitle("Ace of Shades - Launcher"); stage.show(); } 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 d1c5fa4..d7ba0e4 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/MainViewController.java @@ -2,29 +2,22 @@ package nl.andrewl.aos2_launcher; import javafx.application.Platform; import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.css.PseudoClass; +import javafx.collections.ListChangeListener; import javafx.fxml.FXML; -import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.control.ProgressIndicator; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.VBox; import nl.andrewl.aos2_launcher.model.Profile; import nl.andrewl.aos2_launcher.model.ProfileSet; import nl.andrewl.aos2_launcher.model.ProgressReporter; import nl.andrewl.aos2_launcher.model.Server; -import nl.andrewl.aos2_launcher.view.BindingUtil; import nl.andrewl.aos2_launcher.view.EditProfileDialog; +import nl.andrewl.aos2_launcher.view.ElementList; import nl.andrewl.aos2_launcher.view.ProfileView; import nl.andrewl.aos2_launcher.view.ServerView; -import java.io.IOException; import java.util.ArrayList; public class MainViewController implements ProgressReporter { @@ -32,80 +25,42 @@ public class MainViewController implements ProgressReporter { @FXML public Button editProfileButton; @FXML public Button removeProfileButton; @FXML public VBox profilesVBox; + private ElementList profilesList; @FXML public VBox serversVBox; - @FXML public Label selectedProfileLabel; - @FXML public Label selectedServerLabel; + private ElementList serversList; @FXML public VBox progressVBox; @FXML public Label progressLabel; @FXML public ProgressBar progressBar; private final ProfileSet profileSet = new ProfileSet(); - private final ObservableList servers = FXCollections.observableArrayList(); - private final ObjectProperty selectedServer = new SimpleObjectProperty<>(null); private final ServersFetcher serversFetcher = new ServersFetcher(); @FXML public void initialize() { - BindingUtil.mapContent(serversVBox.getChildren(), servers, ServerView::new); - BindingUtil.mapContent(profilesVBox.getChildren(), profileSet.getProfiles(), ProfileView::new); - profileSet.selectedProfileProperty().addListener((observable, oldValue, newValue) -> { - if (newValue == null) { - selectedProfileLabel.setText("None"); - } else { - selectedProfileLabel.setText(newValue.getName()); - } + profilesList = new ElementList<>(profilesVBox, ProfileView::new, ProfileView.class, ProfileView::getProfile); + profileSet.selectedProfileProperty().addListener((observable, oldValue, newValue) -> profileSet.save()); + // A hack since we can't bind the profilesList's elements to the profileSet's. + profileSet.getProfiles().addListener((ListChangeListener) c -> { + var selected = profileSet.getSelectedProfile(); + profilesList.clear(); + profilesList.addAll(profileSet.getProfiles()); + profilesList.selectElement(selected); }); - selectedServer.addListener((observable, oldValue, newValue) -> { - if (newValue == null) { - selectedServerLabel.setText("None"); - } else { - selectedServerLabel.setText(newValue.getName()); - } - }); - BooleanBinding playBind = profileSet.selectedProfileProperty().isNull().or(selectedServer.isNull()); + profileSet.loadOrCreateStandardFile(); + profilesList.selectElement(profileSet.getSelectedProfile()); + profileSet.selectedProfileProperty().bind(profilesList.selectedElementProperty()); + + serversList = new ElementList<>(serversVBox, ServerView::new, ServerView.class, ServerView::getServer); + + BooleanBinding playBind = profileSet.selectedProfileProperty().isNull().or(serversList.selectedElementProperty().isNull()); playButton.disableProperty().bind(playBind); editProfileButton.disableProperty().bind(profileSet.selectedProfileProperty().isNull()); removeProfileButton.disableProperty().bind(profileSet.selectedProfileProperty().isNull()); - profilesVBox.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { - Node target = (Node) event.getTarget(); - while (target != null) { - if (target instanceof ProfileView view) { - if (view.getProfile().equals(profileSet.getSelectedProfile()) && event.isControlDown()) { - selectProfile(null); - } else if (!event.isControlDown() && event.getClickCount() == 2) { - selectProfile(view); - editProfile(); - } else { - selectProfile(view); - } - return; - } - target = target.getParent(); - } - selectProfile(null); - }); - serversVBox.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { - Node target = (Node) event.getTarget(); - while (target != null) { - if (target instanceof ServerView view) { - if (view.getServer().equals(selectedServer.get()) && event.isControlDown()) { - selectServer(null); - } else { - selectServer(view); - } - return; - } - target = target.getParent(); - } - selectServer(null); - }); progressVBox.managedProperty().bind(progressVBox.visibleProperty()); progressVBox.setVisible(false); - profileSet.loadOrCreateStandardFile(); - updateProfileViewSelectedClass(); refreshServers(); } @@ -117,8 +72,8 @@ public class MainViewController implements ProgressReporter { return new ArrayList<>(); }) .thenAccept(newServers -> Platform.runLater(() -> { - this.servers.clear(); - this.servers.addAll(newServers); + serversList.clear(); + serversList.addAll(newServers); })); } @@ -142,58 +97,12 @@ public class MainViewController implements ProgressReporter { @FXML public void play() { - Profile profile = profileSet.getSelectedProfile(); - Server server = this.selectedServer.get(); - SystemVersionValidator.getJreExecutablePath(this) - .thenAccept(jrePath -> { - VersionFetcher.INSTANCE.ensureVersionIsDownloaded(profile.getClientVersion(), this) - .thenAccept(clientJarPath -> { - try { - Process p = new ProcessBuilder() - .command( - jrePath.toAbsolutePath().toString(), - "-jar", - clientJarPath.toAbsolutePath().toString() - ) - .directory(Launcher.BASE_DIR.toFile()) - .inheritIO() - .start(); - p.wait(); - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - }); - }); - } - - private void selectProfile(ProfileView view) { - Profile profile = view == null ? null : view.getProfile(); - profileSet.selectProfile(profile); - updateProfileViewSelectedClass(); - } - - private void updateProfileViewSelectedClass() { - PseudoClass selectedClass = PseudoClass.getPseudoClass("selected"); - for (var node : profilesVBox.getChildren()) { - ProfileView view = (ProfileView) node; - view.pseudoClassStateChanged(selectedClass, view.getProfile().equals(profileSet.getSelectedProfile())); - } - } - - private void selectServer(ServerView view) { - Server server = view == null ? null : view.getServer(); - selectedServer.set(server); - updateServerViewSelectedClass(); - } - - private void updateServerViewSelectedClass() { - PseudoClass selectedClass = PseudoClass.getPseudoClass("selected"); - for (var node : serversVBox.getChildren()) { - ServerView view = (ServerView) node; - view.pseudoClassStateChanged(selectedClass, view.getServer().equals(selectedServer.get())); - } + new GameRunner().run( + profileSet.getSelectedProfile(), + serversList.getSelectedElement(), + this, + this.profilesVBox.getScene().getWindow() + ); } @Override diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java index 01c087e..3e7ad7f 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/SystemVersionValidator.java @@ -62,8 +62,8 @@ public class SystemVersionValidator { progressReporter.setActionText("Downloading JRE..."); HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build(); HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().GET().timeout(Duration.ofMinutes(5)); - String preferredJreName = getPreferredJreName(); - String url = JRE_DOWNLOAD_URL + preferredJreName; + String jreArchiveName = getPreferredJreName(); + String url = JRE_DOWNLOAD_URL + jreArchiveName; HttpRequest req = requestBuilder.uri(URI.create(url)).build(); return httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofInputStream()) .thenApplyAsync(resp -> { @@ -74,7 +74,7 @@ public class SystemVersionValidator { FileUtils.deleteRecursive(Launcher.JRE_PATH); } Files.createDirectory(Launcher.JRE_PATH); - Path jreArchiveFile = Launcher.JRE_PATH.resolve(preferredJreName); + Path jreArchiveFile = Launcher.JRE_PATH.resolve(jreArchiveName); FileUtils.downloadWithProgress(jreArchiveFile, resp, progressReporter); progressReporter.setProgress(-1); // Indefinite progress. progressReporter.setActionText("Unpacking JRE..."); diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java index dcd0845..fec3670 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/VersionFetcher.java @@ -38,8 +38,6 @@ public class VersionFetcher { 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) { @@ -130,13 +128,12 @@ public class VersionFetcher { 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."); + throw new RuntimeException("Couldn't find a matching release asset for this system."); } else { - throw new RuntimeException("Error while requesting release assets."); + throw new RuntimeException("Error while requesting release assets from GitHub: " + resp.statusCode()); } }); return downloadUrlFuture.thenComposeAsync(asset -> { @@ -145,7 +142,6 @@ public class VersionFetcher { 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.ofInputStream()) .thenApplyAsync(resp -> { if (resp.statusCode() == 200) { @@ -157,7 +153,7 @@ public class VersionFetcher { } return file; } else { - throw new RuntimeException("Version download failed."); + throw new RuntimeException("Error while downloading release asset from GitHub: " + resp.statusCode()); } }); }); diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java index 85721c6..d9b989d 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/Profile.java @@ -2,24 +2,28 @@ package nl.andrewl.aos2_launcher.model; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import nl.andrewl.aos2_launcher.Launcher; +import java.nio.file.Path; import java.util.UUID; public class Profile { private final UUID id; private final StringProperty name; - private final StringProperty description; + private final StringProperty username; private final StringProperty clientVersion; + private final StringProperty jvmArgs; public Profile() { - this(UUID.randomUUID(), "", null, null); + this(UUID.randomUUID(), "", "Player", null, null); } - public Profile(UUID id, String name, String description, String clientVersion) { + public Profile(UUID id, String name, String username, String clientVersion, String jvmArgs) { this.id = id; this.name = new SimpleStringProperty(name); - this.description = new SimpleStringProperty(description); + this.username = new SimpleStringProperty(username); this.clientVersion = new SimpleStringProperty(clientVersion); + this.jvmArgs = new SimpleStringProperty(jvmArgs); } public UUID getId() { @@ -34,12 +38,12 @@ public class Profile { return name; } - public String getDescription() { - return description.get(); + public String getUsername() { + return username.get(); } - public StringProperty descriptionProperty() { - return description; + public StringProperty usernameProperty() { + return username; } public String getClientVersion() { @@ -50,15 +54,31 @@ public class Profile { return clientVersion; } + public String getJvmArgs() { + return jvmArgs.get(); + } + + public StringProperty jvmArgsProperty() { + return jvmArgs; + } + public void setName(String name) { this.name.set(name); } - public void setDescription(String description) { - this.description.set(description); + public void setUsername(String username) { + this.username.set(username); } public void setClientVersion(String clientVersion) { this.clientVersion.set(clientVersion); } + + public void setJvmArgs(String jvmArgs) { + this.jvmArgs.set(jvmArgs); + } + + public Path getDir() { + return Launcher.PROFILES_DIR.resolve(id.toString()); + } } diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ProfileSet.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ProfileSet.java index e45b7ed..1cfde68 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ProfileSet.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/model/ProfileSet.java @@ -6,6 +6,7 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import nl.andrewl.aos2_launcher.Launcher; +import nl.andrewl.aos2_launcher.util.FileUtils; import java.io.IOException; import java.nio.file.Files; @@ -32,16 +33,26 @@ public class ProfileSet { public void addNewProfile(Profile profile) { profiles.add(profile); - selectedProfile.set(profile); save(); + try { + if (!Files.exists(profile.getDir())) { + Files.createDirectory(profile.getDir()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } } public void removeProfile(Profile profile) { if (profile == null) return; boolean removed = profiles.remove(profile); if (removed) { - if (selectedProfile.get() != null && selectedProfile.get().equals(profile)) { - selectedProfile.set(null); + try { + if (Files.exists(profile.getDir())) { + FileUtils.deleteRecursive(profile.getDir()); + } + } catch (IOException e) { + throw new RuntimeException(e); } save(); } @@ -51,25 +62,27 @@ public class ProfileSet { removeProfile(getSelectedProfile()); } - public void selectProfile(Profile profile) { - if (!profiles.contains(profile)) return; - selectedProfile.set(profile); - } - public void load(Path file) throws IOException { try (var reader = Files.newBufferedReader(file)) { JsonObject data = new Gson().fromJson(reader, JsonObject.class); profiles.clear(); JsonElement selectedProfileIdElement = data.get("selectedProfileId"); - UUID selectedProfileId = selectedProfileIdElement.isJsonNull() ? null : UUID.fromString(selectedProfileIdElement.getAsString()); + UUID selectedProfileId = (selectedProfileIdElement == null || selectedProfileIdElement.isJsonNull()) + ? null + : UUID.fromString(selectedProfileIdElement.getAsString()); JsonArray profilesArray = data.getAsJsonArray("profiles"); for (JsonElement element : profilesArray) { JsonObject profileObj = element.getAsJsonObject(); UUID id = UUID.fromString(profileObj.get("id").getAsString()); String name = profileObj.get("name").getAsString(); - String description = profileObj.get("description").getAsString(); String clientVersion = profileObj.get("clientVersion").getAsString(); - Profile profile = new Profile(id, name, description, clientVersion); + String username = profileObj.get("username").getAsString(); + JsonElement jvmArgsElement = profileObj.get("jvmArgs"); + String jvmArgs = null; + if (jvmArgsElement != null && jvmArgsElement.isJsonPrimitive() && jvmArgsElement.getAsJsonPrimitive().isString()) { + jvmArgs = jvmArgsElement.getAsString(); + } + Profile profile = new Profile(id, name, username, clientVersion, jvmArgs); profiles.add(profile); if (selectedProfileId != null && selectedProfileId.equals(profile.getId())) { selectedProfile.set(profile); @@ -106,8 +119,9 @@ public class ProfileSet { JsonObject obj = new JsonObject(); obj.addProperty("id", profile.getId().toString()); obj.addProperty("name", profile.getName()); - obj.addProperty("description", profile.getDescription()); + obj.addProperty("username", profile.getUsername()); obj.addProperty("clientVersion", profile.getClientVersion()); + obj.addProperty("jvmArgs", profile.getJvmArgs()); profilesArray.add(obj); } data.add("profiles", profilesArray); @@ -122,7 +136,7 @@ public class ProfileSet { try { save(lastFileUsed); } catch (IOException e) { - e.printStackTrace(); + throw new RuntimeException(e); } } } 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 21510b0..ef765d8 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 @@ -19,12 +19,10 @@ import java.io.IOException; import java.util.Objects; public class EditProfileDialog extends Dialog { - @FXML - public TextField nameField; - @FXML - public TextArea descriptionTextArea; - @FXML - public ChoiceBox clientVersionChoiceBox; + @FXML public TextField nameField; + @FXML public TextField usernameField; + @FXML public ChoiceBox clientVersionChoiceBox; + @FXML public TextArea jvmArgsTextArea; private final ObjectProperty profile; @@ -40,18 +38,23 @@ public class EditProfileDialog extends Dialog { setTitle("Edit Profile"); BooleanBinding formInvalid = nameField.textProperty().isEmpty() - .or(clientVersionChoiceBox.valueProperty().isNull()); + .or(clientVersionChoiceBox.valueProperty().isNull()) + .or(usernameField.textProperty().isEmpty()); nameField.setText(profile.getName()); - descriptionTextArea.setText(profile.getDescription()); - VersionFetcher.INSTANCE.getAvailableReleases().thenAccept(releases -> { - Platform.runLater(() -> { - clientVersionChoiceBox.setItems(FXCollections.observableArrayList(releases.stream().map(ClientVersionRelease::tag).toList())); + usernameField.setText(profile.getUsername()); + VersionFetcher.INSTANCE.getAvailableReleases().thenAccept(releases -> Platform.runLater(() -> { + clientVersionChoiceBox.setItems(FXCollections.observableArrayList(releases.stream().map(ClientVersionRelease::tag).toList())); + // If the profile doesn't have a set version, use the latest release. + if (profile.getClientVersion() == null || profile.getClientVersion().isBlank()) { String lastRelease = releases.size() == 0 ? null : releases.get(0).tag(); if (lastRelease != null) { clientVersionChoiceBox.setValue(lastRelease); } - }); - }); + } else { + clientVersionChoiceBox.setValue(profile.getClientVersion()); + } + })); + jvmArgsTextArea.setText(profile.getJvmArgs()); DialogPane pane = new DialogPane(); pane.setContent(parent); @@ -67,13 +70,9 @@ public class EditProfileDialog extends Dialog { } var prof = this.profile.getValue(); prof.setName(nameField.getText().trim()); - String descriptionText = descriptionTextArea.getText().trim(); - if (descriptionText.isBlank()) { - prof.setDescription(null); - } else { - prof.setDescription(descriptionText); - } + prof.setUsername(usernameField.getText().trim()); prof.setClientVersion(clientVersionChoiceBox.getValue()); + prof.setJvmArgs(jvmArgsTextArea.getText()); return this.profile.getValue(); }); setOnShowing(event -> Platform.runLater(() -> nameField.requestFocus())); diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ElementList.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ElementList.java new file mode 100644 index 0000000..5c5ff9a --- /dev/null +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ElementList.java @@ -0,0 +1,111 @@ +package nl.andrewl.aos2_launcher.view; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.css.PseudoClass; +import javafx.scene.Node; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Pane; + +import java.util.Collection; +import java.util.function.Function; + +public class ElementList { + private final Pane container; + + private final ObjectProperty selectedElement = new SimpleObjectProperty<>(null); + private final ObservableList elements = FXCollections.observableArrayList(); + private final Class elementViewType; + private final Function viewElementMapper; + + public ElementList( + Pane container, + Function elementViewMapper, + Class elementViewType, + Function viewElementMapper + ) { + this.container = container; + this.elementViewType = elementViewType; + this.viewElementMapper = viewElementMapper; + BindingUtil.mapContent(container.getChildren(), elements, element -> { + V view = elementViewMapper.apply(element); + view.getStyleClass().add("element-list-item"); + return view; + }); + container.addEventHandler(MouseEvent.MOUSE_CLICKED, this::handleMouseClick); + } + + @SuppressWarnings("unchecked") + private void handleMouseClick(MouseEvent event) { + Node target = (Node) event.getTarget(); + while (target != null) { + if (target.getClass().equals(elementViewType)) { + V elementView = (V) target; + T targetElement = viewElementMapper.apply(elementView); + if (event.isControlDown()) { + if (selectedElement.get() == null) { + selectElement(targetElement); + } else { + selectElement(null); + } + } else { + selectElement(targetElement); + } + return; // Exit since we found a valid target. + } + target = target.getParent(); + } + selectElement(null); + } + + public void selectElement(T element) { + if (element != null && !elements.contains(element)) return; + selectedElement.set(element); + updateSelectedPseudoClass(); + } + + @SuppressWarnings("unchecked") + private void updateSelectedPseudoClass() { + PseudoClass selectedClass = PseudoClass.getPseudoClass("selected"); + for (var node : container.getChildren()) { + if (!node.getClass().equals(elementViewType)) continue; + V view = (V) node; + T thisElement = viewElementMapper.apply(view); + view.pseudoClassStateChanged(selectedClass, thisElement.equals(selectedElement.get())); + } + } + + public T getSelectedElement() { + return selectedElement.get(); + } + + public ObjectProperty selectedElementProperty() { + return selectedElement; + } + + public ObservableList getElements() { + return elements; + } + + public void clear() { + elements.clear(); + selectElement(null); + } + + public void add(T element) { + elements.add(element); + } + + public void addAll(Collection newElements) { + elements.addAll(newElements); + } + + public void remove(T element) { + elements.remove(element); + if (element != null && element.equals(selectedElement.get())) { + selectElement(null); + } + } +} diff --git a/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ProfileView.java b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ProfileView.java index 7d0a2bd..c9ef5af 100644 --- a/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ProfileView.java +++ b/launcher/src/main/java/nl/andrewl/aos2_launcher/view/ProfileView.java @@ -1,22 +1,35 @@ package nl.andrewl.aos2_launcher.view; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; import javafx.scene.control.Label; -import javafx.scene.layout.VBox; +import javafx.scene.layout.Pane; import nl.andrewl.aos2_launcher.model.Profile; -public class ProfileView extends VBox { +import java.io.IOException; + +public class ProfileView extends Pane { private final Profile profile; + @FXML public Label nameLabel; + @FXML public Label clientVersionLabel; + @FXML public Label usernameLabel; + public ProfileView(Profile profile) { this.profile = profile; - var nameLabel = new Label(); + + try { + FXMLLoader loader = new FXMLLoader(ProfileView.class.getResource("/profile_view.fxml")); + loader.setController(this); + Node node = loader.load(); + getChildren().add(node); + } catch (IOException e) { + throw new RuntimeException(e); + } nameLabel.textProperty().bind(profile.nameProperty()); - var descriptionLabel = new Label(); - descriptionLabel.textProperty().bind(profile.descriptionProperty()); - var versionLabel = new Label(); - versionLabel.textProperty().bind(profile.clientVersionProperty()); - getChildren().addAll(nameLabel, descriptionLabel, versionLabel); - getStyleClass().add("list-item"); + clientVersionLabel.textProperty().bind(profile.clientVersionProperty()); + usernameLabel.textProperty().bind(profile.usernameProperty()); } public Profile getProfile() { diff --git a/launcher/src/main/resources/dialog/edit_profile.fxml b/launcher/src/main/resources/dialog/edit_profile.fxml index 7c28f79..ccd8501 100644 --- a/launcher/src/main/resources/dialog/edit_profile.fxml +++ b/launcher/src/main/resources/dialog/edit_profile.fxml @@ -10,16 +10,20 @@ - -