Compare commits
	
		
			23 Commits
		
	
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						11be12d595 | |
| 
							
							
								
									
								
								 | 
						0161bd5479 | |
| 
							
							
								
									
								
								 | 
						89ec1ddccc | |
| 
							
							
								
									
								
								 | 
						60d45a071f | |
| 
							
							
								
									
								
								 | 
						38b01f0531 | |
| 
							
							
								
									
								
								 | 
						782f32be8f | |
| 
							
							
								
									
								
								 | 
						da5ab070a8 | |
| 
							
							
								
									
								
								 | 
						4d7b01d4ae | |
| 
							
							
								
									
								
								 | 
						19d6dc7a5e | |
| 
							
							
								
									
								
								 | 
						99a1ef3441 | |
| 
							
							
								
									
								
								 | 
						f7959796b4 | |
| 
							
							
								
									
								
								 | 
						16b7a1e653 | |
| 
							
							
								
									
								
								 | 
						359d1aa1b8 | |
| 
							
							
								
									
								
								 | 
						8cfdf32bc0 | |
| 
							
							
								
									
								
								 | 
						4db1ecd191 | |
| 
							
							
								
									
								
								 | 
						c4a4479602 | |
| 
							
							
								
									
								
								 | 
						c10dd7cd02 | |
| 
							
							
								
									
								
								 | 
						e38ae65ff9 | |
| 
							
							
								
									
								
								 | 
						a808ac1920 | |
| 
							
							
								
									
								
								 | 
						5e462869a7 | |
| 
							
							
								
									
								
								 | 
						9aeb5cd048 | |
| 
							
							
								
									
								
								 | 
						20295596fa | |
| 
							
							
								
									
								
								 | 
						352faff005 | 
| 
						 | 
					@ -5,7 +5,7 @@
 | 
				
			||||||
    <parent>
 | 
					    <parent>
 | 
				
			||||||
        <artifactId>ace-of-shades-2</artifactId>
 | 
					        <artifactId>ace-of-shades-2</artifactId>
 | 
				
			||||||
        <groupId>nl.andrewl</groupId>
 | 
					        <groupId>nl.andrewl</groupId>
 | 
				
			||||||
        <version>1.3.0</version>
 | 
					        <version>1.5.0</version>
 | 
				
			||||||
    </parent>
 | 
					    </parent>
 | 
				
			||||||
    <modelVersion>4.0.0</modelVersion>
 | 
					    <modelVersion>4.0.0</modelVersion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
package nl.andrewl.aos2_client;
 | 
					package nl.andrewl.aos2_client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import nl.andrewl.aos2_client.config.ClientConfig;
 | 
					import nl.andrewl.aos2_client.config.ClientConfig;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_client.config.ConnectConfig;
 | 
				
			||||||
import nl.andrewl.aos2_client.control.InputHandler;
 | 
					import nl.andrewl.aos2_client.control.InputHandler;
 | 
				
			||||||
import nl.andrewl.aos2_client.model.Chat;
 | 
					import nl.andrewl.aos2_client.model.Chat;
 | 
				
			||||||
import nl.andrewl.aos2_client.model.ClientPlayer;
 | 
					import nl.andrewl.aos2_client.model.ClientPlayer;
 | 
				
			||||||
| 
						 | 
					@ -26,7 +27,9 @@ import java.util.concurrent.ConcurrentHashMap;
 | 
				
			||||||
import java.util.concurrent.ConcurrentLinkedQueue;
 | 
					import java.util.concurrent.ConcurrentLinkedQueue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class Client implements Runnable {
 | 
					public class Client implements Runnable {
 | 
				
			||||||
	private final ClientConfig config;
 | 
						public final ConnectConfig connectConfig;
 | 
				
			||||||
 | 
						public final ClientConfig config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private final CommunicationHandler communicationHandler;
 | 
						private final CommunicationHandler communicationHandler;
 | 
				
			||||||
	private final InputHandler inputHandler;
 | 
						private final InputHandler inputHandler;
 | 
				
			||||||
	private final Camera camera;
 | 
						private final Camera camera;
 | 
				
			||||||
| 
						 | 
					@ -42,8 +45,9 @@ public class Client implements Runnable {
 | 
				
			||||||
	private final Chat chat;
 | 
						private final Chat chat;
 | 
				
			||||||
	private final Queue<Runnable> mainThreadActions;
 | 
						private final Queue<Runnable> mainThreadActions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public Client(ClientConfig config) {
 | 
						public Client(ClientConfig config, ConnectConfig connectConfig) {
 | 
				
			||||||
		this.config = config;
 | 
							this.config = config;
 | 
				
			||||||
 | 
							this.connectConfig = connectConfig;
 | 
				
			||||||
		this.camera = new Camera();
 | 
							this.camera = new Camera();
 | 
				
			||||||
		this.players = new ConcurrentHashMap<>();
 | 
							this.players = new ConcurrentHashMap<>();
 | 
				
			||||||
		this.teams = new ConcurrentHashMap<>();
 | 
							this.teams = new ConcurrentHashMap<>();
 | 
				
			||||||
| 
						 | 
					@ -156,6 +160,14 @@ public class Client implements Runnable {
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		} else if (msg instanceof PlayerLeaveMessage leaveMessage) {
 | 
							} else if (msg instanceof PlayerLeaveMessage leaveMessage) {
 | 
				
			||||||
			runLater(() -> players.remove(leaveMessage.id()));
 | 
								runLater(() -> players.remove(leaveMessage.id()));
 | 
				
			||||||
 | 
							} else if (msg instanceof PlayerTeamUpdateMessage teamUpdateMessage) {
 | 
				
			||||||
 | 
								runLater(() -> {
 | 
				
			||||||
 | 
									OtherPlayer op = players.get(teamUpdateMessage.playerId());
 | 
				
			||||||
 | 
									Team team = teamUpdateMessage.teamId() == -1 ? null : teams.get(teamUpdateMessage.teamId());
 | 
				
			||||||
 | 
									if (op != null) {
 | 
				
			||||||
 | 
										op.setTeam(team);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
		} else if (msg instanceof SoundMessage soundMessage) {
 | 
							} else if (msg instanceof SoundMessage soundMessage) {
 | 
				
			||||||
			if (soundManager != null) {
 | 
								if (soundManager != null) {
 | 
				
			||||||
				soundManager.play(
 | 
									soundManager.play(
 | 
				
			||||||
| 
						 | 
					@ -257,13 +269,22 @@ public class Client implements Runnable {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static void main(String[] args) throws IOException {
 | 
						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();
 | 
				
			||||||
 | 
							ConnectConfig connectCfg = new ConnectConfig(host, port, username, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		List<Path> configPaths = Config.getCommonConfigPaths();
 | 
							List<Path> configPaths = Config.getCommonConfigPaths();
 | 
				
			||||||
		configPaths.add(0, Path.of("client.yaml")); // Add this first so we create client.yaml if needed.
 | 
							configPaths.add(0, Path.of("client.yaml")); // Add this first so we create client.yaml if needed.
 | 
				
			||||||
		if (args.length > 0) {
 | 
							if (args.length > 3) {
 | 
				
			||||||
			configPaths.add(Path.of(args[0].trim()));
 | 
								configPaths.add(Path.of(args[3].trim()));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		ClientConfig clientConfig = Config.loadConfig(ClientConfig.class, configPaths, new ClientConfig(), "default-config.yaml");
 | 
							ClientConfig clientConfig = Config.loadConfig(ClientConfig.class, configPaths, new ClientConfig(), "default-config.yaml");
 | 
				
			||||||
		Client client = new Client(clientConfig);
 | 
							Client client = new Client(clientConfig, connectCfg);
 | 
				
			||||||
		client.run();
 | 
							client.run();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,8 @@ import nl.andrewl.aos_core.model.Team;
 | 
				
			||||||
import nl.andrewl.aos_core.model.item.ItemStack;
 | 
					import nl.andrewl.aos_core.model.item.ItemStack;
 | 
				
			||||||
import nl.andrewl.aos_core.model.world.World;
 | 
					import nl.andrewl.aos_core.model.world.World;
 | 
				
			||||||
import nl.andrewl.aos_core.model.world.WorldIO;
 | 
					import nl.andrewl.aos_core.model.world.WorldIO;
 | 
				
			||||||
import nl.andrewl.aos_core.net.*;
 | 
					import nl.andrewl.aos_core.net.TcpReceiver;
 | 
				
			||||||
 | 
					import nl.andrewl.aos_core.net.UdpReceiver;
 | 
				
			||||||
import nl.andrewl.aos_core.net.connect.ConnectAcceptMessage;
 | 
					import nl.andrewl.aos_core.net.connect.ConnectAcceptMessage;
 | 
				
			||||||
import nl.andrewl.aos_core.net.connect.ConnectRejectMessage;
 | 
					import nl.andrewl.aos_core.net.connect.ConnectRejectMessage;
 | 
				
			||||||
import nl.andrewl.aos_core.net.connect.ConnectRequestMessage;
 | 
					import nl.andrewl.aos_core.net.connect.ConnectRequestMessage;
 | 
				
			||||||
| 
						 | 
					@ -46,16 +47,14 @@ public class CommunicationHandler {
 | 
				
			||||||
		if (socket != null && !socket.isClosed()) {
 | 
							if (socket != null && !socket.isClosed()) {
 | 
				
			||||||
			socket.close();
 | 
								socket.close();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		InetAddress address = InetAddress.getByName(client.getConfig().serverHost);
 | 
							InetAddress address = InetAddress.getByName(client.connectConfig.host());
 | 
				
			||||||
		int port = client.getConfig().serverPort;
 | 
							System.out.printf("Connecting to server at %s, port %d, with username \"%s\"...%n", address, client.connectConfig.port(), client.connectConfig.username());
 | 
				
			||||||
		String username = client.getConfig().username;
 | 
					 | 
				
			||||||
		System.out.printf("Connecting to server at %s, port %d, with username \"%s\"...%n", address, port, username);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		socket = new Socket(address, port);
 | 
							socket = new Socket(address, client.connectConfig.port());
 | 
				
			||||||
		socket.setSoTimeout(1000);
 | 
							socket.setSoTimeout(1000);
 | 
				
			||||||
		in = Net.getInputStream(socket.getInputStream());
 | 
							in = Net.getInputStream(socket.getInputStream());
 | 
				
			||||||
		out = Net.getOutputStream(socket.getOutputStream());
 | 
							out = Net.getOutputStream(socket.getOutputStream());
 | 
				
			||||||
		Net.write(new ConnectRequestMessage(username), out);
 | 
							Net.write(new ConnectRequestMessage(client.connectConfig.username(), client.connectConfig.spectator()), out);
 | 
				
			||||||
		Message response = Net.read(in);
 | 
							Message response = Net.read(in);
 | 
				
			||||||
		socket.setSoTimeout(0);
 | 
							socket.setSoTimeout(0);
 | 
				
			||||||
		if (response instanceof ConnectRejectMessage rejectMessage) {
 | 
							if (response instanceof ConnectRejectMessage rejectMessage) {
 | 
				
			||||||
| 
						 | 
					@ -63,7 +62,7 @@ public class CommunicationHandler {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (response instanceof ConnectAcceptMessage acceptMessage) {
 | 
							if (response instanceof ConnectAcceptMessage acceptMessage) {
 | 
				
			||||||
			this.clientId = acceptMessage.clientId();
 | 
								this.clientId = acceptMessage.clientId();
 | 
				
			||||||
			client.setMyPlayer(new ClientPlayer(clientId, username));
 | 
								client.setMyPlayer(new ClientPlayer(clientId, client.connectConfig.username()));
 | 
				
			||||||
			receiveInitialData();
 | 
								receiveInitialData();
 | 
				
			||||||
			establishDatagramConnection();
 | 
								establishDatagramConnection();
 | 
				
			||||||
			new Thread(new TcpReceiver(in, client::onMessageReceived).withShutdownHook(this::shutdown)).start();
 | 
								new Thread(new TcpReceiver(in, client::onMessageReceived).withShutdownHook(this::shutdown)).start();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,6 @@
 | 
				
			||||||
package nl.andrewl.aos2_client.config;
 | 
					package nl.andrewl.aos2_client.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class ClientConfig {
 | 
					public class ClientConfig {
 | 
				
			||||||
	public String serverHost = "localhost";
 | 
					 | 
				
			||||||
	public int serverPort = 25565;
 | 
					 | 
				
			||||||
	public String username = "player";
 | 
					 | 
				
			||||||
	public InputConfig input = new InputConfig();
 | 
						public InputConfig input = new InputConfig();
 | 
				
			||||||
	public DisplayConfig display = new DisplayConfig();
 | 
						public DisplayConfig display = new DisplayConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_client.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * The data that's needed by the client to initially establish a connection.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record ConnectConfig(
 | 
				
			||||||
 | 
							String host,
 | 
				
			||||||
 | 
							int port,
 | 
				
			||||||
 | 
							String username,
 | 
				
			||||||
 | 
							boolean spectator
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,4 @@
 | 
				
			||||||
# Ace of Shades 2 Client Configuration
 | 
					# Ace of Shades 2 Client Configuration
 | 
				
			||||||
# Set these properties to connect to a server.
 | 
					 | 
				
			||||||
serverHost: localhost
 | 
					 | 
				
			||||||
serverPort: 25565
 | 
					 | 
				
			||||||
username: player
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Settings for input.
 | 
					# Settings for input.
 | 
				
			||||||
input:
 | 
					input:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@
 | 
				
			||||||
    <parent>
 | 
					    <parent>
 | 
				
			||||||
        <artifactId>ace-of-shades-2</artifactId>
 | 
					        <artifactId>ace-of-shades-2</artifactId>
 | 
				
			||||||
        <groupId>nl.andrewl</groupId>
 | 
					        <groupId>nl.andrewl</groupId>
 | 
				
			||||||
        <version>1.3.0</version>
 | 
					        <version>1.5.0</version>
 | 
				
			||||||
    </parent>
 | 
					    </parent>
 | 
				
			||||||
    <modelVersion>4.0.0</modelVersion>
 | 
					    <modelVersion>4.0.0</modelVersion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,29 +26,37 @@ public final class Net {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static final Serializer serializer = new Serializer();
 | 
						private static final Serializer serializer = new Serializer();
 | 
				
			||||||
	static {
 | 
						static {
 | 
				
			||||||
		serializer.registerType(1, ConnectRequestMessage.class);
 | 
							int i = 1;
 | 
				
			||||||
		serializer.registerType(2, ConnectAcceptMessage.class);
 | 
							// Basic protocol messages.
 | 
				
			||||||
		serializer.registerType(3, ConnectRejectMessage.class);
 | 
							serializer.registerType(i++, ConnectRequestMessage.class);
 | 
				
			||||||
		serializer.registerType(4, DatagramInit.class);
 | 
							serializer.registerType(i++, ConnectAcceptMessage.class);
 | 
				
			||||||
		serializer.registerType(5, ChunkHashMessage.class);
 | 
							serializer.registerType(i++, ConnectRejectMessage.class);
 | 
				
			||||||
		serializer.registerType(6, ChunkDataMessage.class);
 | 
							serializer.registerType(i++, DatagramInit.class);
 | 
				
			||||||
		serializer.registerType(7, ChunkUpdateMessage.class);
 | 
					
 | 
				
			||||||
		serializer.registerType(8, ClientInputState.class);
 | 
							// World messages.
 | 
				
			||||||
		serializer.registerType(9, ClientOrientationState.class);
 | 
							serializer.registerType(i++, ChunkHashMessage.class);
 | 
				
			||||||
		serializer.registerType(10, PlayerUpdateMessage.class);
 | 
							serializer.registerType(i++, ChunkDataMessage.class);
 | 
				
			||||||
		serializer.registerType(11, PlayerJoinMessage.class);
 | 
							serializer.registerType(i++, ChunkUpdateMessage.class);
 | 
				
			||||||
		serializer.registerType(12, PlayerLeaveMessage.class);
 | 
							serializer.registerType(i++, ProjectileMessage.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Player/client messages.
 | 
				
			||||||
 | 
							serializer.registerType(i++, ClientInputState.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, ClientOrientationState.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, ClientHealthMessage.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, PlayerUpdateMessage.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, PlayerJoinMessage.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, PlayerLeaveMessage.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, PlayerTeamUpdateMessage.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, BlockColorMessage.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, InventorySelectedStackMessage.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, ChatMessage.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, ChatWrittenMessage.class);
 | 
				
			||||||
 | 
							serializer.registerType(i++, ClientRecoilMessage.class);
 | 
				
			||||||
		// Separate serializers for client inventory messages.
 | 
							// Separate serializers for client inventory messages.
 | 
				
			||||||
		serializer.registerTypeSerializer(13, new InventorySerializer());
 | 
							serializer.registerTypeSerializer(i++, new InventorySerializer());
 | 
				
			||||||
		serializer.registerTypeSerializer(14, new ItemStackSerializer());
 | 
							serializer.registerTypeSerializer(i++, new ItemStackSerializer());
 | 
				
			||||||
		serializer.registerType(15, InventorySelectedStackMessage.class);
 | 
					
 | 
				
			||||||
		serializer.registerType(16, SoundMessage.class);
 | 
							serializer.registerType(i++, SoundMessage.class);
 | 
				
			||||||
		serializer.registerType(17, ProjectileMessage.class);
 | 
					 | 
				
			||||||
		serializer.registerType(18, ClientHealthMessage.class);
 | 
					 | 
				
			||||||
		serializer.registerType(19, BlockColorMessage.class);
 | 
					 | 
				
			||||||
		serializer.registerType(20, ChatMessage.class);
 | 
					 | 
				
			||||||
		serializer.registerType(21, ChatWrittenMessage.class);
 | 
					 | 
				
			||||||
		serializer.registerType(22, ClientRecoilMessage.class);
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static ExtendedDataInputStream getInputStream(InputStream in) {
 | 
						public static ExtendedDataInputStream getInputStream(InputStream in) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos_core.net.client;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.record_net.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * A message that's sent by the server to announce that a player has changed to
 | 
				
			||||||
 | 
					 * a specified team. Both the player and team should already be recognized by
 | 
				
			||||||
 | 
					 * all clients; otherwise they can ignore this.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record PlayerTeamUpdateMessage(
 | 
				
			||||||
 | 
							int playerId,
 | 
				
			||||||
 | 
							int teamId
 | 
				
			||||||
 | 
					) implements Message {}
 | 
				
			||||||
| 
						 | 
					@ -2,4 +2,10 @@ package nl.andrewl.aos_core.net.connect;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import nl.andrewl.record_net.Message;
 | 
					import nl.andrewl.record_net.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public record ConnectRequestMessage(String username) implements Message {}
 | 
					/**
 | 
				
			||||||
 | 
					 * The first message that a client sends via TCP to the server, to indicate
 | 
				
			||||||
 | 
					 * that they'd like to join.
 | 
				
			||||||
 | 
					 * @param username The player's chosen username.
 | 
				
			||||||
 | 
					 * @param spectator Whether the player wants to be a spectator.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record ConnectRequestMessage(String username, boolean spectator) implements Message {}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 31 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.7 KiB  | 
| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					cd target
 | 
				
			||||||
 | 
					module_jars=(lib/*)
 | 
				
			||||||
 | 
					eligible_main_jars=("*.jar")
 | 
				
			||||||
 | 
					main_jar=(${eligible_main_jars[0]})
 | 
				
			||||||
 | 
					module_path=$(join_by ":" ${module_jars[@]})
 | 
				
			||||||
 | 
					module_path="$main_jar:$module_path"
 | 
				
			||||||
 | 
					echo $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." \
 | 
				
			||||||
 | 
					  --icon ../icon.ico \
 | 
				
			||||||
 | 
					  --linux-shortcut \
 | 
				
			||||||
 | 
					  --linux-deb-maintainer "andrewlalisofficial@gmail.com" \
 | 
				
			||||||
 | 
					  --linux-menu-group "Game" \
 | 
				
			||||||
 | 
					  --linux-app-category "Game" \
 | 
				
			||||||
 | 
					  --module-path "$module_path" \
 | 
				
			||||||
 | 
					  --module aos2_launcher/nl.andrewl.aos2_launcher.Launcher \
 | 
				
			||||||
 | 
					  --add-modules jdk.crypto.cryptoki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,42 @@
 | 
				
			||||||
 | 
					# This script prepares and runs the jpackage command to generate a Windows AOS Client installer.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$projectDir = $PSScriptRoot
 | 
				
			||||||
 | 
					Push-Location $projectDir\target
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Remove existing file if it exists.
 | 
				
			||||||
 | 
					Write-Output "Removing existing exe file."
 | 
				
			||||||
 | 
					Get-ChildItem *.exe | ForEach-Object { Remove-Item -Path $_.FullName -Force }
 | 
				
			||||||
 | 
					Write-Output "Done."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Run the build
 | 
				
			||||||
 | 
					Write-Output "Building the project."
 | 
				
			||||||
 | 
					Push-Location $projectDir
 | 
				
			||||||
 | 
					mvn clean package
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Get list of dependency modules that maven copied into the lib directory.
 | 
				
			||||||
 | 
					Push-Location $projectDir\target
 | 
				
			||||||
 | 
					$modules = Get-ChildItem -Path lib -Name | ForEach-Object { "lib\$_" }
 | 
				
			||||||
 | 
					# Add our own main module.
 | 
				
			||||||
 | 
					$mainModuleJar = Get-ChildItem -Name -Include "aos2-launcher-*.jar" -Exclude "*-jar-with-dependencies.jar"
 | 
				
			||||||
 | 
					$modules += $mainModuleJar
 | 
				
			||||||
 | 
					Write-Output "Found modules: $modules"
 | 
				
			||||||
 | 
					$modulePath = $modules -join ';'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Write-Output "Running jpackage..."
 | 
				
			||||||
 | 
					jpackage `
 | 
				
			||||||
 | 
					 --type msi `
 | 
				
			||||||
 | 
					 --name "Ace-of-Shades" `
 | 
				
			||||||
 | 
					 --app-version "1.0.0" `
 | 
				
			||||||
 | 
					 --description "Top-down 2D shooter game inspired by Ace of Spades." `
 | 
				
			||||||
 | 
					 --icon ..\icon.ico `
 | 
				
			||||||
 | 
					 --win-shortcut `
 | 
				
			||||||
 | 
					 --win-dir-chooser `
 | 
				
			||||||
 | 
					 --win-per-user-install `
 | 
				
			||||||
 | 
					 --win-menu `
 | 
				
			||||||
 | 
					 --win-shortcut `
 | 
				
			||||||
 | 
					 --win-menu-group "Game" `
 | 
				
			||||||
 | 
					 --module-path "$modulePath" `
 | 
				
			||||||
 | 
					 --module aos2_launcher/nl.andrewl.aos2_launcher.Launcher `
 | 
				
			||||||
 | 
					 --add-modules jdk.crypto.cryptoki
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Write-Output "Done!"
 | 
				
			||||||
| 
						 | 
					@ -11,8 +11,9 @@
 | 
				
			||||||
    <properties>
 | 
					    <properties>
 | 
				
			||||||
        <maven.compiler.source>18</maven.compiler.source>
 | 
					        <maven.compiler.source>18</maven.compiler.source>
 | 
				
			||||||
        <maven.compiler.target>18</maven.compiler.target>
 | 
					        <maven.compiler.target>18</maven.compiler.target>
 | 
				
			||||||
        <javafx.version>18.0.1</javafx.version>
 | 
					        <javafx.version>18.0.2</javafx.version>
 | 
				
			||||||
        <javafx.maven.plugin.version>0.0.8</javafx.maven.plugin.version>
 | 
					        <javafx.maven.plugin.version>0.0.8</javafx.maven.plugin.version>
 | 
				
			||||||
 | 
					        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 | 
				
			||||||
    </properties>
 | 
					    </properties>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <dependencies>
 | 
					    <dependencies>
 | 
				
			||||||
| 
						 | 
					@ -26,6 +27,12 @@
 | 
				
			||||||
            <artifactId>javafx-fxml</artifactId>
 | 
					            <artifactId>javafx-fxml</artifactId>
 | 
				
			||||||
            <version>${javafx.version}</version>
 | 
					            <version>${javafx.version}</version>
 | 
				
			||||||
        </dependency>
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>com.google.code.gson</groupId>
 | 
				
			||||||
 | 
					            <artifactId>gson</artifactId>
 | 
				
			||||||
 | 
					            <version>2.9.1</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
    </dependencies>
 | 
					    </dependencies>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <build>
 | 
					    <build>
 | 
				
			||||||
| 
						 | 
					@ -47,6 +54,46 @@
 | 
				
			||||||
                    <target>17</target>
 | 
					                    <target>17</target>
 | 
				
			||||||
                </configuration>
 | 
					                </configuration>
 | 
				
			||||||
            </plugin>
 | 
					            </plugin>
 | 
				
			||||||
 | 
					            <plugin>
 | 
				
			||||||
 | 
					                <groupId>org.apache.maven.plugins</groupId>
 | 
				
			||||||
 | 
					                <artifactId>maven-assembly-plugin</artifactId>
 | 
				
			||||||
 | 
					                <version>3.3.0</version>
 | 
				
			||||||
 | 
					                <configuration>
 | 
				
			||||||
 | 
					                    <archive>
 | 
				
			||||||
 | 
					                        <manifest>
 | 
				
			||||||
 | 
					                            <mainClass>nl.andrewl.aos2_launcher.Launcher</mainClass>
 | 
				
			||||||
 | 
					                        </manifest>
 | 
				
			||||||
 | 
					                    </archive>
 | 
				
			||||||
 | 
					                    <descriptorRefs>
 | 
				
			||||||
 | 
					                        <descriptorRef>jar-with-dependencies</descriptorRef>
 | 
				
			||||||
 | 
					                    </descriptorRefs>
 | 
				
			||||||
 | 
					                </configuration>
 | 
				
			||||||
 | 
					                <executions>
 | 
				
			||||||
 | 
					                    <execution>
 | 
				
			||||||
 | 
					                        <id>make-assembly</id>
 | 
				
			||||||
 | 
					                        <phase>package</phase>
 | 
				
			||||||
 | 
					                        <goals>
 | 
				
			||||||
 | 
					                            <goal>single</goal>
 | 
				
			||||||
 | 
					                        </goals>
 | 
				
			||||||
 | 
					                    </execution>
 | 
				
			||||||
 | 
					                </executions>
 | 
				
			||||||
 | 
					            </plugin>
 | 
				
			||||||
 | 
					            <plugin>
 | 
				
			||||||
 | 
					                <groupId>org.apache.maven.plugins</groupId>
 | 
				
			||||||
 | 
					                <artifactId>maven-dependency-plugin</artifactId>
 | 
				
			||||||
 | 
					                <version>2.8</version>
 | 
				
			||||||
 | 
					                <executions>
 | 
				
			||||||
 | 
					                    <execution>
 | 
				
			||||||
 | 
					                        <phase>package</phase>
 | 
				
			||||||
 | 
					                        <goals>
 | 
				
			||||||
 | 
					                            <goal>copy-dependencies</goal>
 | 
				
			||||||
 | 
					                        </goals>
 | 
				
			||||||
 | 
					                        <configuration>
 | 
				
			||||||
 | 
					                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
 | 
				
			||||||
 | 
					                        </configuration>
 | 
				
			||||||
 | 
					                    </execution>
 | 
				
			||||||
 | 
					                </executions>
 | 
				
			||||||
 | 
					            </plugin>
 | 
				
			||||||
        </plugins>
 | 
					        </plugins>
 | 
				
			||||||
    </build>
 | 
					    </build>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,10 @@ module aos2_launcher {
 | 
				
			||||||
	requires javafx.graphics;
 | 
						requires javafx.graphics;
 | 
				
			||||||
	requires javafx.fxml;
 | 
						requires javafx.fxml;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						requires java.net.http;
 | 
				
			||||||
 | 
						requires com.google.gson;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	exports nl.andrewl.aos2_launcher to javafx.graphics;
 | 
						exports nl.andrewl.aos2_launcher to javafx.graphics;
 | 
				
			||||||
	opens nl.andrewl.aos2_launcher to javafx.fxml;
 | 
						opens nl.andrewl.aos2_launcher to javafx.fxml;
 | 
				
			||||||
 | 
						opens nl.andrewl.aos2_launcher.view to javafx.fxml;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_launcher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class EditProfileController {
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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();
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -7,22 +7,40 @@ import javafx.scene.Scene;
 | 
				
			||||||
import javafx.stage.Stage;
 | 
					import javafx.stage.Stage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.io.IOException;
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.nio.file.Files;
 | 
				
			||||||
 | 
					import java.nio.file.Path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The main starting point for the launcher app.
 | 
					 * The main starting point for the launcher app.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
public class Launcher extends Application {
 | 
					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
 | 
						@Override
 | 
				
			||||||
	public void start(Stage stage) throws IOException {
 | 
						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(PROFILES_DIR)) Files.createDirectory(PROFILES_DIR);
 | 
				
			||||||
		FXMLLoader loader = new FXMLLoader(Launcher.class.getResource("/main_view.fxml"));
 | 
							FXMLLoader loader = new FXMLLoader(Launcher.class.getResource("/main_view.fxml"));
 | 
				
			||||||
		Parent rootNode = loader.load();
 | 
							Parent rootNode = loader.load();
 | 
				
			||||||
		Scene scene = new Scene(rootNode);
 | 
							Scene scene = new Scene(rootNode);
 | 
				
			||||||
		scene.getStylesheets().add(Launcher.class.getResource("/styles.css").toExternalForm());
 | 
							addStylesheet(scene, "/font/fonts.css");
 | 
				
			||||||
 | 
							addStylesheet(scene, "/styles.css");
 | 
				
			||||||
		stage.setScene(scene);
 | 
							stage.setScene(scene);
 | 
				
			||||||
		stage.setTitle("Ace of Shades 2 - Launcher");
 | 
							stage.setTitle("Ace of Shades - Launcher");
 | 
				
			||||||
		stage.show();
 | 
							stage.show();
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private void addStylesheet(Scene scene, String resource) throws IOException {
 | 
				
			||||||
 | 
							var url = Launcher.class.getResource(resource);
 | 
				
			||||||
 | 
							if (url == null) throw new IOException("Could not load resource at " + resource);
 | 
				
			||||||
 | 
							scene.getStylesheets().add(url.toExternalForm());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static void main(String[] args) {
 | 
						public static void main(String[] args) {
 | 
				
			||||||
		launch(args);
 | 
							launch(args);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,140 @@
 | 
				
			||||||
package nl.andrewl.aos2_launcher;
 | 
					package nl.andrewl.aos2_launcher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javafx.application.Platform;
 | 
				
			||||||
 | 
					import javafx.beans.binding.BooleanBinding;
 | 
				
			||||||
 | 
					import javafx.collections.ListChangeListener;
 | 
				
			||||||
import javafx.fxml.FXML;
 | 
					import javafx.fxml.FXML;
 | 
				
			||||||
import javafx.scene.layout.TilePane;
 | 
					import javafx.scene.control.*;
 | 
				
			||||||
 | 
					import javafx.scene.layout.VBox;
 | 
				
			||||||
 | 
					import javafx.stage.Window;
 | 
				
			||||||
 | 
					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.EditProfileDialog;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_launcher.view.ElementList;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_launcher.view.ProfileView;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_launcher.view.ServerView;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class MainViewController implements ProgressReporter {
 | 
				
			||||||
 | 
						@FXML public Button playButton;
 | 
				
			||||||
 | 
						@FXML public Button editProfileButton;
 | 
				
			||||||
 | 
						@FXML public Button removeProfileButton;
 | 
				
			||||||
 | 
						@FXML public VBox profilesVBox;
 | 
				
			||||||
 | 
						private ElementList<Profile, ProfileView> profilesList;
 | 
				
			||||||
 | 
						@FXML public VBox serversVBox;
 | 
				
			||||||
 | 
						private ElementList<Server, ServerView> serversList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@FXML public VBox progressVBox;
 | 
				
			||||||
 | 
						@FXML public Label progressLabel;
 | 
				
			||||||
 | 
						@FXML public ProgressBar progressBar;
 | 
				
			||||||
 | 
						@FXML public TextField registryUrlField;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private final ProfileSet profileSet = new ProfileSet();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private ServersFetcher serversFetcher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class MainViewController {
 | 
					 | 
				
			||||||
	@FXML
 | 
						@FXML
 | 
				
			||||||
	public TilePane profilesTilePane;
 | 
						public void initialize() {
 | 
				
			||||||
 | 
							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<? super Profile>) c -> {
 | 
				
			||||||
 | 
								var selected = profileSet.getSelectedProfile();
 | 
				
			||||||
 | 
								profilesList.clear();
 | 
				
			||||||
 | 
								profilesList.addAll(profileSet.getProfiles());
 | 
				
			||||||
 | 
								profilesList.selectElement(selected);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							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());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							progressVBox.managedProperty().bind(progressVBox.visibleProperty());
 | 
				
			||||||
 | 
							progressVBox.setVisible(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							serversFetcher = new ServersFetcher(registryUrlField.textProperty());
 | 
				
			||||||
 | 
							Platform.runLater(this::refreshServers);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@FXML
 | 
				
			||||||
 | 
						public void refreshServers() {
 | 
				
			||||||
 | 
							Window owner = this.profilesVBox.getScene().getWindow();
 | 
				
			||||||
 | 
							serversFetcher.fetchServers(owner)
 | 
				
			||||||
 | 
									.exceptionally(throwable -> {
 | 
				
			||||||
 | 
										throwable.printStackTrace();
 | 
				
			||||||
 | 
										Platform.runLater(() -> {
 | 
				
			||||||
 | 
											Alert alert = new Alert(Alert.AlertType.ERROR);
 | 
				
			||||||
 | 
											alert.setHeaderText("Couldn't fetch servers.");
 | 
				
			||||||
 | 
											alert.setContentText("An error occurred, and the list of servers couldn't be fetched: " + throwable.getMessage() + ". Are you sure that you have the correct registry URL? Check the \"Servers\" tab.");
 | 
				
			||||||
 | 
											alert.initOwner(owner);
 | 
				
			||||||
 | 
											alert.show();
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
										return new ArrayList<>();
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									.thenAccept(newServers -> Platform.runLater(() -> {
 | 
				
			||||||
 | 
										serversList.clear();
 | 
				
			||||||
 | 
										serversList.addAll(newServers);
 | 
				
			||||||
 | 
									}));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@FXML
 | 
				
			||||||
 | 
						public void addProfile() {
 | 
				
			||||||
 | 
							EditProfileDialog dialog = new EditProfileDialog(profilesVBox.getScene().getWindow());
 | 
				
			||||||
 | 
							dialog.showAndWait().ifPresent(profileSet::addNewProfile);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@FXML
 | 
				
			||||||
 | 
						public void editProfile() {
 | 
				
			||||||
 | 
							EditProfileDialog dialog = new EditProfileDialog(profilesVBox.getScene().getWindow(), profileSet.getSelectedProfile());
 | 
				
			||||||
 | 
							dialog.showAndWait();
 | 
				
			||||||
 | 
							profileSet.save();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@FXML
 | 
				
			||||||
 | 
						public void removeProfile() {
 | 
				
			||||||
 | 
							profileSet.removeSelectedProfile();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@FXML
 | 
				
			||||||
 | 
						public void play() {
 | 
				
			||||||
 | 
							new GameRunner().run(
 | 
				
			||||||
 | 
									profileSet.getSelectedProfile(),
 | 
				
			||||||
 | 
									serversList.getSelectedElement(),
 | 
				
			||||||
 | 
									this,
 | 
				
			||||||
 | 
									this.profilesVBox.getScene().getWindow()
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void enableProgress() {
 | 
				
			||||||
 | 
							Platform.runLater(() -> {
 | 
				
			||||||
 | 
								progressVBox.setVisible(true);
 | 
				
			||||||
 | 
								progressBar.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
 | 
				
			||||||
 | 
								progressLabel.setText(null);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void disableProgress() {
 | 
				
			||||||
 | 
							Platform.runLater(() -> progressVBox.setVisible(false));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void setActionText(String text) {
 | 
				
			||||||
 | 
							Platform.runLater(() -> progressLabel.setText(text));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void setProgress(double progress) {
 | 
				
			||||||
 | 
							Platform.runLater(() -> progressBar.setProgress(progress));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,74 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_launcher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.google.gson.Gson;
 | 
				
			||||||
 | 
					import com.google.gson.JsonArray;
 | 
				
			||||||
 | 
					import com.google.gson.JsonElement;
 | 
				
			||||||
 | 
					import com.google.gson.JsonObject;
 | 
				
			||||||
 | 
					import javafx.application.Platform;
 | 
				
			||||||
 | 
					import javafx.beans.property.SimpleStringProperty;
 | 
				
			||||||
 | 
					import javafx.beans.property.StringProperty;
 | 
				
			||||||
 | 
					import javafx.scene.control.Alert;
 | 
				
			||||||
 | 
					import javafx.stage.Window;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_launcher.model.Server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.net.URI;
 | 
				
			||||||
 | 
					import java.net.http.HttpClient;
 | 
				
			||||||
 | 
					import java.net.http.HttpRequest;
 | 
				
			||||||
 | 
					import java.net.http.HttpResponse;
 | 
				
			||||||
 | 
					import java.time.Duration;
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ServersFetcher {
 | 
				
			||||||
 | 
						private final HttpClient httpClient;
 | 
				
			||||||
 | 
						private final Gson gson;
 | 
				
			||||||
 | 
						private final StringProperty registryUrl;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ServersFetcher(StringProperty registryUrlProperty) {
 | 
				
			||||||
 | 
							httpClient = HttpClient.newBuilder().build();
 | 
				
			||||||
 | 
							gson = new Gson();
 | 
				
			||||||
 | 
							this.registryUrl = new SimpleStringProperty("http://localhost:8080");
 | 
				
			||||||
 | 
							registryUrl.bind(registryUrlProperty);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public CompletableFuture<List<Server>> fetchServers(Window owner) {
 | 
				
			||||||
 | 
							if (registryUrl.get() == null || registryUrl.get().isBlank()) {
 | 
				
			||||||
 | 
								Platform.runLater(() -> {
 | 
				
			||||||
 | 
									Alert alert = new Alert(Alert.AlertType.WARNING);
 | 
				
			||||||
 | 
									alert.setContentText("Invalid or missing registry URL. Can't fetch the list of servers.");
 | 
				
			||||||
 | 
									alert.initOwner(owner);
 | 
				
			||||||
 | 
									alert.show();
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								return CompletableFuture.completedFuture(new ArrayList<>());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							HttpRequest req = HttpRequest.newBuilder(URI.create(registryUrl.get() + "/servers"))
 | 
				
			||||||
 | 
									.GET()
 | 
				
			||||||
 | 
									.timeout(Duration.ofSeconds(3))
 | 
				
			||||||
 | 
									.header("Accept", "application/json")
 | 
				
			||||||
 | 
									.build();
 | 
				
			||||||
 | 
							return httpClient.sendAsync(req, HttpResponse.BodyHandlers.ofString())
 | 
				
			||||||
 | 
									.thenApplyAsync(resp -> {
 | 
				
			||||||
 | 
										if (resp.statusCode() == 200) {
 | 
				
			||||||
 | 
											JsonArray serversArray = gson.fromJson(resp.body(), JsonArray.class);
 | 
				
			||||||
 | 
											List<Server> servers = new ArrayList<>(serversArray.size());
 | 
				
			||||||
 | 
											for (JsonElement serverJson : serversArray) {
 | 
				
			||||||
 | 
												if (serverJson instanceof JsonObject obj) {
 | 
				
			||||||
 | 
													servers.add(new Server(
 | 
				
			||||||
 | 
															obj.get("host").getAsString(),
 | 
				
			||||||
 | 
															obj.get("port").getAsInt(),
 | 
				
			||||||
 | 
															obj.get("name").getAsString(),
 | 
				
			||||||
 | 
															obj.get("description").getAsString(),
 | 
				
			||||||
 | 
															obj.get("maxPlayers").getAsInt(),
 | 
				
			||||||
 | 
															obj.get("currentPlayers").getAsInt(),
 | 
				
			||||||
 | 
															obj.get("lastUpdatedAt").getAsLong()
 | 
				
			||||||
 | 
													));
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											return servers;
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											throw new RuntimeException("Invalid response: " + resp.statusCode());
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,134 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_launcher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_launcher.model.ProgressReporter;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_launcher.util.FileUtils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					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.nio.file.attribute.BasicFileAttributes;
 | 
				
			||||||
 | 
					import java.time.Duration;
 | 
				
			||||||
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
 | 
					import java.util.function.BiPredicate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static final String JRE_DOWNLOAD_URL = "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.4+8/";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static CompletableFuture<Path> getJreExecutablePath(ProgressReporter progressReporter) {
 | 
				
			||||||
 | 
							Optional<Path> optionalExecutablePath = findJreExecutable();
 | 
				
			||||||
 | 
							return optionalExecutablePath.map(CompletableFuture::completedFuture)
 | 
				
			||||||
 | 
									.orElseGet(() -> downloadAppropriateJre(progressReporter));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static CompletableFuture<Path> downloadAppropriateJre(ProgressReporter progressReporter) {
 | 
				
			||||||
 | 
							progressReporter.enableProgress();
 | 
				
			||||||
 | 
							progressReporter.setActionText("Downloading JRE...");
 | 
				
			||||||
 | 
							HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
 | 
				
			||||||
 | 
							HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().GET().timeout(Duration.ofMinutes(5));
 | 
				
			||||||
 | 
							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 -> {
 | 
				
			||||||
 | 
										if (resp.statusCode() == 200) {
 | 
				
			||||||
 | 
											// Download sequentially, and update the progress.
 | 
				
			||||||
 | 
											try {
 | 
				
			||||||
 | 
												if (Files.exists(Launcher.JRE_PATH)) {
 | 
				
			||||||
 | 
													FileUtils.deleteRecursive(Launcher.JRE_PATH);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												Files.createDirectory(Launcher.JRE_PATH);
 | 
				
			||||||
 | 
												Path jreArchiveFile = Launcher.JRE_PATH.resolve(jreArchiveName);
 | 
				
			||||||
 | 
												FileUtils.downloadWithProgress(jreArchiveFile, resp, progressReporter);
 | 
				
			||||||
 | 
												progressReporter.setProgress(-1); // Indefinite progress.
 | 
				
			||||||
 | 
												progressReporter.setActionText("Unpacking JRE...");
 | 
				
			||||||
 | 
												ProcessBuilder pb = new ProcessBuilder().inheritIO();
 | 
				
			||||||
 | 
												if (OS_LINUX || OS_MAC) {
 | 
				
			||||||
 | 
													pb.command("tar", "-xzf", jreArchiveFile.toAbsolutePath().toString(), "-C", Launcher.JRE_PATH.toAbsolutePath().toString());
 | 
				
			||||||
 | 
												} else if (OS_WINDOWS) {
 | 
				
			||||||
 | 
													pb.command("powershell", "-command", "\"Expand-Archive -Force '" + jreArchiveFile.toAbsolutePath() + "' '" + Launcher.JRE_PATH.toAbsolutePath() + "'\"");
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												Process process = pb.start();
 | 
				
			||||||
 | 
												int result = process.waitFor();
 | 
				
			||||||
 | 
												if (result != 0) throw new IOException("Archive extraction process exited with non-zero code: " + result);
 | 
				
			||||||
 | 
												Files.delete(jreArchiveFile);
 | 
				
			||||||
 | 
												progressReporter.setActionText("Looking for java executable...");
 | 
				
			||||||
 | 
												Optional<Path> optionalExecutablePath = findJreExecutable();
 | 
				
			||||||
 | 
												if (optionalExecutablePath.isEmpty()) throw new IOException("Couldn't find java executable.");
 | 
				
			||||||
 | 
												progressReporter.disableProgress();
 | 
				
			||||||
 | 
												return optionalExecutablePath.get();
 | 
				
			||||||
 | 
											} catch (IOException | InterruptedException e) {
 | 
				
			||||||
 | 
												throw new RuntimeException(e);
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											throw new RuntimeException("JRE download failed: " + resp.statusCode());
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static Optional<Path> findJreExecutable() {
 | 
				
			||||||
 | 
							if (!Files.exists(Launcher.JRE_PATH)) return Optional.empty();
 | 
				
			||||||
 | 
							BiPredicate<Path, BasicFileAttributes> pred = (path, basicFileAttributes) -> {
 | 
				
			||||||
 | 
								String filename = path.getFileName().toString();
 | 
				
			||||||
 | 
								return Files.isExecutable(path) && (filename.equals("java") || filename.equals("java.exe"));
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
							try (var s = Files.find(Launcher.JRE_PATH, 3, pred)) {
 | 
				
			||||||
 | 
								return s.findFirst();
 | 
				
			||||||
 | 
							} catch (IOException e) {
 | 
				
			||||||
 | 
								e.printStackTrace();
 | 
				
			||||||
 | 
								return Optional.empty();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static String getPreferredJreName() {
 | 
				
			||||||
 | 
							if (OS_LINUX) {
 | 
				
			||||||
 | 
								if (ARCH_AARCH64) return "OpenJDK17U-jre_aarch64_linux_hotspot_17.0.4_8.tar.gz";
 | 
				
			||||||
 | 
								if (ARCH_AMD64) return "OpenJDK17U-jre_x64_linux_hotspot_17.0.4_8.tar.gz";
 | 
				
			||||||
 | 
								if (ARCH_ARM || ARCH_ARM32) return "OpenJDK17U-jre_arm_linux_hotspot_17.0.4_8.tar.gz";
 | 
				
			||||||
 | 
							} else if (OS_MAC) {
 | 
				
			||||||
 | 
								if (ARCH_AARCH64) return "OpenJDK17U-jre_aarch64_mac_hotspot_17.0.4_8.tar.gz";
 | 
				
			||||||
 | 
								if (ARCH_X86_64) return "OpenJDK17U-jre_x64_mac_hotspot_17.0.4_8.tar.gz";
 | 
				
			||||||
 | 
							} else if (OS_WINDOWS) {
 | 
				
			||||||
 | 
								if (ARCH_AARCH64 || ARCH_AMD64) return "OpenJDK17U-jre_x64_windows_hotspot_17.0.4_8.zip";
 | 
				
			||||||
 | 
								if (ARCH_X86) return "OpenJDK17U-jre_x86-32_windows_hotspot_17.0.4_8.zip";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							System.err.println("Couldn't determine the preferred JRE version. Defaulting to x64_windows.");
 | 
				
			||||||
 | 
							return "OpenJDK17U-jre_x64_windows_hotspot_17.0.4_8.zip";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,177 @@
 | 
				
			||||||
 | 
					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 nl.andrewl.aos2_launcher.model.ProgressReporter;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_launcher.util.FileUtils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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<ClientVersionRelease> availableReleases;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private final HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build();
 | 
				
			||||||
 | 
						private boolean loaded = false;
 | 
				
			||||||
 | 
						private CompletableFuture<List<ClientVersionRelease>> activeReleaseFetchFuture;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public VersionFetcher() {
 | 
				
			||||||
 | 
							this.availableReleases = new ArrayList<>();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public CompletableFuture<ClientVersionRelease> getRelease(String versionTag) {
 | 
				
			||||||
 | 
							return getAvailableReleases().thenApply(releases -> releases.stream()
 | 
				
			||||||
 | 
									.filter(r -> r.tag().equals(versionTag))
 | 
				
			||||||
 | 
									.findFirst().orElse(null));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public CompletableFuture<List<ClientVersionRelease>> getAvailableReleases() {
 | 
				
			||||||
 | 
							if (loaded) {
 | 
				
			||||||
 | 
								return CompletableFuture.completedFuture(Collections.unmodifiableList(availableReleases));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return fetchReleasesFromGitHub();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private CompletableFuture<List<ClientVersionRelease>> 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<String> 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<Path> ensureVersionIsDownloaded(String versionTag, ProgressReporter progressReporter) {
 | 
				
			||||||
 | 
							try (var s = Files.list(Launcher.VERSIONS_DIR)) {
 | 
				
			||||||
 | 
								Optional<Path> 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);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							progressReporter.enableProgress();
 | 
				
			||||||
 | 
							progressReporter.setActionText("Downloading client " + versionTag + "...");
 | 
				
			||||||
 | 
							var future = getRelease(versionTag)
 | 
				
			||||||
 | 
									.thenComposeAsync(release -> downloadVersion(release, progressReporter));
 | 
				
			||||||
 | 
							future.thenRun(progressReporter::disableProgress);
 | 
				
			||||||
 | 
							return future;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private CompletableFuture<Path> downloadVersion(ClientVersionRelease release, ProgressReporter progressReporter) {
 | 
				
			||||||
 | 
							System.out.println("Downloading version " + release.tag());
 | 
				
			||||||
 | 
							HttpRequest req = HttpRequest.newBuilder(URI.create(release.assetsUrl()))
 | 
				
			||||||
 | 
									.GET().timeout(Duration.ofSeconds(3)).build();
 | 
				
			||||||
 | 
							CompletableFuture<JsonObject> 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)) {
 | 
				
			||||||
 | 
													return assetObj;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											throw new RuntimeException("Couldn't find a matching release asset for this system.");
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											throw new RuntimeException("Error while requesting release assets from GitHub: " + resp.statusCode());
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
							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);
 | 
				
			||||||
 | 
								return httpClient.sendAsync(downloadRequest, HttpResponse.BodyHandlers.ofInputStream())
 | 
				
			||||||
 | 
										.thenApplyAsync(resp -> {
 | 
				
			||||||
 | 
											if (resp.statusCode() == 200) {
 | 
				
			||||||
 | 
												// Download sequentially, and update the progress.
 | 
				
			||||||
 | 
												try {
 | 
				
			||||||
 | 
													FileUtils.downloadWithProgress(file, resp, progressReporter);
 | 
				
			||||||
 | 
												} catch (IOException e) {
 | 
				
			||||||
 | 
													throw new RuntimeException(e);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
												return file;
 | 
				
			||||||
 | 
											} else {
 | 
				
			||||||
 | 
												throw new RuntimeException("Error while downloading release asset from GitHub: " + resp.statusCode());
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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.");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,84 @@
 | 
				
			||||||
 | 
					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 username;
 | 
				
			||||||
 | 
						private final StringProperty clientVersion;
 | 
				
			||||||
 | 
						private final StringProperty jvmArgs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Profile() {
 | 
				
			||||||
 | 
							this(UUID.randomUUID(), "", "Player", null, null);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Profile(UUID id, String name, String username, String clientVersion, String jvmArgs) {
 | 
				
			||||||
 | 
							this.id = id;
 | 
				
			||||||
 | 
							this.name = new SimpleStringProperty(name);
 | 
				
			||||||
 | 
							this.username = new SimpleStringProperty(username);
 | 
				
			||||||
 | 
							this.clientVersion = new SimpleStringProperty(clientVersion);
 | 
				
			||||||
 | 
							this.jvmArgs = new SimpleStringProperty(jvmArgs);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public UUID getId() {
 | 
				
			||||||
 | 
							return id;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String getName() {
 | 
				
			||||||
 | 
							return name.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public StringProperty nameProperty() {
 | 
				
			||||||
 | 
							return name;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String getUsername() {
 | 
				
			||||||
 | 
							return username.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public StringProperty usernameProperty() {
 | 
				
			||||||
 | 
							return username;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String getClientVersion() {
 | 
				
			||||||
 | 
							return clientVersion.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public StringProperty clientVersionProperty() {
 | 
				
			||||||
 | 
							return clientVersion;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String getJvmArgs() {
 | 
				
			||||||
 | 
							return jvmArgs.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public StringProperty jvmArgsProperty() {
 | 
				
			||||||
 | 
							return jvmArgs;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void setName(String name) {
 | 
				
			||||||
 | 
							this.name.set(name);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,155 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_launcher.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.google.gson.*;
 | 
				
			||||||
 | 
					import javafx.beans.property.ObjectProperty;
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					import java.nio.file.Path;
 | 
				
			||||||
 | 
					import java.util.UUID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Model for managing the set of profiles in the app.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class ProfileSet {
 | 
				
			||||||
 | 
						private final ObservableList<Profile> profiles;
 | 
				
			||||||
 | 
						private final ObjectProperty<Profile> selectedProfile;
 | 
				
			||||||
 | 
						private Path lastFileUsed = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ProfileSet() {
 | 
				
			||||||
 | 
							this.profiles = FXCollections.observableArrayList();
 | 
				
			||||||
 | 
							this.selectedProfile = new SimpleObjectProperty<>(null);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ProfileSet(Path file) throws IOException {
 | 
				
			||||||
 | 
							this();
 | 
				
			||||||
 | 
							load(file);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void addNewProfile(Profile profile) {
 | 
				
			||||||
 | 
							profiles.add(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) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									if (Files.exists(profile.getDir())) {
 | 
				
			||||||
 | 
										FileUtils.deleteRecursive(profile.getDir());
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} catch (IOException e) {
 | 
				
			||||||
 | 
									throw new RuntimeException(e);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								save();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void removeSelectedProfile() {
 | 
				
			||||||
 | 
							removeProfile(getSelectedProfile());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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 == 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 clientVersion = profileObj.get("clientVersion").getAsString();
 | 
				
			||||||
 | 
									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);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								lastFileUsed = file;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void loadOrCreateStandardFile() {
 | 
				
			||||||
 | 
							if (!Files.exists(Launcher.PROFILES_FILE)) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									save(Launcher.PROFILES_FILE);
 | 
				
			||||||
 | 
								} catch (IOException e) {
 | 
				
			||||||
 | 
									throw new RuntimeException(e);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									load(Launcher.PROFILES_FILE);
 | 
				
			||||||
 | 
								} catch (IOException e) {
 | 
				
			||||||
 | 
									throw new RuntimeException(e);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void save(Path file) throws IOException {
 | 
				
			||||||
 | 
							Gson gson = new GsonBuilder().setPrettyPrinting().create();
 | 
				
			||||||
 | 
							JsonObject data = new JsonObject();
 | 
				
			||||||
 | 
							String selectedProfileId = selectedProfile.getValue() == null ? null : selectedProfile.getValue().getId().toString();
 | 
				
			||||||
 | 
							data.addProperty("selectedProfileId", selectedProfileId);
 | 
				
			||||||
 | 
							JsonArray profilesArray = new JsonArray(profiles.size());
 | 
				
			||||||
 | 
							for (Profile profile : profiles) {
 | 
				
			||||||
 | 
								JsonObject obj = new JsonObject();
 | 
				
			||||||
 | 
								obj.addProperty("id", profile.getId().toString());
 | 
				
			||||||
 | 
								obj.addProperty("name", profile.getName());
 | 
				
			||||||
 | 
								obj.addProperty("username", profile.getUsername());
 | 
				
			||||||
 | 
								obj.addProperty("clientVersion", profile.getClientVersion());
 | 
				
			||||||
 | 
								obj.addProperty("jvmArgs", profile.getJvmArgs());
 | 
				
			||||||
 | 
								profilesArray.add(obj);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							data.add("profiles", profilesArray);
 | 
				
			||||||
 | 
							try (var writer = Files.newBufferedWriter(file)) {
 | 
				
			||||||
 | 
								gson.toJson(data, writer);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							lastFileUsed = file;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void save() {
 | 
				
			||||||
 | 
							if (lastFileUsed != null) {
 | 
				
			||||||
 | 
								try {
 | 
				
			||||||
 | 
									save(lastFileUsed);
 | 
				
			||||||
 | 
								} catch (IOException e) {
 | 
				
			||||||
 | 
									throw new RuntimeException(e);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ObservableList<Profile> getProfiles() {
 | 
				
			||||||
 | 
							return profiles;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Profile getSelectedProfile() {
 | 
				
			||||||
 | 
							return selectedProfile.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ObjectProperty<Profile> selectedProfileProperty() {
 | 
				
			||||||
 | 
							return selectedProfile;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_launcher.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public interface ProgressReporter {
 | 
				
			||||||
 | 
						void enableProgress();
 | 
				
			||||||
 | 
						void disableProgress();
 | 
				
			||||||
 | 
						void setActionText(String text);
 | 
				
			||||||
 | 
						void setProgress(double progress);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,84 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_launcher.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javafx.beans.property.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.time.Instant;
 | 
				
			||||||
 | 
					import java.time.LocalDateTime;
 | 
				
			||||||
 | 
					import java.time.ZoneId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class Server {
 | 
				
			||||||
 | 
						private final StringProperty host;
 | 
				
			||||||
 | 
						private final IntegerProperty port;
 | 
				
			||||||
 | 
						private final StringProperty name;
 | 
				
			||||||
 | 
						private final StringProperty description;
 | 
				
			||||||
 | 
						private final IntegerProperty maxPlayers;
 | 
				
			||||||
 | 
						private final IntegerProperty currentPlayers;
 | 
				
			||||||
 | 
						private final ObjectProperty<LocalDateTime> lastUpdatedAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Server(String host, int port, String name, String description, int maxPlayers, int currentPlayers, long lastUpdatedAt) {
 | 
				
			||||||
 | 
							this.host = new SimpleStringProperty(host);
 | 
				
			||||||
 | 
							this.port = new SimpleIntegerProperty(port);
 | 
				
			||||||
 | 
							this.name = new SimpleStringProperty(name);
 | 
				
			||||||
 | 
							this.description = new SimpleStringProperty(description);
 | 
				
			||||||
 | 
							this.maxPlayers = new SimpleIntegerProperty(maxPlayers);
 | 
				
			||||||
 | 
							this.currentPlayers = new SimpleIntegerProperty(currentPlayers);
 | 
				
			||||||
 | 
							LocalDateTime ts = Instant.ofEpochMilli(lastUpdatedAt).atZone(ZoneId.systemDefault()).toLocalDateTime();
 | 
				
			||||||
 | 
							this.lastUpdatedAt = new SimpleObjectProperty<>(ts);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String getHost() {
 | 
				
			||||||
 | 
							return host.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public StringProperty hostProperty() {
 | 
				
			||||||
 | 
							return host;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public int getPort() {
 | 
				
			||||||
 | 
							return port.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public IntegerProperty portProperty() {
 | 
				
			||||||
 | 
							return port;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String getName() {
 | 
				
			||||||
 | 
							return name.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public StringProperty nameProperty() {
 | 
				
			||||||
 | 
							return name;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String getDescription() {
 | 
				
			||||||
 | 
							return description.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public StringProperty descriptionProperty() {
 | 
				
			||||||
 | 
							return description;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public int getMaxPlayers() {
 | 
				
			||||||
 | 
							return maxPlayers.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public IntegerProperty maxPlayersProperty() {
 | 
				
			||||||
 | 
							return maxPlayers;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public int getCurrentPlayers() {
 | 
				
			||||||
 | 
							return currentPlayers.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public IntegerProperty currentPlayersProperty() {
 | 
				
			||||||
 | 
							return currentPlayers;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public LocalDateTime getLastUpdatedAt() {
 | 
				
			||||||
 | 
							return lastUpdatedAt.get();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Property<LocalDateTime> lastUpdatedAtProperty() {
 | 
				
			||||||
 | 
							return lastUpdatedAt;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,74 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_launcher.util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_launcher.model.ProgressReporter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.io.InputStream;
 | 
				
			||||||
 | 
					import java.net.http.HttpResponse;
 | 
				
			||||||
 | 
					import java.nio.file.FileVisitResult;
 | 
				
			||||||
 | 
					import java.nio.file.Files;
 | 
				
			||||||
 | 
					import java.nio.file.Path;
 | 
				
			||||||
 | 
					import java.nio.file.SimpleFileVisitor;
 | 
				
			||||||
 | 
					import java.nio.file.attribute.BasicFileAttributes;
 | 
				
			||||||
 | 
					import java.text.CharacterIterator;
 | 
				
			||||||
 | 
					import java.text.StringCharacterIterator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class FileUtils {
 | 
				
			||||||
 | 
						public static String humanReadableByteCountSI(long bytes) {
 | 
				
			||||||
 | 
							if (-1000 < bytes && bytes < 1000) {
 | 
				
			||||||
 | 
								return bytes + " B";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							CharacterIterator ci = new StringCharacterIterator("kMGTPE");
 | 
				
			||||||
 | 
							while (bytes <= -999_950 || bytes >= 999_950) {
 | 
				
			||||||
 | 
								bytes /= 1000;
 | 
				
			||||||
 | 
								ci.next();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return String.format("%.1f %cB", bytes / 1000.0, ci.current());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static String humanReadableByteCountBin(long bytes) {
 | 
				
			||||||
 | 
							long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes);
 | 
				
			||||||
 | 
							if (absB < 1024) {
 | 
				
			||||||
 | 
								return bytes + " B";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							long value = absB;
 | 
				
			||||||
 | 
							CharacterIterator ci = new StringCharacterIterator("KMGTPE");
 | 
				
			||||||
 | 
							for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) {
 | 
				
			||||||
 | 
								value >>= 10;
 | 
				
			||||||
 | 
								ci.next();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							value *= Long.signum(bytes);
 | 
				
			||||||
 | 
							return String.format("%.1f %ciB", value / 1024.0, ci.current());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static void deleteRecursive(Path p) throws IOException {
 | 
				
			||||||
 | 
							Files.walkFileTree(p, new SimpleFileVisitor<>() {
 | 
				
			||||||
 | 
								@Override
 | 
				
			||||||
 | 
								public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
 | 
				
			||||||
 | 
									Files.delete(file);
 | 
				
			||||||
 | 
									return FileVisitResult.CONTINUE;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								@Override
 | 
				
			||||||
 | 
								public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
 | 
				
			||||||
 | 
									Files.delete(dir);
 | 
				
			||||||
 | 
									return FileVisitResult.CONTINUE;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static void downloadWithProgress(Path outputFile, HttpResponse<InputStream> resp, ProgressReporter reporter) throws IOException {
 | 
				
			||||||
 | 
							reporter.setProgress(0);
 | 
				
			||||||
 | 
							long size = resp.headers().firstValueAsLong("Content-Length").orElse(1);
 | 
				
			||||||
 | 
							try (var out = Files.newOutputStream(outputFile); var in = resp.body()) {
 | 
				
			||||||
 | 
								byte[] buffer = new byte[8192];
 | 
				
			||||||
 | 
								long bytesRead = 0;
 | 
				
			||||||
 | 
								while (bytesRead < size) {
 | 
				
			||||||
 | 
									int readCount = in.read(buffer);
 | 
				
			||||||
 | 
									out.write(buffer, 0, readCount);
 | 
				
			||||||
 | 
									bytesRead += readCount;
 | 
				
			||||||
 | 
									reporter.setProgress((double) bytesRead / size);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,90 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_launcher.view;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javafx.beans.WeakListener;
 | 
				
			||||||
 | 
					import javafx.collections.ListChangeListener;
 | 
				
			||||||
 | 
					import javafx.collections.ObservableList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.lang.ref.WeakReference;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.function.Function;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import static java.util.stream.Collectors.toList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class BindingUtil {
 | 
				
			||||||
 | 
						public static <E, F> void mapContent(ObservableList<F> mapped, ObservableList<? extends E> source,
 | 
				
			||||||
 | 
															 Function<? super E, ? extends F> mapper) {
 | 
				
			||||||
 | 
							map(mapped, source, mapper);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static <E, F> Object map(ObservableList<F> mapped, ObservableList<? extends E> source,
 | 
				
			||||||
 | 
														 Function<? super E, ? extends F> mapper) {
 | 
				
			||||||
 | 
							final ListContentMapping<E, F> contentMapping = new ListContentMapping<>(mapped, mapper);
 | 
				
			||||||
 | 
							mapped.setAll(source.stream().map(mapper).collect(toList()));
 | 
				
			||||||
 | 
							source.removeListener(contentMapping);
 | 
				
			||||||
 | 
							source.addListener(contentMapping);
 | 
				
			||||||
 | 
							return contentMapping;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private static class ListContentMapping<E, F> implements ListChangeListener<E>, WeakListener {
 | 
				
			||||||
 | 
							private final WeakReference<List<F>> mappedRef;
 | 
				
			||||||
 | 
							private final Function<? super E, ? extends F> mapper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							public ListContentMapping(List<F> mapped, Function<? super E, ? extends F> mapper) {
 | 
				
			||||||
 | 
								this.mappedRef = new WeakReference<>(mapped);
 | 
				
			||||||
 | 
								this.mapper = mapper;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@Override
 | 
				
			||||||
 | 
							public void onChanged(Change<? extends E> change) {
 | 
				
			||||||
 | 
								final List<F> mapped = mappedRef.get();
 | 
				
			||||||
 | 
								if (mapped == null) {
 | 
				
			||||||
 | 
									change.getList().removeListener(this);
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									while (change.next()) {
 | 
				
			||||||
 | 
										if (change.wasPermutated()) {
 | 
				
			||||||
 | 
											mapped.subList(change.getFrom(), change.getTo()).clear();
 | 
				
			||||||
 | 
											mapped.addAll(change.getFrom(), change.getList().subList(change.getFrom(), change.getTo())
 | 
				
			||||||
 | 
													.stream().map(mapper).toList());
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											if (change.wasRemoved()) {
 | 
				
			||||||
 | 
												mapped.subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											if (change.wasAdded()) {
 | 
				
			||||||
 | 
												mapped.addAll(change.getFrom(), change.getAddedSubList()
 | 
				
			||||||
 | 
														.stream().map(mapper).toList());
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@Override
 | 
				
			||||||
 | 
							public boolean wasGarbageCollected() {
 | 
				
			||||||
 | 
								return mappedRef.get() == null;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@Override
 | 
				
			||||||
 | 
							public int hashCode() {
 | 
				
			||||||
 | 
								final List<F> list = mappedRef.get();
 | 
				
			||||||
 | 
								return (list == null) ? 0 : list.hashCode();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							@Override
 | 
				
			||||||
 | 
							public boolean equals(Object obj) {
 | 
				
			||||||
 | 
								if (this == obj) {
 | 
				
			||||||
 | 
									return true;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								final List<F> mapped1 = mappedRef.get();
 | 
				
			||||||
 | 
								if (mapped1 == null) {
 | 
				
			||||||
 | 
									return false;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (obj instanceof final ListContentMapping<?, ?> other) {
 | 
				
			||||||
 | 
									final List<?> mapped2 = other.mappedRef.get();
 | 
				
			||||||
 | 
									return mapped1 == mapped2;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return false;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,96 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_launcher.view;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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.fxml.FXML;
 | 
				
			||||||
 | 
					import javafx.fxml.FXMLLoader;
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					import java.util.Objects;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class EditProfileDialog extends Dialog<Profile> {
 | 
				
			||||||
 | 
						@FXML public TextField nameField;
 | 
				
			||||||
 | 
						@FXML public TextField usernameField;
 | 
				
			||||||
 | 
						@FXML public ChoiceBox<String> clientVersionChoiceBox;
 | 
				
			||||||
 | 
						@FXML public TextArea jvmArgsTextArea;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private final ObjectProperty<Profile> profile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public EditProfileDialog(Window owner, Profile profile) {
 | 
				
			||||||
 | 
							this.profile = new SimpleObjectProperty<>(profile);
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								FXMLLoader loader = new FXMLLoader(EditProfileDialog.class.getResource("/dialog/edit_profile.fxml"));
 | 
				
			||||||
 | 
								loader.setController(this);
 | 
				
			||||||
 | 
								Parent parent = loader.load();
 | 
				
			||||||
 | 
								initOwner(owner);
 | 
				
			||||||
 | 
								initModality(Modality.APPLICATION_MODAL);
 | 
				
			||||||
 | 
								setResizable(true);
 | 
				
			||||||
 | 
								setTitle("Edit Profile");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								BooleanBinding formInvalid = nameField.textProperty().isEmpty()
 | 
				
			||||||
 | 
										.or(clientVersionChoiceBox.valueProperty().isNull())
 | 
				
			||||||
 | 
										.or(usernameField.textProperty().isEmpty());
 | 
				
			||||||
 | 
								nameField.setText(profile.getName());
 | 
				
			||||||
 | 
								usernameField.setText(profile.getUsername());
 | 
				
			||||||
 | 
								VersionFetcher.INSTANCE.getAvailableReleases()
 | 
				
			||||||
 | 
										.whenComplete((releases, throwable) -> Platform.runLater(() -> {
 | 
				
			||||||
 | 
											if (throwable == null) {
 | 
				
			||||||
 | 
												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());
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											} else {
 | 
				
			||||||
 | 
												throwable.printStackTrace();
 | 
				
			||||||
 | 
												Alert alert = new Alert(Alert.AlertType.ERROR);
 | 
				
			||||||
 | 
												alert.initOwner(this.getOwner());
 | 
				
			||||||
 | 
												alert.setContentText("An error occurred while fetching the latest game releases: " + throwable.getMessage());
 | 
				
			||||||
 | 
												alert.show();
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
								}));
 | 
				
			||||||
 | 
								jvmArgsTextArea.setText(profile.getJvmArgs());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								DialogPane pane = new DialogPane();
 | 
				
			||||||
 | 
								pane.setContent(parent);
 | 
				
			||||||
 | 
								ButtonType okButton = new ButtonType("Ok", ButtonBar.ButtonData.OK_DONE);
 | 
				
			||||||
 | 
								ButtonType cancelButton = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
 | 
				
			||||||
 | 
								pane.getButtonTypes().add(okButton);
 | 
				
			||||||
 | 
								pane.getButtonTypes().add(cancelButton);
 | 
				
			||||||
 | 
								pane.lookupButton(okButton).disableProperty().bind(formInvalid);
 | 
				
			||||||
 | 
								setDialogPane(pane);
 | 
				
			||||||
 | 
								setResultConverter(buttonType -> {
 | 
				
			||||||
 | 
									if (!Objects.equals(ButtonBar.ButtonData.OK_DONE, buttonType.getButtonData())) {
 | 
				
			||||||
 | 
										return null;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									var prof = this.profile.getValue();
 | 
				
			||||||
 | 
									prof.setName(nameField.getText().trim());
 | 
				
			||||||
 | 
									prof.setUsername(usernameField.getText().trim());
 | 
				
			||||||
 | 
									prof.setClientVersion(clientVersionChoiceBox.getValue());
 | 
				
			||||||
 | 
									prof.setJvmArgs(jvmArgsTextArea.getText());
 | 
				
			||||||
 | 
									return this.profile.getValue();
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								setOnShowing(event -> Platform.runLater(() -> nameField.requestFocus()));
 | 
				
			||||||
 | 
							} catch (IOException e) {
 | 
				
			||||||
 | 
								throw new RuntimeException(e);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public EditProfileDialog(Window owner) {
 | 
				
			||||||
 | 
							this(owner, new Profile());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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<T, V extends Node> {
 | 
				
			||||||
 | 
						private final Pane container;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private final ObjectProperty<T> selectedElement = new SimpleObjectProperty<>(null);
 | 
				
			||||||
 | 
						private final ObservableList<T> elements = FXCollections.observableArrayList();
 | 
				
			||||||
 | 
						private final Class<V> elementViewType;
 | 
				
			||||||
 | 
						private final Function<V, T> viewElementMapper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ElementList(
 | 
				
			||||||
 | 
								Pane container,
 | 
				
			||||||
 | 
								Function<T, V> elementViewMapper,
 | 
				
			||||||
 | 
								Class<V> elementViewType,
 | 
				
			||||||
 | 
								Function<V, T> 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<T> selectedElementProperty() {
 | 
				
			||||||
 | 
							return selectedElement;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ObservableList<T> getElements() {
 | 
				
			||||||
 | 
							return elements;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void clear() {
 | 
				
			||||||
 | 
							elements.clear();
 | 
				
			||||||
 | 
							selectElement(null);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void add(T element) {
 | 
				
			||||||
 | 
							elements.add(element);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void addAll(Collection<T> newElements) {
 | 
				
			||||||
 | 
							elements.addAll(newElements);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void remove(T element) {
 | 
				
			||||||
 | 
							elements.remove(element);
 | 
				
			||||||
 | 
							if (element != null && element.equals(selectedElement.get())) {
 | 
				
			||||||
 | 
								selectElement(null);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,38 @@
 | 
				
			||||||
 | 
					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.Pane;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_launcher.model.Profile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							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());
 | 
				
			||||||
 | 
							clientVersionLabel.textProperty().bind(profile.clientVersionProperty());
 | 
				
			||||||
 | 
							usernameLabel.textProperty().bind(profile.usernameProperty());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Profile getProfile() {
 | 
				
			||||||
 | 
							return this.profile;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_launcher.view;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import javafx.scene.control.Label;
 | 
				
			||||||
 | 
					import javafx.scene.layout.VBox;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_launcher.model.Server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ServerView extends VBox {
 | 
				
			||||||
 | 
						private final Server server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ServerView(Server server) {
 | 
				
			||||||
 | 
							this.server = server;
 | 
				
			||||||
 | 
							var hostLabel = new Label();
 | 
				
			||||||
 | 
							hostLabel.textProperty().bind(server.hostProperty());
 | 
				
			||||||
 | 
							var portLabel = new Label();
 | 
				
			||||||
 | 
							portLabel.setText(Integer.toString(server.getPort()));
 | 
				
			||||||
 | 
							server.portProperty().addListener((observableValue, x1, x2) -> {
 | 
				
			||||||
 | 
								portLabel.setText(x2.toString());
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
							var nameLabel = new Label();
 | 
				
			||||||
 | 
							nameLabel.textProperty().bind(server.nameProperty());
 | 
				
			||||||
 | 
							var descriptionLabel = new Label();
 | 
				
			||||||
 | 
							descriptionLabel.textProperty().bind(server.descriptionProperty());
 | 
				
			||||||
 | 
							var playersLabel = new Label();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var nodes = getChildren();
 | 
				
			||||||
 | 
							nodes.addAll(hostLabel, portLabel, nameLabel, descriptionLabel);
 | 
				
			||||||
 | 
							getStyleClass().add("list-item");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Server getServer() {
 | 
				
			||||||
 | 
							return server;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<?import javafx.geometry.*?>
 | 
				
			||||||
 | 
					<?import javafx.scene.control.*?>
 | 
				
			||||||
 | 
					<?import javafx.scene.layout.*?>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<AnchorPane minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
 | 
				
			||||||
 | 
					   <VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" spacing="10.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
 | 
				
			||||||
 | 
					      <padding>
 | 
				
			||||||
 | 
					         <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
 | 
				
			||||||
 | 
					      </padding>
 | 
				
			||||||
 | 
					      <AnchorPane VBox.vgrow="NEVER">
 | 
				
			||||||
 | 
					         <Label text="Profile Name" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0" />
 | 
				
			||||||
 | 
					         <TextField fx:id="nameField" promptText="Enter a name for the profile..." AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="150.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
 | 
				
			||||||
 | 
					      </AnchorPane>
 | 
				
			||||||
 | 
					      <AnchorPane VBox.vgrow="NEVER">
 | 
				
			||||||
 | 
					         <Label text="Username" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0" />
 | 
				
			||||||
 | 
					         <TextField fx:id="usernameField" promptText="Enter a username..." AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="150.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
 | 
				
			||||||
 | 
					      </AnchorPane>
 | 
				
			||||||
 | 
					      <AnchorPane VBox.vgrow="NEVER">
 | 
				
			||||||
 | 
					         <Label text="Client Version" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0" />
 | 
				
			||||||
 | 
					         <ChoiceBox fx:id="clientVersionChoiceBox" prefWidth="150.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="150.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
 | 
				
			||||||
 | 
					      </AnchorPane>
 | 
				
			||||||
 | 
					      <AnchorPane>
 | 
				
			||||||
 | 
					         <Label text="JVM Arguments" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0"/>
 | 
				
			||||||
 | 
					         <TextArea fx:id="jvmArgsTextArea" prefHeight="100.0" prefWidth="200.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="150.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"/>
 | 
				
			||||||
 | 
					      </AnchorPane>
 | 
				
			||||||
 | 
					   </VBox>
 | 
				
			||||||
 | 
					</AnchorPane>
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -0,0 +1,27 @@
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					    src: url('JetBrainsMono-Regular.ttf');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					    src: url('JetBrainsMono-Bold.ttf');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					    src: url('JetBrainsMono-Light.ttf');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					    src: url('JetBrainsMono-Italic.ttf');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					    src: url('JetBrainsMono-BoldItalic.ttf');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
					    src: url('JetBrainsMono-LightItalic.ttf');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.root {
 | 
				
			||||||
 | 
					    -fx-font-family: "JetBrains Mono";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,24 +1,50 @@
 | 
				
			||||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<?import javafx.geometry.*?>
 | 
				
			||||||
<?import javafx.scene.control.*?>
 | 
					<?import javafx.scene.control.*?>
 | 
				
			||||||
<?import javafx.scene.layout.*?>
 | 
					<?import javafx.scene.layout.*?>
 | 
				
			||||||
 | 
					<?import javafx.scene.text.*?>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<VBox xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/16"
 | 
					<VBox minHeight="300.0" minWidth="300.0" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="nl.andrewl.aos2_launcher.MainViewController">
 | 
				
			||||||
      maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0"
 | 
					    <TabPane tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
 | 
				
			||||||
      fx:controller="nl.andrewl.aos2_launcher.MainViewController"
 | 
					        <Tab text="Profiles">
 | 
				
			||||||
>
 | 
					            <VBox>
 | 
				
			||||||
    <MenuBar>
 | 
					                <HBox alignment="CENTER" styleClass="button-bar" VBox.vgrow="NEVER">
 | 
				
			||||||
        <Menu mnemonicParsing="false" text="File">
 | 
					                    <Button onAction="#addProfile" text="Add Profile" />
 | 
				
			||||||
            <MenuItem mnemonicParsing="false" text="Exit"/>
 | 
					                    <Button fx:id="editProfileButton" onAction="#editProfile" text="Edit Profile" />
 | 
				
			||||||
        </Menu>
 | 
					                    <Button fx:id="removeProfileButton" onAction="#removeProfile" text="Remove Profile" />
 | 
				
			||||||
        <Menu mnemonicParsing="false" text="Profiles">
 | 
					                </HBox>
 | 
				
			||||||
            <MenuItem mnemonicParsing="false" text="New Profile"/>
 | 
					                <ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS">
 | 
				
			||||||
        </Menu>
 | 
					                    <VBox fx:id="profilesVBox" styleClass="banner-list" />
 | 
				
			||||||
        <Menu mnemonicParsing="false" text="Help">
 | 
					                </ScrollPane>
 | 
				
			||||||
            <MenuItem mnemonicParsing="false" text="About"/>
 | 
					            </VBox>
 | 
				
			||||||
        </Menu>
 | 
					        </Tab>
 | 
				
			||||||
    </MenuBar>
 | 
					        <Tab text="Servers">
 | 
				
			||||||
    <ScrollPane VBox.vgrow="ALWAYS">
 | 
					            <VBox>
 | 
				
			||||||
        <TilePane fx:id="profilesTilePane"/>
 | 
					                <HBox alignment="CENTER" styleClass="button-bar" VBox.vgrow="NEVER">
 | 
				
			||||||
    </ScrollPane>
 | 
					                    <Button onAction="#refreshServers" text="Refresh" />
 | 
				
			||||||
 | 
					               <TextField fx:id="registryUrlField" prefWidth="300.0" promptText="Registry URL" text="http://localhost:8080" style="-fx-font-size: 10px;" />
 | 
				
			||||||
 | 
					                </HBox>
 | 
				
			||||||
 | 
					                <ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS">
 | 
				
			||||||
 | 
					                    <VBox fx:id="serversVBox" styleClass="banner-list" />
 | 
				
			||||||
 | 
					                </ScrollPane>
 | 
				
			||||||
 | 
					            </VBox>
 | 
				
			||||||
 | 
					        </Tab>
 | 
				
			||||||
 | 
					    </TabPane>
 | 
				
			||||||
 | 
					    <HBox alignment="CENTER" styleClass="button-bar" VBox.vgrow="NEVER">
 | 
				
			||||||
 | 
					        <Button fx:id="playButton" mnemonicParsing="false" onAction="#play" text="Play" />
 | 
				
			||||||
 | 
					    </HBox>
 | 
				
			||||||
 | 
					    <VBox fx:id="progressVBox" VBox.vgrow="NEVER">
 | 
				
			||||||
 | 
					        <AnchorPane VBox.vgrow="NEVER">
 | 
				
			||||||
 | 
					            <padding>
 | 
				
			||||||
 | 
					                <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
 | 
				
			||||||
 | 
					            </padding>
 | 
				
			||||||
 | 
					            <Label fx:id="progressLabel" text="Work in progress..." AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="0.0">
 | 
				
			||||||
 | 
					                <font>
 | 
				
			||||||
 | 
					                    <Font size="10.0" />
 | 
				
			||||||
 | 
					                </font>
 | 
				
			||||||
 | 
					            </Label>
 | 
				
			||||||
 | 
					            <ProgressBar fx:id="progressBar" prefWidth="200.0" progress="0.0" AnchorPane.bottomAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
 | 
				
			||||||
 | 
					        </AnchorPane>
 | 
				
			||||||
 | 
					    </VBox>
 | 
				
			||||||
</VBox>
 | 
					</VBox>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,31 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<?import javafx.geometry.Insets?>
 | 
				
			||||||
 | 
					<?import javafx.scene.control.*?>
 | 
				
			||||||
 | 
					<?import javafx.scene.layout.*?>
 | 
				
			||||||
 | 
					<BorderPane prefWidth="300.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
 | 
				
			||||||
 | 
					   <padding><Insets top="5" bottom="5" left="5" right="5"/></padding>
 | 
				
			||||||
 | 
					   <top>
 | 
				
			||||||
 | 
					      <Label fx:id="nameLabel" text="Profile Name" BorderPane.alignment="CENTER_LEFT" style="-fx-font-size: 16px; -fx-font-weight: bold;">
 | 
				
			||||||
 | 
					         <BorderPane.margin>
 | 
				
			||||||
 | 
					            <Insets bottom="5.0" />
 | 
				
			||||||
 | 
					         </BorderPane.margin>
 | 
				
			||||||
 | 
					      </Label>
 | 
				
			||||||
 | 
					   </top>
 | 
				
			||||||
 | 
					   <center>
 | 
				
			||||||
 | 
					      <VBox BorderPane.alignment="CENTER">
 | 
				
			||||||
 | 
					         <AnchorPane VBox.vgrow="NEVER">
 | 
				
			||||||
 | 
					            <Label text="Client Version" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
 | 
				
			||||||
 | 
					                   AnchorPane.topAnchor="0.0"/>
 | 
				
			||||||
 | 
					            <Label fx:id="clientVersionLabel" text="v1.0.0" textAlignment="RIGHT" AnchorPane.bottomAnchor="0.0"
 | 
				
			||||||
 | 
					                   AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" style="-fx-font-weight: bold;"/>
 | 
				
			||||||
 | 
					         </AnchorPane>
 | 
				
			||||||
 | 
					         <AnchorPane VBox.vgrow="NEVER">
 | 
				
			||||||
 | 
					            <Label text="Username" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
 | 
				
			||||||
 | 
					                   AnchorPane.topAnchor="0.0"/>
 | 
				
			||||||
 | 
					            <Label fx:id="usernameLabel" text="Player" textAlignment="RIGHT" AnchorPane.bottomAnchor="0.0"
 | 
				
			||||||
 | 
					                   AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" style="-fx-font-weight: bold;"/>
 | 
				
			||||||
 | 
					         </AnchorPane>
 | 
				
			||||||
 | 
					      </VBox>
 | 
				
			||||||
 | 
					   </center>
 | 
				
			||||||
 | 
					</BorderPane>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,22 @@
 | 
				
			||||||
 | 
					.test{
 | 
				
			||||||
 | 
					    -fx-background-color: blue;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.button-bar {
 | 
				
			||||||
 | 
					    -fx-padding: 5 0 5 0;
 | 
				
			||||||
 | 
					    -fx-spacing: 5;
 | 
				
			||||||
 | 
					    -fx-font-weight: bold;
 | 
				
			||||||
 | 
					    -fx-font-size: 16px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.banner-list {
 | 
				
			||||||
 | 
					    -fx-spacing: 5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.element-list-item:selected {
 | 
				
			||||||
 | 
					    -fx-background-color: #e3e3e3;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#playButton {
 | 
				
			||||||
 | 
					    -fx-border-radius: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								pom.xml
								
								
								
								
							
							
						
						
									
										2
									
								
								pom.xml
								
								
								
								
							| 
						 | 
					@ -7,7 +7,7 @@
 | 
				
			||||||
    <groupId>nl.andrewl</groupId>
 | 
					    <groupId>nl.andrewl</groupId>
 | 
				
			||||||
    <artifactId>ace-of-shades-2</artifactId>
 | 
					    <artifactId>ace-of-shades-2</artifactId>
 | 
				
			||||||
    <packaging>pom</packaging>
 | 
					    <packaging>pom</packaging>
 | 
				
			||||||
    <version>1.3.0</version>
 | 
					    <version>1.5.0</version>
 | 
				
			||||||
    <modules>
 | 
					    <modules>
 | 
				
			||||||
        <module>core</module>
 | 
					        <module>core</module>
 | 
				
			||||||
        <module>server</module>
 | 
					        <module>server</module>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					HELP.md
 | 
				
			||||||
 | 
					target/
 | 
				
			||||||
 | 
					!.mvn/wrapper/maven-wrapper.jar
 | 
				
			||||||
 | 
					!**/src/main/**/target/
 | 
				
			||||||
 | 
					!**/src/test/**/target/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### STS ###
 | 
				
			||||||
 | 
					.apt_generated
 | 
				
			||||||
 | 
					.classpath
 | 
				
			||||||
 | 
					.factorypath
 | 
				
			||||||
 | 
					.project
 | 
				
			||||||
 | 
					.settings
 | 
				
			||||||
 | 
					.springBeans
 | 
				
			||||||
 | 
					.sts4-cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### IntelliJ IDEA ###
 | 
				
			||||||
 | 
					.idea
 | 
				
			||||||
 | 
					*.iws
 | 
				
			||||||
 | 
					*.iml
 | 
				
			||||||
 | 
					*.ipr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### NetBeans ###
 | 
				
			||||||
 | 
					/nbproject/private/
 | 
				
			||||||
 | 
					/nbbuild/
 | 
				
			||||||
 | 
					/dist/
 | 
				
			||||||
 | 
					/nbdist/
 | 
				
			||||||
 | 
					/.nb-gradle/
 | 
				
			||||||
 | 
					build/
 | 
				
			||||||
 | 
					!**/src/main/**/build/
 | 
				
			||||||
 | 
					!**/src/test/**/build/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### VS Code ###
 | 
				
			||||||
 | 
					.vscode/
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
 | 
				
			||||||
 | 
					wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					# Ace of Shades Server Registry
 | 
				
			||||||
 | 
					The registry is a REST API that keeps track of any servers that have recently announced their status to it. Servers can periodically send a simple JSON object with metadata about the server (name, description, players, etc.) so that players can more easily search for a server to play on.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fetching
 | 
				
			||||||
 | 
					Client/launcher applications that want to get a list of servers from the registry should send a GET request to the API's `/servers` endpoint.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The following array of servers is returned from GET requests to the API's `/servers` endpoint:
 | 
				
			||||||
 | 
					```json
 | 
				
			||||||
 | 
					[
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        "host": "0:0:0:0:0:0:0:1",
 | 
				
			||||||
 | 
					        "port": 1234,
 | 
				
			||||||
 | 
					        "name": "Andrew's Server",
 | 
				
			||||||
 | 
					        "description": "A good server.",
 | 
				
			||||||
 | 
					        "maxPlayers": 32,
 | 
				
			||||||
 | 
					        "currentPlayers": 2,
 | 
				
			||||||
 | 
					        "lastUpdatedAt": 1659710488855
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Posting
 | 
				
			||||||
 | 
					The following payload should be sent by servers to the API's `/servers` endpoint via POST:
 | 
				
			||||||
 | 
					```json
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    "port": 1234,
 | 
				
			||||||
 | 
					    "token": "abc123",
 | 
				
			||||||
 | 
					    "name": "Andrew's Server",
 | 
				
			||||||
 | 
					    "description": "A good server.",
 | 
				
			||||||
 | 
					    "maxPlayers": 32,
 | 
				
			||||||
 | 
					    "currentPlayers": 2
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					Note that this should only be done at most once per minute. Any more frequent, and you'll receive 429 Too-Many-Requests responses, and continued spam may permanently block your server.
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					#!/usr/bin/env bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Put your GRAALVM location here.
 | 
				
			||||||
 | 
					export GRAALVM_HOME=/home/andrew/Downloads/graalvm-ce-java17-22.2.0
 | 
				
			||||||
 | 
					mvn -Pnative -DskipTests clean package
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,316 @@
 | 
				
			||||||
 | 
					#!/bin/sh
 | 
				
			||||||
 | 
					# ----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					# Licensed to the Apache Software Foundation (ASF) under one
 | 
				
			||||||
 | 
					# or more contributor license agreements.  See the NOTICE file
 | 
				
			||||||
 | 
					# distributed with this work for additional information
 | 
				
			||||||
 | 
					# regarding copyright ownership.  The ASF licenses this file
 | 
				
			||||||
 | 
					# to you under the Apache License, Version 2.0 (the
 | 
				
			||||||
 | 
					# "License"); you may not use this file except in compliance
 | 
				
			||||||
 | 
					# with the License.  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#    https://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Unless required by applicable law or agreed to in writing,
 | 
				
			||||||
 | 
					# software distributed under the License is distributed on an
 | 
				
			||||||
 | 
					# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 | 
				
			||||||
 | 
					# KIND, either express or implied.  See the License for the
 | 
				
			||||||
 | 
					# specific language governing permissions and limitations
 | 
				
			||||||
 | 
					# under the License.
 | 
				
			||||||
 | 
					# ----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# ----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					# Maven Start Up Batch script
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Required ENV vars:
 | 
				
			||||||
 | 
					# ------------------
 | 
				
			||||||
 | 
					#   JAVA_HOME - location of a JDK home dir
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Optional ENV vars
 | 
				
			||||||
 | 
					# -----------------
 | 
				
			||||||
 | 
					#   M2_HOME - location of maven2's installed home dir
 | 
				
			||||||
 | 
					#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
 | 
				
			||||||
 | 
					#     e.g. to debug Maven itself, use
 | 
				
			||||||
 | 
					#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
 | 
				
			||||||
 | 
					#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
 | 
				
			||||||
 | 
					# ----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ -z "$MAVEN_SKIP_RC" ] ; then
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if [ -f /usr/local/etc/mavenrc ] ; then
 | 
				
			||||||
 | 
					    . /usr/local/etc/mavenrc
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if [ -f /etc/mavenrc ] ; then
 | 
				
			||||||
 | 
					    . /etc/mavenrc
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if [ -f "$HOME/.mavenrc" ] ; then
 | 
				
			||||||
 | 
					    . "$HOME/.mavenrc"
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# OS specific support.  $var _must_ be set to either true or false.
 | 
				
			||||||
 | 
					cygwin=false;
 | 
				
			||||||
 | 
					darwin=false;
 | 
				
			||||||
 | 
					mingw=false
 | 
				
			||||||
 | 
					case "`uname`" in
 | 
				
			||||||
 | 
					  CYGWIN*) cygwin=true ;;
 | 
				
			||||||
 | 
					  MINGW*) mingw=true;;
 | 
				
			||||||
 | 
					  Darwin*) darwin=true
 | 
				
			||||||
 | 
					    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
 | 
				
			||||||
 | 
					    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
 | 
				
			||||||
 | 
					    if [ -z "$JAVA_HOME" ]; then
 | 
				
			||||||
 | 
					      if [ -x "/usr/libexec/java_home" ]; then
 | 
				
			||||||
 | 
					        export JAVA_HOME="`/usr/libexec/java_home`"
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        export JAVA_HOME="/Library/Java/Home"
 | 
				
			||||||
 | 
					      fi
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    ;;
 | 
				
			||||||
 | 
					esac
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ -z "$JAVA_HOME" ] ; then
 | 
				
			||||||
 | 
					  if [ -r /etc/gentoo-release ] ; then
 | 
				
			||||||
 | 
					    JAVA_HOME=`java-config --jre-home`
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ -z "$M2_HOME" ] ; then
 | 
				
			||||||
 | 
					  ## resolve links - $0 may be a link to maven's home
 | 
				
			||||||
 | 
					  PRG="$0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # need this for relative symlinks
 | 
				
			||||||
 | 
					  while [ -h "$PRG" ] ; do
 | 
				
			||||||
 | 
					    ls=`ls -ld "$PRG"`
 | 
				
			||||||
 | 
					    link=`expr "$ls" : '.*-> \(.*\)$'`
 | 
				
			||||||
 | 
					    if expr "$link" : '/.*' > /dev/null; then
 | 
				
			||||||
 | 
					      PRG="$link"
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      PRG="`dirname "$PRG"`/$link"
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					  done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  saveddir=`pwd`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  M2_HOME=`dirname "$PRG"`/..
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # make it fully qualified
 | 
				
			||||||
 | 
					  M2_HOME=`cd "$M2_HOME" && pwd`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  cd "$saveddir"
 | 
				
			||||||
 | 
					  # echo Using m2 at $M2_HOME
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# For Cygwin, ensure paths are in UNIX format before anything is touched
 | 
				
			||||||
 | 
					if $cygwin ; then
 | 
				
			||||||
 | 
					  [ -n "$M2_HOME" ] &&
 | 
				
			||||||
 | 
					    M2_HOME=`cygpath --unix "$M2_HOME"`
 | 
				
			||||||
 | 
					  [ -n "$JAVA_HOME" ] &&
 | 
				
			||||||
 | 
					    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
 | 
				
			||||||
 | 
					  [ -n "$CLASSPATH" ] &&
 | 
				
			||||||
 | 
					    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# For Mingw, ensure paths are in UNIX format before anything is touched
 | 
				
			||||||
 | 
					if $mingw ; then
 | 
				
			||||||
 | 
					  [ -n "$M2_HOME" ] &&
 | 
				
			||||||
 | 
					    M2_HOME="`(cd "$M2_HOME"; pwd)`"
 | 
				
			||||||
 | 
					  [ -n "$JAVA_HOME" ] &&
 | 
				
			||||||
 | 
					    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ -z "$JAVA_HOME" ]; then
 | 
				
			||||||
 | 
					  javaExecutable="`which javac`"
 | 
				
			||||||
 | 
					  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
 | 
				
			||||||
 | 
					    # readlink(1) is not available as standard on Solaris 10.
 | 
				
			||||||
 | 
					    readLink=`which readlink`
 | 
				
			||||||
 | 
					    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
 | 
				
			||||||
 | 
					      if $darwin ; then
 | 
				
			||||||
 | 
					        javaHome="`dirname \"$javaExecutable\"`"
 | 
				
			||||||
 | 
					        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        javaExecutable="`readlink -f \"$javaExecutable\"`"
 | 
				
			||||||
 | 
					      fi
 | 
				
			||||||
 | 
					      javaHome="`dirname \"$javaExecutable\"`"
 | 
				
			||||||
 | 
					      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
 | 
				
			||||||
 | 
					      JAVA_HOME="$javaHome"
 | 
				
			||||||
 | 
					      export JAVA_HOME
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ -z "$JAVACMD" ] ; then
 | 
				
			||||||
 | 
					  if [ -n "$JAVA_HOME"  ] ; then
 | 
				
			||||||
 | 
					    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
 | 
				
			||||||
 | 
					      # IBM's JDK on AIX uses strange locations for the executables
 | 
				
			||||||
 | 
					      JAVACMD="$JAVA_HOME/jre/sh/java"
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      JAVACMD="$JAVA_HOME/bin/java"
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					  else
 | 
				
			||||||
 | 
					    JAVACMD="`\\unset -f command; \\command -v java`"
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ ! -x "$JAVACMD" ] ; then
 | 
				
			||||||
 | 
					  echo "Error: JAVA_HOME is not defined correctly." >&2
 | 
				
			||||||
 | 
					  echo "  We cannot execute $JAVACMD" >&2
 | 
				
			||||||
 | 
					  exit 1
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ -z "$JAVA_HOME" ] ; then
 | 
				
			||||||
 | 
					  echo "Warning: JAVA_HOME environment variable is not set."
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# traverses directory structure from process work directory to filesystem root
 | 
				
			||||||
 | 
					# first directory with .mvn subdirectory is considered project base directory
 | 
				
			||||||
 | 
					find_maven_basedir() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if [ -z "$1" ]
 | 
				
			||||||
 | 
					  then
 | 
				
			||||||
 | 
					    echo "Path not specified to find_maven_basedir"
 | 
				
			||||||
 | 
					    return 1
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  basedir="$1"
 | 
				
			||||||
 | 
					  wdir="$1"
 | 
				
			||||||
 | 
					  while [ "$wdir" != '/' ] ; do
 | 
				
			||||||
 | 
					    if [ -d "$wdir"/.mvn ] ; then
 | 
				
			||||||
 | 
					      basedir=$wdir
 | 
				
			||||||
 | 
					      break
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
 | 
				
			||||||
 | 
					    if [ -d "${wdir}" ]; then
 | 
				
			||||||
 | 
					      wdir=`cd "$wdir/.."; pwd`
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    # end of workaround
 | 
				
			||||||
 | 
					  done
 | 
				
			||||||
 | 
					  echo "${basedir}"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# concatenates all lines of a file
 | 
				
			||||||
 | 
					concat_lines() {
 | 
				
			||||||
 | 
					  if [ -f "$1" ]; then
 | 
				
			||||||
 | 
					    echo "$(tr -s '\n' ' ' < "$1")"
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					BASE_DIR=`find_maven_basedir "$(pwd)"`
 | 
				
			||||||
 | 
					if [ -z "$BASE_DIR" ]; then
 | 
				
			||||||
 | 
					  exit 1;
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					##########################################################################################
 | 
				
			||||||
 | 
					# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
 | 
				
			||||||
 | 
					# This allows using the maven wrapper in projects that prohibit checking in binary data.
 | 
				
			||||||
 | 
					##########################################################################################
 | 
				
			||||||
 | 
					if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
 | 
				
			||||||
 | 
					    if [ "$MVNW_VERBOSE" = true ]; then
 | 
				
			||||||
 | 
					      echo "Found .mvn/wrapper/maven-wrapper.jar"
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					    if [ "$MVNW_VERBOSE" = true ]; then
 | 
				
			||||||
 | 
					      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    if [ -n "$MVNW_REPOURL" ]; then
 | 
				
			||||||
 | 
					      jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    while IFS="=" read key value; do
 | 
				
			||||||
 | 
					      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
 | 
				
			||||||
 | 
					      esac
 | 
				
			||||||
 | 
					    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
 | 
				
			||||||
 | 
					    if [ "$MVNW_VERBOSE" = true ]; then
 | 
				
			||||||
 | 
					      echo "Downloading from: $jarUrl"
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
 | 
				
			||||||
 | 
					    if $cygwin; then
 | 
				
			||||||
 | 
					      wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if command -v wget > /dev/null; then
 | 
				
			||||||
 | 
					        if [ "$MVNW_VERBOSE" = true ]; then
 | 
				
			||||||
 | 
					          echo "Found wget ... using wget"
 | 
				
			||||||
 | 
					        fi
 | 
				
			||||||
 | 
					        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
 | 
				
			||||||
 | 
					            wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					            wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
 | 
				
			||||||
 | 
					        fi
 | 
				
			||||||
 | 
					    elif command -v curl > /dev/null; then
 | 
				
			||||||
 | 
					        if [ "$MVNW_VERBOSE" = true ]; then
 | 
				
			||||||
 | 
					          echo "Found curl ... using curl"
 | 
				
			||||||
 | 
					        fi
 | 
				
			||||||
 | 
					        if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
 | 
				
			||||||
 | 
					            curl -o "$wrapperJarPath" "$jarUrl" -f
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					            curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
 | 
				
			||||||
 | 
					        fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					        if [ "$MVNW_VERBOSE" = true ]; then
 | 
				
			||||||
 | 
					          echo "Falling back to using Java to download"
 | 
				
			||||||
 | 
					        fi
 | 
				
			||||||
 | 
					        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
 | 
				
			||||||
 | 
					        # For Cygwin, switch paths to Windows format before running javac
 | 
				
			||||||
 | 
					        if $cygwin; then
 | 
				
			||||||
 | 
					          javaClass=`cygpath --path --windows "$javaClass"`
 | 
				
			||||||
 | 
					        fi
 | 
				
			||||||
 | 
					        if [ -e "$javaClass" ]; then
 | 
				
			||||||
 | 
					            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
 | 
				
			||||||
 | 
					                if [ "$MVNW_VERBOSE" = true ]; then
 | 
				
			||||||
 | 
					                  echo " - Compiling MavenWrapperDownloader.java ..."
 | 
				
			||||||
 | 
					                fi
 | 
				
			||||||
 | 
					                # Compiling the Java class
 | 
				
			||||||
 | 
					                ("$JAVA_HOME/bin/javac" "$javaClass")
 | 
				
			||||||
 | 
					            fi
 | 
				
			||||||
 | 
					            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
 | 
				
			||||||
 | 
					                # Running the downloader
 | 
				
			||||||
 | 
					                if [ "$MVNW_VERBOSE" = true ]; then
 | 
				
			||||||
 | 
					                  echo " - Running MavenWrapperDownloader.java ..."
 | 
				
			||||||
 | 
					                fi
 | 
				
			||||||
 | 
					                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
 | 
				
			||||||
 | 
					            fi
 | 
				
			||||||
 | 
					        fi
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					##########################################################################################
 | 
				
			||||||
 | 
					# End of extension
 | 
				
			||||||
 | 
					##########################################################################################
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
 | 
				
			||||||
 | 
					if [ "$MVNW_VERBOSE" = true ]; then
 | 
				
			||||||
 | 
					  echo $MAVEN_PROJECTBASEDIR
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# For Cygwin, switch paths to Windows format before running java
 | 
				
			||||||
 | 
					if $cygwin; then
 | 
				
			||||||
 | 
					  [ -n "$M2_HOME" ] &&
 | 
				
			||||||
 | 
					    M2_HOME=`cygpath --path --windows "$M2_HOME"`
 | 
				
			||||||
 | 
					  [ -n "$JAVA_HOME" ] &&
 | 
				
			||||||
 | 
					    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
 | 
				
			||||||
 | 
					  [ -n "$CLASSPATH" ] &&
 | 
				
			||||||
 | 
					    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
 | 
				
			||||||
 | 
					  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
 | 
				
			||||||
 | 
					    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Provide a "standardized" way to retrieve the CLI args that will
 | 
				
			||||||
 | 
					# work with both Windows and non-Windows executions.
 | 
				
			||||||
 | 
					MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
 | 
				
			||||||
 | 
					export MAVEN_CMD_LINE_ARGS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					exec "$JAVACMD" \
 | 
				
			||||||
 | 
					  $MAVEN_OPTS \
 | 
				
			||||||
 | 
					  $MAVEN_DEBUG_OPTS \
 | 
				
			||||||
 | 
					  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
 | 
				
			||||||
 | 
					  "-Dmaven.home=${M2_HOME}" \
 | 
				
			||||||
 | 
					  "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
 | 
				
			||||||
 | 
					  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,188 @@
 | 
				
			||||||
 | 
					@REM ----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					@REM Licensed to the Apache Software Foundation (ASF) under one
 | 
				
			||||||
 | 
					@REM or more contributor license agreements.  See the NOTICE file
 | 
				
			||||||
 | 
					@REM distributed with this work for additional information
 | 
				
			||||||
 | 
					@REM regarding copyright ownership.  The ASF licenses this file
 | 
				
			||||||
 | 
					@REM to you under the Apache License, Version 2.0 (the
 | 
				
			||||||
 | 
					@REM "License"); you may not use this file except in compliance
 | 
				
			||||||
 | 
					@REM with the License.  You may obtain a copy of the License at
 | 
				
			||||||
 | 
					@REM
 | 
				
			||||||
 | 
					@REM    https://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					@REM
 | 
				
			||||||
 | 
					@REM Unless required by applicable law or agreed to in writing,
 | 
				
			||||||
 | 
					@REM software distributed under the License is distributed on an
 | 
				
			||||||
 | 
					@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 | 
				
			||||||
 | 
					@REM KIND, either express or implied.  See the License for the
 | 
				
			||||||
 | 
					@REM specific language governing permissions and limitations
 | 
				
			||||||
 | 
					@REM under the License.
 | 
				
			||||||
 | 
					@REM ----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM ----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					@REM Maven Start Up Batch script
 | 
				
			||||||
 | 
					@REM
 | 
				
			||||||
 | 
					@REM Required ENV vars:
 | 
				
			||||||
 | 
					@REM JAVA_HOME - location of a JDK home dir
 | 
				
			||||||
 | 
					@REM
 | 
				
			||||||
 | 
					@REM Optional ENV vars
 | 
				
			||||||
 | 
					@REM M2_HOME - location of maven2's installed home dir
 | 
				
			||||||
 | 
					@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
 | 
				
			||||||
 | 
					@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
 | 
				
			||||||
 | 
					@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
 | 
				
			||||||
 | 
					@REM     e.g. to debug Maven itself, use
 | 
				
			||||||
 | 
					@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
 | 
				
			||||||
 | 
					@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
 | 
				
			||||||
 | 
					@REM ----------------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
 | 
				
			||||||
 | 
					@echo off
 | 
				
			||||||
 | 
					@REM set title of command window
 | 
				
			||||||
 | 
					title %0
 | 
				
			||||||
 | 
					@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
 | 
				
			||||||
 | 
					@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM set %HOME% to equivalent of $HOME
 | 
				
			||||||
 | 
					if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM Execute a user defined script before this one
 | 
				
			||||||
 | 
					if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
 | 
				
			||||||
 | 
					@REM check for pre script, once with legacy .bat ending and once with .cmd ending
 | 
				
			||||||
 | 
					if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
 | 
				
			||||||
 | 
					if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
 | 
				
			||||||
 | 
					:skipRcPre
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@setlocal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set ERROR_CODE=0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM To isolate internal variables from possible post scripts, we use another setlocal
 | 
				
			||||||
 | 
					@setlocal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM ==== START VALIDATION ====
 | 
				
			||||||
 | 
					if not "%JAVA_HOME%" == "" goto OkJHome
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo.
 | 
				
			||||||
 | 
					echo Error: JAVA_HOME not found in your environment. >&2
 | 
				
			||||||
 | 
					echo Please set the JAVA_HOME variable in your environment to match the >&2
 | 
				
			||||||
 | 
					echo location of your Java installation. >&2
 | 
				
			||||||
 | 
					echo.
 | 
				
			||||||
 | 
					goto error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:OkJHome
 | 
				
			||||||
 | 
					if exist "%JAVA_HOME%\bin\java.exe" goto init
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo.
 | 
				
			||||||
 | 
					echo Error: JAVA_HOME is set to an invalid directory. >&2
 | 
				
			||||||
 | 
					echo JAVA_HOME = "%JAVA_HOME%" >&2
 | 
				
			||||||
 | 
					echo Please set the JAVA_HOME variable in your environment to match the >&2
 | 
				
			||||||
 | 
					echo location of your Java installation. >&2
 | 
				
			||||||
 | 
					echo.
 | 
				
			||||||
 | 
					goto error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM ==== END VALIDATION ====
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:init
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
 | 
				
			||||||
 | 
					@REM Fallback to current working directory if not found.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
 | 
				
			||||||
 | 
					IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set EXEC_DIR=%CD%
 | 
				
			||||||
 | 
					set WDIR=%EXEC_DIR%
 | 
				
			||||||
 | 
					:findBaseDir
 | 
				
			||||||
 | 
					IF EXIST "%WDIR%"\.mvn goto baseDirFound
 | 
				
			||||||
 | 
					cd ..
 | 
				
			||||||
 | 
					IF "%WDIR%"=="%CD%" goto baseDirNotFound
 | 
				
			||||||
 | 
					set WDIR=%CD%
 | 
				
			||||||
 | 
					goto findBaseDir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:baseDirFound
 | 
				
			||||||
 | 
					set MAVEN_PROJECTBASEDIR=%WDIR%
 | 
				
			||||||
 | 
					cd "%EXEC_DIR%"
 | 
				
			||||||
 | 
					goto endDetectBaseDir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:baseDirNotFound
 | 
				
			||||||
 | 
					set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
 | 
				
			||||||
 | 
					cd "%EXEC_DIR%"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:endDetectBaseDir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@setlocal EnableExtensions EnableDelayedExpansion
 | 
				
			||||||
 | 
					for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
 | 
				
			||||||
 | 
					@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:endReadAdditionalConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
 | 
				
			||||||
 | 
					set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
 | 
				
			||||||
 | 
					set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
 | 
				
			||||||
 | 
					    IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
 | 
				
			||||||
 | 
					@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
 | 
				
			||||||
 | 
					if exist %WRAPPER_JAR% (
 | 
				
			||||||
 | 
					    if "%MVNW_VERBOSE%" == "true" (
 | 
				
			||||||
 | 
					        echo Found %WRAPPER_JAR%
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					) else (
 | 
				
			||||||
 | 
					    if not "%MVNW_REPOURL%" == "" (
 | 
				
			||||||
 | 
					        SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    if "%MVNW_VERBOSE%" == "true" (
 | 
				
			||||||
 | 
					        echo Couldn't find %WRAPPER_JAR%, downloading it ...
 | 
				
			||||||
 | 
					        echo Downloading from: %DOWNLOAD_URL%
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    powershell -Command "&{"^
 | 
				
			||||||
 | 
							"$webclient = new-object System.Net.WebClient;"^
 | 
				
			||||||
 | 
							"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
 | 
				
			||||||
 | 
							"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
 | 
				
			||||||
 | 
							"}"^
 | 
				
			||||||
 | 
							"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
 | 
				
			||||||
 | 
							"}"
 | 
				
			||||||
 | 
					    if "%MVNW_VERBOSE%" == "true" (
 | 
				
			||||||
 | 
					        echo Finished downloading %WRAPPER_JAR%
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					@REM End of extension
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM Provide a "standardized" way to retrieve the CLI args that will
 | 
				
			||||||
 | 
					@REM work with both Windows and non-Windows executions.
 | 
				
			||||||
 | 
					set MAVEN_CMD_LINE_ARGS=%*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					%MAVEN_JAVA_EXE% ^
 | 
				
			||||||
 | 
					  %JVM_CONFIG_MAVEN_PROPS% ^
 | 
				
			||||||
 | 
					  %MAVEN_OPTS% ^
 | 
				
			||||||
 | 
					  %MAVEN_DEBUG_OPTS% ^
 | 
				
			||||||
 | 
					  -classpath %WRAPPER_JAR% ^
 | 
				
			||||||
 | 
					  "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
 | 
				
			||||||
 | 
					  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
 | 
				
			||||||
 | 
					if ERRORLEVEL 1 goto error
 | 
				
			||||||
 | 
					goto end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:error
 | 
				
			||||||
 | 
					set ERROR_CODE=1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:end
 | 
				
			||||||
 | 
					@endlocal & set ERROR_CODE=%ERROR_CODE%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
 | 
				
			||||||
 | 
					@REM check for post script, once with legacy .bat ending and once with .cmd ending
 | 
				
			||||||
 | 
					if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
 | 
				
			||||||
 | 
					if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
 | 
				
			||||||
 | 
					:skipRcPost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
 | 
				
			||||||
 | 
					if "%MAVEN_BATCH_PAUSE%"=="on" pause
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					cmd /C exit /B %ERROR_CODE%
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,150 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
				
			||||||
 | 
						xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
				
			||||||
 | 
						<modelVersion>4.0.0</modelVersion>
 | 
				
			||||||
 | 
						<parent>
 | 
				
			||||||
 | 
							<groupId>org.springframework.boot</groupId>
 | 
				
			||||||
 | 
							<artifactId>spring-boot-starter-parent</artifactId>
 | 
				
			||||||
 | 
							<version>2.7.2</version>
 | 
				
			||||||
 | 
							<relativePath/> <!-- lookup parent from repository -->
 | 
				
			||||||
 | 
						</parent>
 | 
				
			||||||
 | 
						<groupId>nl.andrewl</groupId>
 | 
				
			||||||
 | 
						<artifactId>aos2-registry-api</artifactId>
 | 
				
			||||||
 | 
						<version>0.0.1-SNAPSHOT</version>
 | 
				
			||||||
 | 
						<name>aos2-registry-api</name>
 | 
				
			||||||
 | 
						<description>Registry API for Ace of Shades 2 servers.</description>
 | 
				
			||||||
 | 
						<properties>
 | 
				
			||||||
 | 
							<java.version>17</java.version>
 | 
				
			||||||
 | 
							<repackage.classifier/>
 | 
				
			||||||
 | 
							<spring-native.version>0.12.1</spring-native.version>
 | 
				
			||||||
 | 
						</properties>
 | 
				
			||||||
 | 
						<dependencies>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>org.springframework.boot</groupId>
 | 
				
			||||||
 | 
								<artifactId>spring-boot-starter-webflux</artifactId>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>org.springframework.experimental</groupId>
 | 
				
			||||||
 | 
								<artifactId>spring-native</artifactId>
 | 
				
			||||||
 | 
								<version>${spring-native.version}</version>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>org.springframework.boot</groupId>
 | 
				
			||||||
 | 
								<artifactId>spring-boot-devtools</artifactId>
 | 
				
			||||||
 | 
								<scope>runtime</scope>
 | 
				
			||||||
 | 
								<optional>true</optional>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>org.springframework.boot</groupId>
 | 
				
			||||||
 | 
								<artifactId>spring-boot-starter-test</artifactId>
 | 
				
			||||||
 | 
								<scope>test</scope>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
 | 
							<dependency>
 | 
				
			||||||
 | 
								<groupId>io.projectreactor</groupId>
 | 
				
			||||||
 | 
								<artifactId>reactor-test</artifactId>
 | 
				
			||||||
 | 
								<scope>test</scope>
 | 
				
			||||||
 | 
							</dependency>
 | 
				
			||||||
 | 
						</dependencies>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<build>
 | 
				
			||||||
 | 
							<plugins>
 | 
				
			||||||
 | 
								<plugin>
 | 
				
			||||||
 | 
									<groupId>org.springframework.boot</groupId>
 | 
				
			||||||
 | 
									<artifactId>spring-boot-maven-plugin</artifactId>
 | 
				
			||||||
 | 
									<configuration>
 | 
				
			||||||
 | 
										<classifier>${repackage.classifier}</classifier>
 | 
				
			||||||
 | 
										<image>
 | 
				
			||||||
 | 
											<builder>paketobuildpacks/builder:tiny</builder>
 | 
				
			||||||
 | 
											<env>
 | 
				
			||||||
 | 
												<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
 | 
				
			||||||
 | 
											</env>
 | 
				
			||||||
 | 
										</image>
 | 
				
			||||||
 | 
									</configuration>
 | 
				
			||||||
 | 
								</plugin>
 | 
				
			||||||
 | 
								<plugin>
 | 
				
			||||||
 | 
									<groupId>org.springframework.experimental</groupId>
 | 
				
			||||||
 | 
									<artifactId>spring-aot-maven-plugin</artifactId>
 | 
				
			||||||
 | 
									<version>${spring-native.version}</version>
 | 
				
			||||||
 | 
									<executions>
 | 
				
			||||||
 | 
										<execution>
 | 
				
			||||||
 | 
											<id>test-generate</id>
 | 
				
			||||||
 | 
											<goals>
 | 
				
			||||||
 | 
												<goal>test-generate</goal>
 | 
				
			||||||
 | 
											</goals>
 | 
				
			||||||
 | 
										</execution>
 | 
				
			||||||
 | 
										<execution>
 | 
				
			||||||
 | 
											<id>generate</id>
 | 
				
			||||||
 | 
											<goals>
 | 
				
			||||||
 | 
												<goal>generate</goal>
 | 
				
			||||||
 | 
											</goals>
 | 
				
			||||||
 | 
										</execution>
 | 
				
			||||||
 | 
									</executions>
 | 
				
			||||||
 | 
								</plugin>
 | 
				
			||||||
 | 
							</plugins>
 | 
				
			||||||
 | 
						</build>
 | 
				
			||||||
 | 
						<repositories>
 | 
				
			||||||
 | 
							<repository>
 | 
				
			||||||
 | 
								<id>spring-releases</id>
 | 
				
			||||||
 | 
								<name>Spring Releases</name>
 | 
				
			||||||
 | 
								<url>https://repo.spring.io/release</url>
 | 
				
			||||||
 | 
								<snapshots>
 | 
				
			||||||
 | 
									<enabled>false</enabled>
 | 
				
			||||||
 | 
								</snapshots>
 | 
				
			||||||
 | 
							</repository>
 | 
				
			||||||
 | 
						</repositories>
 | 
				
			||||||
 | 
						<pluginRepositories>
 | 
				
			||||||
 | 
							<pluginRepository>
 | 
				
			||||||
 | 
								<id>spring-releases</id>
 | 
				
			||||||
 | 
								<name>Spring Releases</name>
 | 
				
			||||||
 | 
								<url>https://repo.spring.io/release</url>
 | 
				
			||||||
 | 
								<snapshots>
 | 
				
			||||||
 | 
									<enabled>false</enabled>
 | 
				
			||||||
 | 
								</snapshots>
 | 
				
			||||||
 | 
							</pluginRepository>
 | 
				
			||||||
 | 
						</pluginRepositories>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<profiles>
 | 
				
			||||||
 | 
							<profile>
 | 
				
			||||||
 | 
								<id>native</id>
 | 
				
			||||||
 | 
								<properties>
 | 
				
			||||||
 | 
									<repackage.classifier>exec</repackage.classifier>
 | 
				
			||||||
 | 
									<native-buildtools.version>0.9.13</native-buildtools.version>
 | 
				
			||||||
 | 
								</properties>
 | 
				
			||||||
 | 
								<dependencies>
 | 
				
			||||||
 | 
									<dependency>
 | 
				
			||||||
 | 
										<groupId>org.junit.platform</groupId>
 | 
				
			||||||
 | 
										<artifactId>junit-platform-launcher</artifactId>
 | 
				
			||||||
 | 
										<scope>test</scope>
 | 
				
			||||||
 | 
									</dependency>
 | 
				
			||||||
 | 
								</dependencies>
 | 
				
			||||||
 | 
								<build>
 | 
				
			||||||
 | 
									<plugins>
 | 
				
			||||||
 | 
										<plugin>
 | 
				
			||||||
 | 
											<groupId>org.graalvm.buildtools</groupId>
 | 
				
			||||||
 | 
											<artifactId>native-maven-plugin</artifactId>
 | 
				
			||||||
 | 
											<version>${native-buildtools.version}</version>
 | 
				
			||||||
 | 
											<extensions>true</extensions>
 | 
				
			||||||
 | 
											<executions>
 | 
				
			||||||
 | 
												<execution>
 | 
				
			||||||
 | 
													<id>test-native</id>
 | 
				
			||||||
 | 
													<phase>test</phase>
 | 
				
			||||||
 | 
													<goals>
 | 
				
			||||||
 | 
														<goal>test</goal>
 | 
				
			||||||
 | 
													</goals>
 | 
				
			||||||
 | 
												</execution>
 | 
				
			||||||
 | 
												<execution>
 | 
				
			||||||
 | 
													<id>build-native</id>
 | 
				
			||||||
 | 
													<phase>package</phase>
 | 
				
			||||||
 | 
													<goals>
 | 
				
			||||||
 | 
														<goal>build</goal>
 | 
				
			||||||
 | 
													</goals>
 | 
				
			||||||
 | 
												</execution>
 | 
				
			||||||
 | 
											</executions>
 | 
				
			||||||
 | 
										</plugin>
 | 
				
			||||||
 | 
									</plugins>
 | 
				
			||||||
 | 
								</build>
 | 
				
			||||||
 | 
							</profile>
 | 
				
			||||||
 | 
						</profiles>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2registryapi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.boot.SpringApplication;
 | 
				
			||||||
 | 
					import org.springframework.boot.autoconfigure.SpringBootApplication;
 | 
				
			||||||
 | 
					import org.springframework.scheduling.annotation.EnableScheduling;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@SpringBootApplication
 | 
				
			||||||
 | 
					@EnableScheduling
 | 
				
			||||||
 | 
					public class Aos2RegistryApiApplication {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static void main(String[] args) {
 | 
				
			||||||
 | 
							SpringApplication.run(Aos2RegistryApiApplication.class, args);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2registryapi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.aos2registryapi.dto.ServerInfoPayload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.ArrayList;
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ServerInfoValidator {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public boolean validateName(String name) {
 | 
				
			||||||
 | 
							return name != null && !name.isBlank() && name.length() <= 64;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public boolean validateDescription(String description) {
 | 
				
			||||||
 | 
							return description == null ||
 | 
				
			||||||
 | 
									(!description.isBlank() && description.length() <= 256);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public boolean validatePlayerCounts(int max, int current) {
 | 
				
			||||||
 | 
							return max > 0 && current >= 0 && current <= max && max < 1000;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Optional<List<String>> validatePayload(ServerInfoPayload payload) {
 | 
				
			||||||
 | 
							List<String> messages = new ArrayList<>(3);
 | 
				
			||||||
 | 
							if (payload.port() < 0 || payload.port() > 65535) messages.add("Invalid port.");
 | 
				
			||||||
 | 
							if (!validateName(payload.name())) messages.add("Invalid name.");
 | 
				
			||||||
 | 
							if (!validateDescription(payload.description())) messages.add("Invalid description.");
 | 
				
			||||||
 | 
							if (!validatePlayerCounts(payload.maxPlayers(), payload.currentPlayers())) messages.add("Invalid player counts.");
 | 
				
			||||||
 | 
							if (messages.size() > 0) return Optional.of(messages);
 | 
				
			||||||
 | 
							return Optional.empty();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,89 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2registryapi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.aos2registryapi.dto.ServerInfoPayload;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2registryapi.dto.ServerInfoResponse;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2registryapi.model.ServerIdentifier;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2registryapi.model.ServerInfo;
 | 
				
			||||||
 | 
					import org.slf4j.Logger;
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
 | 
					import org.springframework.scheduling.annotation.Scheduled;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Component;
 | 
				
			||||||
 | 
					import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					import reactor.core.publisher.Flux;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.time.Duration;
 | 
				
			||||||
 | 
					import java.time.Instant;
 | 
				
			||||||
 | 
					import java.util.Comparator;
 | 
				
			||||||
 | 
					import java.util.LinkedList;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.Queue;
 | 
				
			||||||
 | 
					import java.util.concurrent.ConcurrentHashMap;
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
 | 
					import java.util.stream.Stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component
 | 
				
			||||||
 | 
					public class ServerRegistry {
 | 
				
			||||||
 | 
						private static final Logger log = LoggerFactory.getLogger(ServerRegistry.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static final Duration SERVER_TIMEOUT = Duration.ofMinutes(3);
 | 
				
			||||||
 | 
						public static final Duration SERVER_MIN_UPDATE = Duration.ofSeconds(5);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private final Map<ServerIdentifier, ServerInfo> servers = new ConcurrentHashMap<>();
 | 
				
			||||||
 | 
						private final ServerInfoValidator infoValidator = new ServerInfoValidator();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Flux<ServerInfoResponse> getServers() {
 | 
				
			||||||
 | 
							Stream<ServerInfoResponse> stream = servers.entrySet().stream()
 | 
				
			||||||
 | 
									.sorted(Comparator.comparing(entry -> entry.getValue().getLastUpdatedAt()))
 | 
				
			||||||
 | 
									.map(entry -> new ServerInfoResponse(
 | 
				
			||||||
 | 
											entry.getKey().host(),
 | 
				
			||||||
 | 
											entry.getKey().port(),
 | 
				
			||||||
 | 
											entry.getValue().getName(),
 | 
				
			||||||
 | 
											entry.getValue().getDescription(),
 | 
				
			||||||
 | 
											entry.getValue().getMaxPlayers(),
 | 
				
			||||||
 | 
											entry.getValue().getCurrentPlayers(),
 | 
				
			||||||
 | 
											entry.getValue().getLastUpdatedAt().toEpochMilli()
 | 
				
			||||||
 | 
									));
 | 
				
			||||||
 | 
							return Flux.fromStream(stream);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void acceptInfo(ServerIdentifier ident, ServerInfoPayload payload) {
 | 
				
			||||||
 | 
							var result = infoValidator.validatePayload(payload);
 | 
				
			||||||
 | 
							if (result.isPresent()) {
 | 
				
			||||||
 | 
								throw new ResponseStatusException(HttpStatus.BAD_REQUEST, String.join(" ", result.get()));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							ServerInfo info = servers.get(ident);
 | 
				
			||||||
 | 
							if (info != null) {
 | 
				
			||||||
 | 
								Instant now = Instant.now();
 | 
				
			||||||
 | 
								// Check if this update was sent too fast.
 | 
				
			||||||
 | 
								if (Duration.between(info.getLastUpdatedAt(), now).compareTo(SERVER_MIN_UPDATE) < 0) {
 | 
				
			||||||
 | 
									throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "Server update rate limit exceeded.");
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// Update existing server.
 | 
				
			||||||
 | 
								info.setName(payload.name());
 | 
				
			||||||
 | 
								info.setDescription(payload.description());
 | 
				
			||||||
 | 
								info.setMaxPlayers(payload.maxPlayers());
 | 
				
			||||||
 | 
								info.setCurrentPlayers(payload.currentPlayers());
 | 
				
			||||||
 | 
								info.setLastUpdatedAt(now);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// Save new server.
 | 
				
			||||||
 | 
								servers.put(ident, new ServerInfo(payload.name(), payload.description(), payload.maxPlayers(), payload.currentPlayers()));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Scheduled(fixedRate = 1, timeUnit = TimeUnit.MINUTES, initialDelay = 1)
 | 
				
			||||||
 | 
						public void purgeOldServers() {
 | 
				
			||||||
 | 
							Queue<ServerIdentifier> removalQueue = new LinkedList<>();
 | 
				
			||||||
 | 
							final Instant cutoff = Instant.now().minus(SERVER_TIMEOUT);
 | 
				
			||||||
 | 
							for (var entry : servers.entrySet()) {
 | 
				
			||||||
 | 
								var ident = entry.getKey();
 | 
				
			||||||
 | 
								var server = entry.getValue();
 | 
				
			||||||
 | 
								if (server.getLastUpdatedAt().isBefore(cutoff)) {
 | 
				
			||||||
 | 
									removalQueue.add(ident);
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							while (!removalQueue.isEmpty()) {
 | 
				
			||||||
 | 
								servers.remove(removalQueue.remove());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2registryapi.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.springframework.http.ResponseEntity;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.ExceptionHandler;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.RestControllerAdvice;
 | 
				
			||||||
 | 
					import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@RestControllerAdvice
 | 
				
			||||||
 | 
					public class ErrorAdvice {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@ExceptionHandler(ResponseStatusException.class)
 | 
				
			||||||
 | 
						public ResponseEntity<?> handleRSE(ResponseStatusException e) {
 | 
				
			||||||
 | 
							Map<String, Object> data = new HashMap<>();
 | 
				
			||||||
 | 
							data.put("code", e.getRawStatusCode());
 | 
				
			||||||
 | 
							data.put("message", e.getReason());
 | 
				
			||||||
 | 
							return ResponseEntity.status(e.getStatus()).body(data);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,36 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2registryapi.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.aos2registryapi.ServerRegistry;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2registryapi.dto.ServerInfoPayload;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2registryapi.dto.ServerInfoResponse;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2registryapi.model.ServerIdentifier;
 | 
				
			||||||
 | 
					import org.springframework.http.ResponseEntity;
 | 
				
			||||||
 | 
					import org.springframework.http.server.reactive.ServerHttpRequest;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.*;
 | 
				
			||||||
 | 
					import reactor.core.publisher.Flux;
 | 
				
			||||||
 | 
					import reactor.core.publisher.Mono;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@RestController
 | 
				
			||||||
 | 
					@RequestMapping(path = "/servers")
 | 
				
			||||||
 | 
					public class ServersController {
 | 
				
			||||||
 | 
						private final ServerRegistry serverRegistry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ServersController(ServerRegistry serverRegistry) {
 | 
				
			||||||
 | 
							this.serverRegistry = serverRegistry;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@GetMapping
 | 
				
			||||||
 | 
						public Flux<ServerInfoResponse> getServers() {
 | 
				
			||||||
 | 
							return serverRegistry.getServers();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@PostMapping
 | 
				
			||||||
 | 
						public Mono<ResponseEntity<Object>> updateServer(ServerHttpRequest req, @RequestBody Mono<ServerInfoPayload> payloadMono) {
 | 
				
			||||||
 | 
							String host = req.getRemoteAddress().getAddress().getHostAddress();
 | 
				
			||||||
 | 
							return payloadMono.mapNotNull(payload -> {
 | 
				
			||||||
 | 
								ServerIdentifier ident = new ServerIdentifier(host, payload.port());
 | 
				
			||||||
 | 
								serverRegistry.acceptInfo(ident, payload);
 | 
				
			||||||
 | 
								return ResponseEntity.ok(null);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2registryapi.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record ServerInfoPayload (
 | 
				
			||||||
 | 
							int port,
 | 
				
			||||||
 | 
							String name,
 | 
				
			||||||
 | 
							String description,
 | 
				
			||||||
 | 
							int maxPlayers,
 | 
				
			||||||
 | 
							int currentPlayers
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2registryapi.dto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record ServerInfoResponse (
 | 
				
			||||||
 | 
							String host,
 | 
				
			||||||
 | 
							int port,
 | 
				
			||||||
 | 
							String name,
 | 
				
			||||||
 | 
							String description,
 | 
				
			||||||
 | 
							int maxPlayers,
 | 
				
			||||||
 | 
							int currentPlayers,
 | 
				
			||||||
 | 
							long lastUpdatedAt
 | 
				
			||||||
 | 
					) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2registryapi.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record ServerIdentifier(String host, int port) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,59 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2registryapi.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.time.Instant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class ServerInfo {
 | 
				
			||||||
 | 
						private String name;
 | 
				
			||||||
 | 
						private String description;
 | 
				
			||||||
 | 
						private int maxPlayers;
 | 
				
			||||||
 | 
						private int currentPlayers;
 | 
				
			||||||
 | 
						private Instant lastUpdatedAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ServerInfo(String name, String description, int maxPlayers, int currentPlayers) {
 | 
				
			||||||
 | 
							this.name = name;
 | 
				
			||||||
 | 
							this.description = description;
 | 
				
			||||||
 | 
							this.maxPlayers = maxPlayers;
 | 
				
			||||||
 | 
							this.currentPlayers = currentPlayers;
 | 
				
			||||||
 | 
							this.lastUpdatedAt = Instant.now();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String getName() {
 | 
				
			||||||
 | 
							return name;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public String getDescription() {
 | 
				
			||||||
 | 
							return description;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public int getMaxPlayers() {
 | 
				
			||||||
 | 
							return maxPlayers;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public int getCurrentPlayers() {
 | 
				
			||||||
 | 
							return currentPlayers;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public Instant getLastUpdatedAt() {
 | 
				
			||||||
 | 
							return lastUpdatedAt;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void setName(String name) {
 | 
				
			||||||
 | 
							this.name = name;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void setDescription(String description) {
 | 
				
			||||||
 | 
							this.description = description;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void setMaxPlayers(int maxPlayers) {
 | 
				
			||||||
 | 
							this.maxPlayers = maxPlayers;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void setCurrentPlayers(int currentPlayers) {
 | 
				
			||||||
 | 
							this.currentPlayers = currentPlayers;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void setLastUpdatedAt(Instant lastUpdatedAt) {
 | 
				
			||||||
 | 
							this.lastUpdatedAt = lastUpdatedAt;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					spring.main.web-application-type=REACTIVE
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2registryapi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import org.junit.jupiter.api.Test;
 | 
				
			||||||
 | 
					import org.springframework.boot.test.context.SpringBootTest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@SpringBootTest
 | 
				
			||||||
 | 
					class Aos2RegistryApiApplicationTests {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@Test
 | 
				
			||||||
 | 
						void contextLoads() {
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@
 | 
				
			||||||
    <parent>
 | 
					    <parent>
 | 
				
			||||||
        <artifactId>ace-of-shades-2</artifactId>
 | 
					        <artifactId>ace-of-shades-2</artifactId>
 | 
				
			||||||
        <groupId>nl.andrewl</groupId>
 | 
					        <groupId>nl.andrewl</groupId>
 | 
				
			||||||
        <version>1.3.0</version>
 | 
					        <version>1.5.0</version>
 | 
				
			||||||
    </parent>
 | 
					    </parent>
 | 
				
			||||||
    <modelVersion>4.0.0</modelVersion>
 | 
					    <modelVersion>4.0.0</modelVersion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +33,12 @@
 | 
				
			||||||
            <artifactId>jansi</artifactId>
 | 
					            <artifactId>jansi</artifactId>
 | 
				
			||||||
            <version>2.4.0</version>
 | 
					            <version>2.4.0</version>
 | 
				
			||||||
        </dependency>
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>com.google.code.gson</groupId>
 | 
				
			||||||
 | 
					            <artifactId>gson</artifactId>
 | 
				
			||||||
 | 
					            <version>2.9.1</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
    </dependencies>
 | 
					    </dependencies>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <build>
 | 
					    <build>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -107,12 +107,16 @@ public class ClientCommunicationHandler {
 | 
				
			||||||
						socket.close();
 | 
											socket.close();
 | 
				
			||||||
						return;
 | 
											return;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
										if (server.getPlayerManager().getPlayers().size() >= server.getConfig().maxPlayers) {
 | 
				
			||||||
 | 
											Net.write(new ConnectRejectMessage("Server is full."), out);
 | 
				
			||||||
 | 
											socket.close();
 | 
				
			||||||
 | 
											return;
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					// Try to set the TCP timeout back to 0 now that we've got the correct request.
 | 
										// Try to set the TCP timeout back to 0 now that we've got the correct request.
 | 
				
			||||||
					socket.setSoTimeout(0);
 | 
										socket.setSoTimeout(0);
 | 
				
			||||||
					this.clientAddress = socket.getInetAddress();
 | 
										this.clientAddress = socket.getInetAddress();
 | 
				
			||||||
					connectionEstablished = true;
 | 
										this.player = server.getPlayerManager().register(this, connectMsg.username(), connectMsg.spectator());
 | 
				
			||||||
					this.player = server.getPlayerManager().register(this, connectMsg.username());
 | 
					 | 
				
			||||||
					Net.write(new ConnectAcceptMessage(player.getId()), out);
 | 
										Net.write(new ConnectAcceptMessage(player.getId()), out);
 | 
				
			||||||
					sendInitialData();
 | 
										sendInitialData();
 | 
				
			||||||
					sendTcpMessage(ChatMessage.privateMessage("Welcome to the server, " + player.getUsername() + "."));
 | 
										sendTcpMessage(ChatMessage.privateMessage("Welcome to the server, " + player.getUsername() + "."));
 | 
				
			||||||
| 
						 | 
					@ -123,6 +127,7 @@ public class ClientCommunicationHandler {
 | 
				
			||||||
					TcpReceiver tcpReceiver = new TcpReceiver(in, this::handleTcpMessage)
 | 
										TcpReceiver tcpReceiver = new TcpReceiver(in, this::handleTcpMessage)
 | 
				
			||||||
							.withShutdownHook(() -> server.getPlayerManager().deregister(this.player));
 | 
												.withShutdownHook(() -> server.getPlayerManager().deregister(this.player));
 | 
				
			||||||
					new Thread(tcpReceiver).start();
 | 
										new Thread(tcpReceiver).start();
 | 
				
			||||||
 | 
										connectionEstablished = true;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} catch (SocketTimeoutException e) {
 | 
								} catch (SocketTimeoutException e) {
 | 
				
			||||||
				// Ignore this one, since this will happen if the client doesn't send data properly.
 | 
									// Ignore this one, since this will happen if the client doesn't send data properly.
 | 
				
			||||||
| 
						 | 
					@ -231,7 +236,7 @@ public class ClientCommunicationHandler {
 | 
				
			||||||
			out.writeFloat(player.getOrientation().y());
 | 
								out.writeFloat(player.getOrientation().y());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			out.writeBoolean(player.isCrouching());
 | 
								out.writeBoolean(player.isCrouching());
 | 
				
			||||||
			out.writeInt(player.getInventory().getSelectedItemStack().getType().getId());
 | 
								out.writeInt(player.getInventory().getSelectedItemStack() == null ? -1 : player.getInventory().getSelectedItemStack().getType().getId());
 | 
				
			||||||
			out.writeByte(player.getInventory().getSelectedBlockValue());
 | 
								out.writeByte(player.getInventory().getSelectedBlockValue());
 | 
				
			||||||
			out.writeInt(player.getMode().ordinal());
 | 
								out.writeInt(player.getMode().ordinal());
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,22 +29,30 @@ public class PlayerManager {
 | 
				
			||||||
		this.server = server;
 | 
							this.server = server;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public synchronized ServerPlayer register(ClientCommunicationHandler handler, String username) {
 | 
						public synchronized ServerPlayer register(ClientCommunicationHandler handler, String username, boolean spectator) {
 | 
				
			||||||
		ServerPlayer player = new ServerPlayer(nextClientId++, username);
 | 
							PlayerMode mode = spectator ? PlayerMode.SPECTATOR : PlayerMode.NORMAL;
 | 
				
			||||||
 | 
							Team team = mode != PlayerMode.NORMAL ? null : findBestTeamForNewPlayer();
 | 
				
			||||||
 | 
							ServerPlayer player = new ServerPlayer(nextClientId++, username, team, mode);
 | 
				
			||||||
 | 
							if (player.getMode() == PlayerMode.NORMAL || player.getMode() == PlayerMode.CREATIVE) {
 | 
				
			||||||
 | 
								var inv = player.getInventory();
 | 
				
			||||||
 | 
								inv.getItemStacks().add(new GunItemStack(ItemTypes.RIFLE));
 | 
				
			||||||
 | 
								inv.getItemStacks().add(new GunItemStack(ItemTypes.AK_47));
 | 
				
			||||||
 | 
								inv.getItemStacks().add(new GunItemStack(ItemTypes.WINCHESTER));
 | 
				
			||||||
 | 
								inv.getItemStacks().add(new BlockItemStack(ItemTypes.BLOCK, 50, (byte) 1));
 | 
				
			||||||
 | 
								inv.setSelectedIndex(0);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		System.out.printf("Registered player \"%s\" with id %d.%n", player.getUsername(), player.getId());
 | 
							System.out.printf("Registered player \"%s\" with id %d.%n", player.getUsername(), player.getId());
 | 
				
			||||||
		players.put(player.getId(), player);
 | 
							players.put(player.getId(), player);
 | 
				
			||||||
		clientHandlers.put(player.getId(), handler);
 | 
							clientHandlers.put(player.getId(), handler);
 | 
				
			||||||
		String joinMessage;
 | 
							String joinMessage;
 | 
				
			||||||
		Team team = findBestTeamForNewPlayer();
 | 
							if (player.getTeam() != null) {
 | 
				
			||||||
		if (team != null) {
 | 
								System.out.printf("Player \"%s\" joined the \"%s\" team.%n", player.getUsername(), player.getTeam().getName());
 | 
				
			||||||
			player.setTeam(team);
 | 
								joinMessage = String.format("%s joined the %s team.", username, player.getTeam().getName());
 | 
				
			||||||
			System.out.printf("Player \"%s\" joined the \"%s\" team.%n", player.getUsername(), team.getName());
 | 
					 | 
				
			||||||
			joinMessage = String.format("%s joined the %s team.", username, team.getName());
 | 
					 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			joinMessage = username + " joined the game.";
 | 
								joinMessage = username + " joined the game.";
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		player.setPosition(getBestSpawnPoint(player));
 | 
							player.setPosition(getBestSpawnPoint(player));
 | 
				
			||||||
		setMode(player, PlayerMode.NORMAL);
 | 
					 | 
				
			||||||
		// Tell all other players that this one has joined.
 | 
							// Tell all other players that this one has joined.
 | 
				
			||||||
		broadcastTcpMessageToAllBut(new PlayerJoinMessage(
 | 
							broadcastTcpMessageToAllBut(new PlayerJoinMessage(
 | 
				
			||||||
				player.getId(), player.getUsername(), player.getTeam() == null ? -1 : player.getTeam().getId(),
 | 
									player.getId(), player.getUsername(), player.getTeam() == null ? -1 : player.getTeam().getId(),
 | 
				
			||||||
| 
						 | 
					@ -52,11 +60,13 @@ public class PlayerManager {
 | 
				
			||||||
				player.getVelocity().x(), player.getVelocity().y(), player.getVelocity().z(),
 | 
									player.getVelocity().x(), player.getVelocity().y(), player.getVelocity().z(),
 | 
				
			||||||
				player.getOrientation().x(), player.getOrientation().y(),
 | 
									player.getOrientation().x(), player.getOrientation().y(),
 | 
				
			||||||
				player.isCrouching(),
 | 
									player.isCrouching(),
 | 
				
			||||||
				player.getInventory().getSelectedItemStack().getType().getId(),
 | 
									player.getInventory().getSelectedItemStack() == null ? -1 : player.getInventory().getSelectedItemStack().getType().getId(),
 | 
				
			||||||
				player.getInventory().getSelectedBlockValue(),
 | 
									player.getInventory().getSelectedBlockValue(),
 | 
				
			||||||
				player.getMode()
 | 
									player.getMode()
 | 
				
			||||||
		), player);
 | 
							), player);
 | 
				
			||||||
		broadcastTcpMessageToAllBut(ChatMessage.announce(joinMessage), player);
 | 
							if (player.getMode() != PlayerMode.SPECTATOR) {
 | 
				
			||||||
 | 
								broadcastTcpMessageToAllBut(ChatMessage.announce(joinMessage), player);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return player;
 | 
							return player;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -163,12 +173,9 @@ public class PlayerManager {
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	public void playerKilled(ServerPlayer player, ServerPlayer killedBy) {
 | 
						public void playerKilled(ServerPlayer player, ServerPlayer killedBy) {
 | 
				
			||||||
		Vector3f deathPosition = new Vector3f(player.getPosition());
 | 
							Vector3f deathPosition = new Vector3f(player.getPosition());
 | 
				
			||||||
		player.setPosition(getBestSpawnPoint(player));
 | 
					 | 
				
			||||||
		player.setVelocity(new Vector3f(0));
 | 
					 | 
				
			||||||
		player.incrementDeathCount();
 | 
							player.incrementDeathCount();
 | 
				
			||||||
		resupply(player);
 | 
					 | 
				
			||||||
		broadcastUdpMessage(player.getUpdateMessage(System.currentTimeMillis()));
 | 
					 | 
				
			||||||
		broadcastUdpMessage(new SoundMessage("death", 1, deathPosition));
 | 
							broadcastUdpMessage(new SoundMessage("death", 1, deathPosition));
 | 
				
			||||||
 | 
							respawn(player);
 | 
				
			||||||
		String deathMessage;
 | 
							String deathMessage;
 | 
				
			||||||
		if (killedBy != null) {
 | 
							if (killedBy != null) {
 | 
				
			||||||
			killedBy.incrementKillCount();
 | 
								killedBy.incrementKillCount();
 | 
				
			||||||
| 
						 | 
					@ -198,8 +205,16 @@ public class PlayerManager {
 | 
				
			||||||
		handler.sendTcpMessage(ChatMessage.privateMessage("You've been resupplied at your team base."));
 | 
							handler.sendTcpMessage(ChatMessage.privateMessage("You've been resupplied at your team base."));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void respawn(ServerPlayer player) {
 | 
				
			||||||
 | 
							player.setPosition(getBestSpawnPoint(player));
 | 
				
			||||||
 | 
							player.setVelocity(new Vector3f(0));
 | 
				
			||||||
 | 
							broadcastUdpMessage(player.getUpdateMessage(System.currentTimeMillis()));
 | 
				
			||||||
 | 
							resupply(player);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public void setMode(ServerPlayer player, PlayerMode mode) {
 | 
						public void setMode(ServerPlayer player, PlayerMode mode) {
 | 
				
			||||||
		player.setMode(mode);
 | 
							player.setMode(mode);
 | 
				
			||||||
 | 
							var handler = getHandler(player);
 | 
				
			||||||
		var inv = player.getInventory();
 | 
							var inv = player.getInventory();
 | 
				
			||||||
		inv.clear();
 | 
							inv.clear();
 | 
				
			||||||
		if (mode == PlayerMode.NORMAL || mode == PlayerMode.CREATIVE) {
 | 
							if (mode == PlayerMode.NORMAL || mode == PlayerMode.CREATIVE) {
 | 
				
			||||||
| 
						 | 
					@ -208,7 +223,31 @@ public class PlayerManager {
 | 
				
			||||||
			inv.getItemStacks().add(new GunItemStack(ItemTypes.WINCHESTER));
 | 
								inv.getItemStacks().add(new GunItemStack(ItemTypes.WINCHESTER));
 | 
				
			||||||
			inv.getItemStacks().add(new BlockItemStack(ItemTypes.BLOCK, 50, (byte) 1));
 | 
								inv.getItemStacks().add(new BlockItemStack(ItemTypes.BLOCK, 50, (byte) 1));
 | 
				
			||||||
			inv.setSelectedIndex(0);
 | 
								inv.setSelectedIndex(0);
 | 
				
			||||||
 | 
								handler.sendTcpMessage(new ClientInventoryMessage(inv));
 | 
				
			||||||
 | 
								broadcastUdpMessage(player.getUpdateMessage(System.currentTimeMillis()));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if (mode != PlayerMode.NORMAL) {
 | 
				
			||||||
 | 
								player.setTeam(null);
 | 
				
			||||||
 | 
								broadcastTcpMessage(new PlayerTeamUpdateMessage(player.getId(), -1));
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								player.setTeam(findBestTeamForNewPlayer());
 | 
				
			||||||
 | 
								broadcastTcpMessage(new PlayerTeamUpdateMessage(player.getId(), player.getTeam() == null ? -1 : player.getTeam().getId()));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							handler.sendTcpMessage(ChatMessage.privateMessage("Your mode has been updated to " + mode.name() + "."));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void setTeam(ServerPlayer player, Team team) {
 | 
				
			||||||
 | 
							if (Objects.equals(team, player.getTeam()) || player.getMode() != PlayerMode.NORMAL) return;
 | 
				
			||||||
 | 
							player.setTeam(team);
 | 
				
			||||||
 | 
							broadcastUdpMessage(new PlayerTeamUpdateMessage(player.getId(), team == null ? -1 : team.getId()));
 | 
				
			||||||
 | 
							respawn(player);
 | 
				
			||||||
 | 
							String chatMessage;
 | 
				
			||||||
 | 
							if (team != null) {
 | 
				
			||||||
 | 
								chatMessage = "%s has changed to the %s team.".formatted(player.getUsername(), team.getName());
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								chatMessage = "%s has changed to not be on a team.".formatted(player.getUsername());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							broadcastTcpMessage(ChatMessage.announce(chatMessage));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public void handleUdpInit(DatagramInit init, DatagramPacket packet) {
 | 
						public void handleUdpInit(DatagramInit init, DatagramPacket packet) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,59 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.google.gson.Gson;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.net.URI;
 | 
				
			||||||
 | 
					import java.net.http.HttpClient;
 | 
				
			||||||
 | 
					import java.net.http.HttpRequest;
 | 
				
			||||||
 | 
					import java.net.http.HttpResponse;
 | 
				
			||||||
 | 
					import java.time.Duration;
 | 
				
			||||||
 | 
					import java.util.HashMap;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Component that sends regular updates to any configured server registries.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class RegistryUpdater {
 | 
				
			||||||
 | 
						private final Server server;
 | 
				
			||||||
 | 
						private final HttpClient httpClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public RegistryUpdater(Server server) {
 | 
				
			||||||
 | 
							this.server = server;
 | 
				
			||||||
 | 
							this.httpClient = HttpClient.newBuilder()
 | 
				
			||||||
 | 
									.connectTimeout(Duration.ofSeconds(3))
 | 
				
			||||||
 | 
									.build();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public void sendUpdates() {
 | 
				
			||||||
 | 
							var cfg = server.getConfig();
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
									cfg.registries != null &&
 | 
				
			||||||
 | 
									cfg.registries.length > 0 &&
 | 
				
			||||||
 | 
									cfg.name != null && !cfg.name.isBlank()
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								Map<String, Object> data = new HashMap<>();
 | 
				
			||||||
 | 
								data.put("port", cfg.port);
 | 
				
			||||||
 | 
								data.put("name", cfg.name);
 | 
				
			||||||
 | 
								data.put("description", cfg.description);
 | 
				
			||||||
 | 
								data.put("maxPlayers", cfg.maxPlayers);
 | 
				
			||||||
 | 
								data.put("currentPlayers", server.getPlayerManager().getPlayers().size());
 | 
				
			||||||
 | 
								String json = new Gson().toJson(data);
 | 
				
			||||||
 | 
								for (String registryUrl : server.getConfig().registries) {
 | 
				
			||||||
 | 
									HttpRequest req = HttpRequest.newBuilder(URI.create(registryUrl + "/servers"))
 | 
				
			||||||
 | 
											.POST(HttpRequest.BodyPublishers.ofString(json))
 | 
				
			||||||
 | 
											.header("Content-Type", "application/json")
 | 
				
			||||||
 | 
											.timeout(Duration.ofSeconds(3))
 | 
				
			||||||
 | 
											.build();
 | 
				
			||||||
 | 
									try {
 | 
				
			||||||
 | 
										var resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
 | 
				
			||||||
 | 
										if (resp.statusCode() != 200) {
 | 
				
			||||||
 | 
											System.err.println("Error response when sending registry update to " + registryUrl + ": " + resp.statusCode() + " " + resp.body());
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									} catch (IOException | InterruptedException e) {
 | 
				
			||||||
 | 
										System.err.println("An error occurred while sending registry update to " + registryUrl + ": " + e.getMessage());
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,10 @@ import java.net.*;
 | 
				
			||||||
import java.nio.file.Files;
 | 
					import java.nio.file.Files;
 | 
				
			||||||
import java.nio.file.Path;
 | 
					import java.nio.file.Path;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.concurrent.Executors;
 | 
				
			||||||
import java.util.concurrent.ForkJoinPool;
 | 
					import java.util.concurrent.ForkJoinPool;
 | 
				
			||||||
 | 
					import java.util.concurrent.ScheduledExecutorService;
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The central server, which mainly contains all the different managers and
 | 
					 * The central server, which mainly contains all the different managers and
 | 
				
			||||||
| 
						 | 
					@ -83,11 +86,18 @@ public class Server implements Runnable {
 | 
				
			||||||
		running = true;
 | 
							running = true;
 | 
				
			||||||
		new Thread(new UdpReceiver(datagramSocket, this::handleUdpMessage)).start();
 | 
							new Thread(new UdpReceiver(datagramSocket, this::handleUdpMessage)).start();
 | 
				
			||||||
		new Thread(worldUpdater).start();
 | 
							new Thread(worldUpdater).start();
 | 
				
			||||||
 | 
							ScheduledExecutorService executorService = null;
 | 
				
			||||||
 | 
							if (config.registries != null && config.registries.length > 0) {
 | 
				
			||||||
 | 
								executorService = Executors.newSingleThreadScheduledExecutor();
 | 
				
			||||||
 | 
								var registryUpdater = new RegistryUpdater(this);
 | 
				
			||||||
 | 
								executorService.scheduleAtFixedRate(registryUpdater::sendUpdates, 0, 30, TimeUnit.SECONDS);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		System.out.printf("Started AoS2 Server on TCP/UDP port %d; now accepting connections.%n", serverSocket.getLocalPort());
 | 
							System.out.printf("Started AoS2 Server on TCP/UDP port %d; now accepting connections.%n", serverSocket.getLocalPort());
 | 
				
			||||||
		while (running) {
 | 
							while (running) {
 | 
				
			||||||
			acceptClientConnection();
 | 
								acceptClientConnection();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		System.out.println("Shutting down the server.");
 | 
							System.out.println("Shutting down the server.");
 | 
				
			||||||
 | 
							if (executorService != null) executorService.shutdown();
 | 
				
			||||||
		playerManager.deregisterAll();
 | 
							playerManager.deregisterAll();
 | 
				
			||||||
		worldUpdater.shutdown();
 | 
							worldUpdater.shutdown();
 | 
				
			||||||
		datagramSocket.close(); // Shuts down the UdpReceiver.
 | 
							datagramSocket.close(); // Shuts down the UdpReceiver.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +33,9 @@ public class TeamManager {
 | 
				
			||||||
		for (var team : teams.values()) {
 | 
							for (var team : teams.values()) {
 | 
				
			||||||
			if (team.getName().equals(ident)) return Optional.of(team);
 | 
								if (team.getName().equals(ident)) return Optional.of(team);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							for (var team : teams.values()) {// Try again ignoring case.
 | 
				
			||||||
 | 
								if (team.getName().equalsIgnoreCase(ident)) return Optional.of(team);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			int id = Integer.parseInt(ident);
 | 
								int id = Integer.parseInt(ident);
 | 
				
			||||||
			for (var team : teams.values()) {
 | 
								for (var team : teams.values()) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ import nl.andrewl.aos2_server.Server;
 | 
				
			||||||
import nl.andrewl.aos2_server.cli.ingame.commands.KillCommand;
 | 
					import nl.andrewl.aos2_server.cli.ingame.commands.KillCommand;
 | 
				
			||||||
import nl.andrewl.aos2_server.cli.ingame.commands.KillDeathRatioCommand;
 | 
					import nl.andrewl.aos2_server.cli.ingame.commands.KillDeathRatioCommand;
 | 
				
			||||||
import nl.andrewl.aos2_server.cli.ingame.commands.PlayerModeCommand;
 | 
					import nl.andrewl.aos2_server.cli.ingame.commands.PlayerModeCommand;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_server.cli.ingame.commands.TeamsCommand;
 | 
				
			||||||
import nl.andrewl.aos2_server.model.ServerPlayer;
 | 
					import nl.andrewl.aos2_server.model.ServerPlayer;
 | 
				
			||||||
import nl.andrewl.aos_core.net.client.ChatMessage;
 | 
					import nl.andrewl.aos_core.net.client.ChatMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,6 +28,7 @@ public class PlayerCommandHandler {
 | 
				
			||||||
		commands.put("kd", new KillDeathRatioCommand());
 | 
							commands.put("kd", new KillDeathRatioCommand());
 | 
				
			||||||
		commands.put("kill", new KillCommand());
 | 
							commands.put("kill", new KillCommand());
 | 
				
			||||||
		commands.put("mode", new PlayerModeCommand());
 | 
							commands.put("mode", new PlayerModeCommand());
 | 
				
			||||||
 | 
							commands.put("teams", new TeamsCommand());
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public void handle(String rawCommand, ServerPlayer player, ClientCommunicationHandler handler) {
 | 
						public void handle(String rawCommand, ServerPlayer player, ClientCommunicationHandler handler) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,6 @@ import nl.andrewl.aos2_server.cli.ingame.PlayerCommand;
 | 
				
			||||||
import nl.andrewl.aos2_server.model.ServerPlayer;
 | 
					import nl.andrewl.aos2_server.model.ServerPlayer;
 | 
				
			||||||
import nl.andrewl.aos_core.model.PlayerMode;
 | 
					import nl.andrewl.aos_core.model.PlayerMode;
 | 
				
			||||||
import nl.andrewl.aos_core.net.client.ChatMessage;
 | 
					import nl.andrewl.aos_core.net.client.ChatMessage;
 | 
				
			||||||
import nl.andrewl.aos_core.net.client.ClientInventoryMessage;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class PlayerModeCommand implements PlayerCommand {
 | 
					public class PlayerModeCommand implements PlayerCommand {
 | 
				
			||||||
	@Override
 | 
						@Override
 | 
				
			||||||
| 
						 | 
					@ -19,9 +18,6 @@ public class PlayerModeCommand implements PlayerCommand {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			PlayerMode mode = PlayerMode.valueOf(modeText);
 | 
								PlayerMode mode = PlayerMode.valueOf(modeText);
 | 
				
			||||||
			server.getPlayerManager().setMode(player, mode);
 | 
								server.getPlayerManager().setMode(player, mode);
 | 
				
			||||||
			handler.sendTcpMessage(new ClientInventoryMessage(player.getInventory()));
 | 
					 | 
				
			||||||
			server.getPlayerManager().broadcastUdpMessage(player.getUpdateMessage(System.currentTimeMillis()));
 | 
					 | 
				
			||||||
			handler.sendTcpMessage(ChatMessage.privateMessage("Your mode has been updated to " + mode.name() + "."));
 | 
					 | 
				
			||||||
		} catch (IllegalArgumentException e) {
 | 
							} catch (IllegalArgumentException e) {
 | 
				
			||||||
			handler.sendTcpMessage(ChatMessage.privateMessage("Invalid mode. Should be NORMAL, CREATIVE, or SPECTATOR."));
 | 
								handler.sendTcpMessage(ChatMessage.privateMessage("Invalid mode. Should be NORMAL, CREATIVE, or SPECTATOR."));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,37 @@
 | 
				
			||||||
 | 
					package nl.andrewl.aos2_server.cli.ingame.commands;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_server.ClientCommunicationHandler;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_server.Server;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_server.cli.ingame.PlayerCommand;
 | 
				
			||||||
 | 
					import nl.andrewl.aos2_server.model.ServerPlayer;
 | 
				
			||||||
 | 
					import nl.andrewl.aos_core.model.Team;
 | 
				
			||||||
 | 
					import nl.andrewl.aos_core.net.client.ChatMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.stream.Collectors;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class TeamsCommand implements PlayerCommand {
 | 
				
			||||||
 | 
						@Override
 | 
				
			||||||
 | 
						public void handle(String[] args, ServerPlayer player, ClientCommunicationHandler handler, Server server) {
 | 
				
			||||||
 | 
							if (args.length == 0) {
 | 
				
			||||||
 | 
								String teamsString = server.getTeamManager().getTeams().stream()
 | 
				
			||||||
 | 
										.map(Team::getName).collect(Collectors.joining(", "));
 | 
				
			||||||
 | 
								handler.sendTcpMessage(ChatMessage.privateMessage(teamsString));
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								String cmd = args[0].trim().toLowerCase();
 | 
				
			||||||
 | 
								if (cmd.equals("set")) {
 | 
				
			||||||
 | 
									if (args.length >= 2) {
 | 
				
			||||||
 | 
										String teamIdent = args[1].trim();
 | 
				
			||||||
 | 
										server.getTeamManager().findByIdOrName(teamIdent)
 | 
				
			||||||
 | 
												.ifPresentOrElse(
 | 
				
			||||||
 | 
														team -> server.getPlayerManager().setTeam(player, team),
 | 
				
			||||||
 | 
														() -> handler.sendTcpMessage(ChatMessage.privateMessage("Unknown team."))
 | 
				
			||||||
 | 
												);
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										handler.sendTcpMessage(ChatMessage.privateMessage("Missing required team identifier."));
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									handler.sendTcpMessage(ChatMessage.privateMessage("Unknown subcommand."));
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,11 @@ package nl.andrewl.aos2_server.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class ServerConfig {
 | 
					public class ServerConfig {
 | 
				
			||||||
	public int port = 25565;
 | 
						public int port = 25565;
 | 
				
			||||||
 | 
						public String name = "My Server";
 | 
				
			||||||
 | 
						public String description = "My server";
 | 
				
			||||||
 | 
						public String[] registries = new String[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public int maxPlayers = 32;
 | 
				
			||||||
	public int connectionBacklog = 5;
 | 
						public int connectionBacklog = 5;
 | 
				
			||||||
	public float ticksPerSecond = 20.0f;
 | 
						public float ticksPerSecond = 20.0f;
 | 
				
			||||||
	public String world = "worlds.redfort";
 | 
						public String world = "worlds.redfort";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,8 @@ package nl.andrewl.aos2_server.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import nl.andrewl.aos2_server.logic.PlayerActionManager;
 | 
					import nl.andrewl.aos2_server.logic.PlayerActionManager;
 | 
				
			||||||
import nl.andrewl.aos_core.model.Player;
 | 
					import nl.andrewl.aos_core.model.Player;
 | 
				
			||||||
 | 
					import nl.andrewl.aos_core.model.PlayerMode;
 | 
				
			||||||
 | 
					import nl.andrewl.aos_core.model.Team;
 | 
				
			||||||
import nl.andrewl.aos_core.model.item.Inventory;
 | 
					import nl.andrewl.aos_core.model.item.Inventory;
 | 
				
			||||||
import nl.andrewl.aos_core.net.client.PlayerUpdateMessage;
 | 
					import nl.andrewl.aos_core.net.client.PlayerUpdateMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,8 +26,8 @@ public class ServerPlayer extends Player {
 | 
				
			||||||
	private int deathCount;
 | 
						private int deathCount;
 | 
				
			||||||
	private int killCount;
 | 
						private int killCount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public ServerPlayer(int id, String username) {
 | 
						public ServerPlayer(int id, String username, Team team, PlayerMode mode) {
 | 
				
			||||||
		super(id, username);
 | 
							super(id, username, team, mode);
 | 
				
			||||||
		this.inventory = new Inventory(new ArrayList<>(), 0);
 | 
							this.inventory = new Inventory(new ArrayList<>(), 0);
 | 
				
			||||||
		this.health = 1f;
 | 
							this.health = 1f;
 | 
				
			||||||
		this.actionManager = new PlayerActionManager(this);
 | 
							this.actionManager = new PlayerActionManager(this);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,10 @@
 | 
				
			||||||
# Ace of Shades 2 Server Configuration
 | 
					# Ace of Shades 2 Server Configuration
 | 
				
			||||||
port: 25565
 | 
					port: 25565
 | 
				
			||||||
 | 
					name: My Server
 | 
				
			||||||
 | 
					description: This is my Ace of Shades server.
 | 
				
			||||||
 | 
					registries:
 | 
				
			||||||
 | 
					  - https://reg.aos2.net
 | 
				
			||||||
 | 
					maxPlayers: 32
 | 
				
			||||||
connectionBacklog: 5
 | 
					connectionBacklog: 5
 | 
				
			||||||
ticksPerSecond: 20.0
 | 
					ticksPerSecond: 20.0
 | 
				
			||||||
world: worlds.redfort
 | 
					world: worlds.redfort
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue