Compare commits

..

No commits in common. "main" and "v1.1.0" have entirely different histories.
main ... v1.1.0

127 changed files with 962 additions and 5010 deletions

6
.gitignore vendored
View File

@ -1,13 +1,7 @@
.idea/ .idea/
target/ target/
client-builds/ client-builds/
client.yaml
server.yaml
# Ignore the ./config directory so that developers can put their config files # Ignore the ./config directory so that developers can put their config files
# there for server and client apps. # there for server and client apps.
config config
# Ignore compiled tool executables
build-clients
setversion

View File

@ -8,22 +8,56 @@ _Read this guide to get started and join a server in just a minute or two!_
1. Make sure you've got at least Java 17 installed. You can get it [here](https://adoptium.net/temurin/releases). 1. Make sure you've got at least Java 17 installed. You can get it [here](https://adoptium.net/temurin/releases).
2. Download the `aos2-client` JAR file from the [releases page](https://github.com/andrewlalis/ace-of-shades-2/releases) that's compatible with your system. 2. Download the `aos2-client` JAR file from the [releases page](https://github.com/andrewlalis/ace-of-shades-2/releases) that's compatible with your system.
3. Run the game by double-clicking the JAR file, or entering `java -jar <jarfile>` in a terminal. This should generate a `client.yaml` file. 3. Create a file named `config.yaml` in the same directory as the JAR file, and place the following text in it:
```yaml
serverHost: localhost
serverPort: 25565
username: myUsername
input:
mouseSensitivity: 0.005
display:
fullscreen: true
captureCursor: true
fov: 80
```
4. Set the `serverHost`, `serverPort`, and `username` properties accordingly for the server you want to join. 4. Set the `serverHost`, `serverPort`, and `username` properties accordingly for the server you want to join.
5. Run the game again to join the server and start playing! 5. Run the game by double-clicking the `aos2-client` JAR file, or enter `java -jar aos2-client-{version}.jar` in a terminal.
### Controls
- `WASD` to move, `SPACE` to jump, `LEFT-CONTROL` to crouch.
- `LEFT-CLICK` to use your held item (shoot, destroy blocks, etc.).
- `RIGHT-CLICK` to interact with things using your held item (place blocks, etc.).
- `R` to reload.
- `T` to chat. Press `ENTER` to send the chat.
- `ESCAPE` to exit the game.
- Numbers are used for selecting different items.
- `F3` toggles showing debug info.
## Setting up a Server ## Setting up a Server
Setting up a server is quite easy. Just go to the [releases page](https://github.com/andrewlalis/ace-of-shades-2/releases) and download the latest `aos2-server` JAR file, and run it. It'll create a `server.yaml` configuration file if you don't provide one. Setting up a server is quite easy. Just go to the [releases page](https://github.com/andrewlalis/ace-of-shades-2/releases) and download the latest `aos2-server` JAR file. Similar to the client, it's best if you provide a `config.yaml` file to the server, in the same directory. The following snippet shows the structure and default values of a server's configuration.
```yaml
port: 25565
connectionBacklog: 5
ticksPerSecond: 20.0
world: worlds.redfort
teams:
- name: Red
color: [0.8, 0, 0]
spawnPoint: A
- name: Blue
color: [0, 0, 0.8]
spawnPoint: B
physics:
gravity: 29.43
walkingSpeed: 4
crouchingSpeed: 1.5
sprintingSpeed: 9
movementAcceleration: 2
movementDeceleration: 1
jumpVerticalSpeed: 8
actions:
blockBreakCooldown: 0.25
blockPlaceCooldown: 0.1
blockBreakReach: 5
blockPlaceReach: 5
blockBulletDamageResistance: 3
blockBulletDamageCooldown: 10
resupplyCooldown: 30
resupplyRadius: 3
teamSpawnProtection: 10
movementAccuracyDecreaseFactor: 0.01
friendlyFire: false
```
## Configuration ## Configuration
Both the client and server use a similar style of YAML-based configuration, where upon booting up, the program will look for a configuration file in the current working directory with one of the following names: `configuration`, `config`, `cfg`, ending in either `.yaml` or `.yml`. Alternatively, you can provide the path to a configuration file at a different location via a single command-line argument. For example: Both the client and server use a similar style of YAML-based configuration, where upon booting up, the program will look for a configuration file in the current working directory with one of the following names: `configuration`, `config`, `cfg`, ending in either `.yaml` or `.yml`. Alternatively, you can provide the path to a configuration file at a different location via a single command-line argument. For example:

View File

@ -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.5.0</version> <version>1.1.0</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>

View File

@ -52,14 +52,6 @@ public class Camera {
velocity.set(p.getVelocity()); velocity.set(p.getVelocity());
} }
public void setToPlayerScopeView(Player p) {
Vector3f pos = new Vector3f();
Matrix4f tx = p.getHeldItemTransform();
tx.transformPosition(pos);
position.set(pos);
velocity.set(p.getVelocity());
}
public void setOrientationToPlayer(Player p) { public void setOrientationToPlayer(Player p) {
orientation.set(p.getOrientation()); orientation.set(p.getOrientation());
} }

View File

@ -1,14 +1,14 @@
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.*;
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;
import nl.andrewl.aos2_client.model.OtherPlayer; import nl.andrewl.aos2_client.model.OtherPlayer;
import nl.andrewl.aos2_client.render.GameRenderer; import nl.andrewl.aos2_client.render.GameRenderer;
import nl.andrewl.aos2_client.sound.SoundManager; import nl.andrewl.aos2_client.sound.SoundManager;
import nl.andrewl.aos_core.config.Config; import nl.andrewl.aos_core.config.Config;
import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.Projectile; import nl.andrewl.aos_core.model.Projectile;
import nl.andrewl.aos_core.model.Team; import nl.andrewl.aos_core.model.Team;
import nl.andrewl.aos_core.net.client.*; import nl.andrewl.aos_core.net.client.*;
@ -17,22 +17,22 @@ import nl.andrewl.aos_core.net.world.ChunkHashMessage;
import nl.andrewl.aos_core.net.world.ChunkUpdateMessage; import nl.andrewl.aos_core.net.world.ChunkUpdateMessage;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class Client implements Runnable { public class Client implements Runnable {
public final ConnectConfig connectConfig; private static final Logger log = LoggerFactory.getLogger(Client.class);
public final ClientConfig config; public static final double FPS = 60;
private final ClientConfig config;
private final CommunicationHandler communicationHandler; private final CommunicationHandler communicationHandler;
private final InputHandler inputHandler; private final InputHandler inputHandler;
private final Camera camera;
private GameRenderer gameRenderer; private GameRenderer gameRenderer;
private SoundManager soundManager; private SoundManager soundManager;
private long lastPlayerUpdate = 0; private long lastPlayerUpdate = 0;
@ -43,19 +43,15 @@ public class Client implements Runnable {
private final Map<Integer, Projectile> projectiles; private final Map<Integer, Projectile> projectiles;
private final Map<Integer, Team> teams; private final Map<Integer, Team> teams;
private final Chat chat; private final Chat chat;
private final Queue<Runnable> mainThreadActions;
public Client(ClientConfig config, ConnectConfig connectConfig) { public Client(ClientConfig config) {
this.config = config; this.config = config;
this.connectConfig = connectConfig;
this.camera = new Camera();
this.players = new ConcurrentHashMap<>(); this.players = new ConcurrentHashMap<>();
this.teams = new ConcurrentHashMap<>(); this.teams = new ConcurrentHashMap<>();
this.projectiles = new ConcurrentHashMap<>(); this.projectiles = new ConcurrentHashMap<>();
this.communicationHandler = new CommunicationHandler(this); this.communicationHandler = new CommunicationHandler(this);
this.inputHandler = new InputHandler(this, communicationHandler, camera); this.inputHandler = new InputHandler(this, communicationHandler);
this.chat = new Chat(); this.chat = new Chat();
this.mainThreadActions = new ConcurrentLinkedQueue<>();
} }
public ClientConfig getConfig() { public ClientConfig getConfig() {
@ -80,28 +76,30 @@ public class Client implements Runnable {
try { try {
communicationHandler.establishConnection(); communicationHandler.establishConnection();
} catch (IOException e) { } catch (IOException e) {
System.err.println("Couldn't connect to the server: " + e.getMessage()); log.error("Couldn't connect to the server: {}", e.getMessage());
return; return;
} }
gameRenderer = new GameRenderer(this, inputHandler, camera); gameRenderer = new GameRenderer(config.display, this);
gameRenderer.setupWindow(
new PlayerViewCursorCallback(config.input, this, gameRenderer.getCamera(), communicationHandler),
new PlayerInputKeyCallback(inputHandler),
new PlayerInputMouseClickCallback(inputHandler),
new PlayerInputMouseScrollCallback(this, communicationHandler)
);
soundManager = new SoundManager(); soundManager = new SoundManager();
log.debug("Sound system initialized.");
long lastFrameAt = System.currentTimeMillis(); long lastFrameAt = System.currentTimeMillis();
while (!gameRenderer.windowShouldClose() && !communicationHandler.isDone()) { while (!gameRenderer.windowShouldClose() && !communicationHandler.isDone()) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
float dt = (now - lastFrameAt) / 1000f; float dt = (now - lastFrameAt) / 1000f;
world.processQueuedChunkUpdates(); world.processQueuedChunkUpdates();
while (!mainThreadActions.isEmpty()) {
mainThreadActions.remove().run();
}
soundManager.updateListener(myPlayer.getPosition(), myPlayer.getVelocity()); soundManager.updateListener(myPlayer.getPosition(), myPlayer.getVelocity());
gameRenderer.getCamera().interpolatePosition(dt); gameRenderer.getCamera().interpolatePosition(dt);
interpolatePlayers(now, dt); interpolatePlayers(now, dt);
interpolateProjectiles(dt); interpolateProjectiles(dt);
soundManager.playWalkingSounds(myPlayer, world, now); soundManager.playWalkingSounds(myPlayer, now);
gameRenderer.draw(); gameRenderer.draw();
lastFrameAt = now; lastFrameAt = now;
} }
@ -120,17 +118,15 @@ public class Client implements Runnable {
communicationHandler.sendMessage(new ChunkHashMessage(u.cx(), u.cy(), u.cz(), -1)); communicationHandler.sendMessage(new ChunkHashMessage(u.cx(), u.cy(), u.cz(), -1));
} }
} else if (msg instanceof PlayerUpdateMessage playerUpdate) { } else if (msg instanceof PlayerUpdateMessage playerUpdate) {
runLater(() -> {
if (playerUpdate.clientId() == myPlayer.getId() && playerUpdate.timestamp() > lastPlayerUpdate) { if (playerUpdate.clientId() == myPlayer.getId() && playerUpdate.timestamp() > lastPlayerUpdate) {
myPlayer.getPosition().set(playerUpdate.px(), playerUpdate.py(), playerUpdate.pz()); myPlayer.getPosition().set(playerUpdate.px(), playerUpdate.py(), playerUpdate.pz());
myPlayer.getVelocity().set(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz()); myPlayer.getVelocity().set(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz());
myPlayer.setCrouching(playerUpdate.crouching()); myPlayer.setCrouching(playerUpdate.crouching());
myPlayer.setMode(playerUpdate.mode());
if (gameRenderer != null) { if (gameRenderer != null) {
gameRenderer.getCamera().setToPlayer(myPlayer); gameRenderer.getCamera().setToPlayer(myPlayer);
} }
if (soundManager != null) { if (soundManager != null) {
soundManager.updateListener(myPlayer.getEyePosition(), myPlayer.getVelocity()); soundManager.updateListener(myPlayer.getPosition(), myPlayer.getVelocity());
} }
lastPlayerUpdate = playerUpdate.timestamp(); lastPlayerUpdate = playerUpdate.timestamp();
} else { } else {
@ -141,9 +137,8 @@ public class Client implements Runnable {
p.updateModelTransform(); p.updateModelTransform();
} }
} }
});
} else if (msg instanceof ClientInventoryMessage inventoryMessage) { } else if (msg instanceof ClientInventoryMessage inventoryMessage) {
runLater(() -> myPlayer.setInventory(inventoryMessage.inv())); myPlayer.setInventory(inventoryMessage.inv());
} else if (msg instanceof InventorySelectedStackMessage selectedStackMessage) { } else if (msg instanceof InventorySelectedStackMessage selectedStackMessage) {
myPlayer.getInventory().setSelectedIndex(selectedStackMessage.index()); myPlayer.getInventory().setSelectedIndex(selectedStackMessage.index());
} else if (msg instanceof ItemStackMessage itemStackMessage) { } else if (msg instanceof ItemStackMessage itemStackMessage) {
@ -154,20 +149,19 @@ public class Client implements Runnable {
player.setSelectedBlockValue(blockColorMessage.block()); player.setSelectedBlockValue(blockColorMessage.block());
} }
} else if (msg instanceof PlayerJoinMessage joinMessage) { } else if (msg instanceof PlayerJoinMessage joinMessage) {
runLater(() -> { Player p = joinMessage.toPlayer();
OtherPlayer op = OtherPlayer.fromJoinMessage(joinMessage, this); OtherPlayer op = new OtherPlayer(p.getId(), p.getUsername());
players.put(op.getId(), op); if (joinMessage.teamId() != -1) {
}); op.setTeam(teams.get(joinMessage.teamId()));
} else if (msg instanceof PlayerLeaveMessage leaveMessage) {
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);
} }
}); op.getPosition().set(p.getPosition());
op.getVelocity().set(p.getVelocity());
op.getOrientation().set(p.getOrientation());
op.setHeldItemId(joinMessage.selectedItemId());
op.setSelectedBlockValue(joinMessage.selectedBlockValue());
players.put(op.getId(), op);
} else if (msg instanceof PlayerLeaveMessage leaveMessage) {
players.remove(leaveMessage.id());
} else if (msg instanceof SoundMessage soundMessage) { } else if (msg instanceof SoundMessage soundMessage) {
if (soundManager != null) { if (soundManager != null) {
soundManager.play( soundManager.play(
@ -178,7 +172,6 @@ public class Client implements Runnable {
); );
} }
} else if (msg instanceof ProjectileMessage pm) { } else if (msg instanceof ProjectileMessage pm) {
runLater(() -> {
Projectile p = projectiles.get(pm.id()); Projectile p = projectiles.get(pm.id());
if (p == null && !pm.destroyed()) { if (p == null && !pm.destroyed()) {
p = new Projectile(pm.id(), new Vector3f(pm.px(), pm.py(), pm.pz()), new Vector3f(pm.vx(), pm.vy(), pm.vz()), pm.type()); p = new Projectile(pm.id(), new Vector3f(pm.px(), pm.py(), pm.pz()), new Vector3f(pm.vx(), pm.vy(), pm.vz()), pm.type());
@ -190,22 +183,13 @@ public class Client implements Runnable {
projectiles.remove(p.getId()); projectiles.remove(p.getId());
} }
} }
});
} else if (msg instanceof ClientHealthMessage healthMessage) { } else if (msg instanceof ClientHealthMessage healthMessage) {
myPlayer.setHealth(healthMessage.health()); myPlayer.setHealth(healthMessage.health());
} else if (msg instanceof ChatMessage chatMessage) { } else if (msg instanceof ChatMessage chatMessage) {
chat.chatReceived(chatMessage); chat.chatReceived(chatMessage);
if (soundManager != null) { if (soundManager != null) {
soundManager.play("chat", 1, myPlayer.getEyePosition(), myPlayer.getVelocity()); soundManager.play("chat", 1, myPlayer.getPosition(), myPlayer.getVelocity());
} }
} else if (msg instanceof ClientRecoilMessage recoil) {
runLater(() -> {
myPlayer.setOrientation(myPlayer.getOrientation().x + recoil.dx(), myPlayer.getOrientation().y + recoil.dy());
if (gameRenderer != null) {
gameRenderer.getCamera().setOrientationToPlayer(myPlayer);
}
communicationHandler.sendDatagramPacket(ClientOrientationState.fromPlayer(myPlayer));
});
} }
} }
@ -217,14 +201,6 @@ public class Client implements Runnable {
return world; return world;
} }
public InputHandler getInputHandler() {
return inputHandler;
}
public CommunicationHandler getCommunicationHandler() {
return communicationHandler;
}
public Map<Integer, Team> getTeams() { public Map<Integer, Team> getTeams() {
return teams; return teams;
} }
@ -241,17 +217,13 @@ public class Client implements Runnable {
return chat; return chat;
} }
public SoundManager getSoundManager() {
return soundManager;
}
public void interpolatePlayers(long now, float dt) { public void interpolatePlayers(long now, float dt) {
Vector3f movement = new Vector3f(); Vector3f movement = new Vector3f();
for (var player : players.values()) { for (var player : players.values()) {
movement.set(player.getVelocity()).mul(dt); movement.set(player.getVelocity()).mul(dt);
player.getPosition().add(movement); player.getPosition().add(movement);
player.updateModelTransform(); player.updateModelTransform();
soundManager.playWalkingSounds(player, world, now); soundManager.playWalkingSounds(player, now);
} }
gameRenderer.getGuiRenderer().updateNamePlates(players.values()); gameRenderer.getGuiRenderer().updateNamePlates(players.values());
} }
@ -264,27 +236,13 @@ public class Client implements Runnable {
} }
} }
public void runLater(Runnable runnable) {
mainThreadActions.add(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. 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());
Client client = new Client(clientConfig, connectCfg); Client client = new Client(clientConfig);
client.run(); client.run();
} }
} }

View File

@ -3,13 +3,11 @@ package nl.andrewl.aos2_client;
import nl.andrewl.aos2_client.model.ClientPlayer; import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos2_client.model.OtherPlayer; import nl.andrewl.aos2_client.model.OtherPlayer;
import nl.andrewl.aos_core.Net; import nl.andrewl.aos_core.Net;
import nl.andrewl.aos_core.model.PlayerMode;
import nl.andrewl.aos_core.model.Team; 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.TcpReceiver; import nl.andrewl.aos_core.net.*;
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;
@ -18,6 +16,8 @@ import nl.andrewl.record_net.Message;
import nl.andrewl.record_net.util.ExtendedDataInputStream; import nl.andrewl.record_net.util.ExtendedDataInputStream;
import nl.andrewl.record_net.util.ExtendedDataOutputStream; import nl.andrewl.record_net.util.ExtendedDataOutputStream;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.DatagramPacket; import java.net.DatagramPacket;
@ -31,6 +31,8 @@ import java.net.Socket;
* methods for sending messages and processing those we receive. * methods for sending messages and processing those we receive.
*/ */
public class CommunicationHandler { public class CommunicationHandler {
private static final Logger log = LoggerFactory.getLogger(CommunicationHandler.class);
private final Client client; private final Client client;
private Socket socket; private Socket socket;
private DatagramSocket datagramSocket; private DatagramSocket datagramSocket;
@ -47,14 +49,16 @@ public class CommunicationHandler {
if (socket != null && !socket.isClosed()) { if (socket != null && !socket.isClosed()) {
socket.close(); socket.close();
} }
InetAddress address = InetAddress.getByName(client.connectConfig.host()); InetAddress address = InetAddress.getByName(client.getConfig().serverHost);
System.out.printf("Connecting to server at %s, port %d, with username \"%s\"...%n", address, client.connectConfig.port(), client.connectConfig.username()); int port = client.getConfig().serverPort;
String username = client.getConfig().username;
log.info("Connecting to server at {}, port {}, with username \"{}\"...", address, port, username);
socket = new Socket(address, client.connectConfig.port()); socket = new Socket(address, 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(client.connectConfig.username(), client.connectConfig.spectator()), out); Net.write(new ConnectRequestMessage(username), 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) {
@ -62,9 +66,12 @@ 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, client.connectConfig.username())); log.debug("Connection accepted. My client id is {}.", clientId);
client.setMyPlayer(new ClientPlayer(clientId, username));
receiveInitialData(); receiveInitialData();
log.debug("Initial data received.");
establishDatagramConnection(); establishDatagramConnection();
log.info("Connection to server established. My client id is {}.", clientId);
new Thread(new TcpReceiver(in, client::onMessageReceived).withShutdownHook(this::shutdown)).start(); new Thread(new TcpReceiver(in, client::onMessageReceived).withShutdownHook(this::shutdown)).start();
new Thread(new UdpReceiver(datagramSocket, (msg, packet) -> client.onMessageReceived(msg))).start(); new Thread(new UdpReceiver(datagramSocket, (msg, packet) -> client.onMessageReceived(msg))).start();
} else { } else {
@ -131,6 +138,7 @@ public class CommunicationHandler {
if (!connectionEstablished) { if (!connectionEstablished) {
throw new IOException("Could not establish a datagram connection to the server after " + attempts + " attempts."); throw new IOException("Could not establish a datagram connection to the server after " + attempts + " attempts.");
} }
log.debug("Established datagram communication with the server.");
} }
public int getClientId() { public int getClientId() {
@ -167,13 +175,13 @@ public class CommunicationHandler {
OtherPlayer player = new OtherPlayer(in.readInt(), in.readString()); OtherPlayer player = new OtherPlayer(in.readInt(), in.readString());
int teamId = in.readInt(); int teamId = in.readInt();
if (teamId != -1) player.setTeam(client.getTeams().get(teamId)); if (teamId != -1) player.setTeam(client.getTeams().get(teamId));
System.out.println(teamId);
player.getPosition().set(in.readFloat(), in.readFloat(), in.readFloat()); player.getPosition().set(in.readFloat(), in.readFloat(), in.readFloat());
player.getVelocity().set(in.readFloat(), in.readFloat(), in.readFloat()); player.getVelocity().set(in.readFloat(), in.readFloat(), in.readFloat());
player.getOrientation().set(in.readFloat(), in.readFloat()); player.getOrientation().set(in.readFloat(), in.readFloat());
player.setCrouching(in.readBoolean()); player.setCrouching(in.readBoolean());
player.setHeldItemId(in.readInt()); player.setHeldItemId(in.readInt());
player.setSelectedBlockValue(in.readByte()); player.setSelectedBlockValue(in.readByte());
player.setMode(PlayerMode.values()[in.readInt()]);
client.getPlayers().put(player.getId(), player); client.getPlayers().put(player.getId(), player);
} }
@ -189,6 +197,5 @@ public class CommunicationHandler {
int teamId = in.readInt(); int teamId = in.readInt();
if (teamId != -1) client.getMyPlayer().setTeam(client.getTeams().get(teamId)); if (teamId != -1) client.getMyPlayer().setTeam(client.getTeams().get(teamId));
client.getMyPlayer().getPosition().set(in.readFloat(), in.readFloat(), in.readFloat()); client.getMyPlayer().getPosition().set(in.readFloat(), in.readFloat(), in.readFloat());
client.getMyPlayer().setMode(PlayerMode.values()[in.readInt()]);
} }
} }

View File

@ -1,6 +1,9 @@
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();

View File

@ -1,11 +0,0 @@
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
) {}

View File

@ -1,23 +0,0 @@
package nl.andrewl.aos2_client.control;
/**
* Represents a particular context in which player input is obtained. Different
* contexts do different things with the input. The main game, for example, will
* move the player when WASD keys are pressed, and a chatting context might type
* out the keys the player presses into a chat buffer. Contexts may choose to
* implement only some specified methods here.
*/
public interface InputContext {
default void onEnable() {}
default void onDisable() {}
default void keyPress(long window, int key, int mods) {}
default void keyRelease(long window, int key, int mods) {}
default void keyRepeat(long window, int key, int mods) {}
default void charInput(long window, int codePoint) {}
default void mouseButtonPress(long window, int button, int mods) {}
default void mouseButtonRelease(long window, int button, int mods) {}
default void mouseScroll(long window, double xOffset, double yOffset) {}
default void mouseCursorPos(long window, double xPos, double yPos) {}
}

View File

@ -1,11 +1,14 @@
package nl.andrewl.aos2_client.control; package nl.andrewl.aos2_client.control;
import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos2_client.Client; import nl.andrewl.aos2_client.Client;
import nl.andrewl.aos2_client.CommunicationHandler; import nl.andrewl.aos2_client.CommunicationHandler;
import nl.andrewl.aos2_client.control.context.ChattingContext; import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos2_client.control.context.ExitMenuContext; import nl.andrewl.aos_core.model.item.BlockItemStack;
import nl.andrewl.aos2_client.control.context.NormalContext; import nl.andrewl.aos_core.model.world.Hit;
import nl.andrewl.aos_core.net.client.BlockColorMessage;
import nl.andrewl.aos_core.net.client.ClientInputState;
import static org.lwjgl.glfw.GLFW.*;
/** /**
* Class which manages the player's input, and sending it to the server. * Class which manages the player's input, and sending it to the server.
@ -14,83 +17,65 @@ public class InputHandler {
private final Client client; private final Client client;
private final CommunicationHandler comm; private final CommunicationHandler comm;
private long windowId; private ClientInputState lastInputState = null;
private final NormalContext normalContext; private boolean forward;
private final ChattingContext chattingContext; private boolean backward;
private final ExitMenuContext exitMenuContext; private boolean left;
private boolean right;
private boolean jumping;
private boolean crouching;
private boolean sprinting;
private boolean hitting;
private boolean interacting;
private boolean reloading;
private InputContext activeContext;
public InputHandler(Client client, CommunicationHandler comm, Camera cam) { public InputHandler(Client client, CommunicationHandler comm) {
this.client = client; this.client = client;
this.comm = comm; this.comm = comm;
this.normalContext = new NormalContext(this, cam);
this.chattingContext = new ChattingContext(this);
this.exitMenuContext = new ExitMenuContext(this);
this.activeContext = normalContext;
} }
public void setWindowId(long windowId) { public void updateInputState(long window) {
this.windowId = windowId; // TODO: Allow customized keybindings.
int selectedInventoryIndex;
selectedInventoryIndex = client.getMyPlayer().getInventory().getSelectedIndex();
if (glfwGetKey(window, GLFW_KEY_1) == GLFW_PRESS) selectedInventoryIndex = 0;
if (glfwGetKey(window, GLFW_KEY_2) == GLFW_PRESS) selectedInventoryIndex = 1;
if (glfwGetKey(window, GLFW_KEY_3) == GLFW_PRESS) selectedInventoryIndex = 2;
if (glfwGetKey(window, GLFW_KEY_4) == GLFW_PRESS) selectedInventoryIndex = 3;
ClientInputState currentInputState = new ClientInputState(
comm.getClientId(),
glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS,
glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS,
glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS,
glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS,
glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS,
glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS,
glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS,
glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1) == GLFW_PRESS,
glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) == GLFW_PRESS,
glfwGetKey(window, GLFW_KEY_R) == GLFW_PRESS,
selectedInventoryIndex
);
if (!currentInputState.equals(lastInputState)) {
comm.sendDatagramPacket(currentInputState);
lastInputState = currentInputState;
} }
public InputContext getActiveContext() { ClientPlayer player = client.getMyPlayer();
return activeContext;
}
private void switchToContext(InputContext newContext) { // Check for "pick block" functionality.
if (newContext.equals(activeContext)) return; if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_3) == GLFW_PRESS && player.getInventory().getSelectedItemStack() instanceof BlockItemStack stack) {
activeContext.onDisable(); Hit hit = client.getWorld().getLookingAtPos(player.getEyePosition(), player.getViewVector(), 50);
newContext.onEnable(); if (hit != null) {
activeContext = newContext; byte selectedBlock = client.getWorld().getBlockAt(hit.pos().x, hit.pos().y, hit.pos().z);
} if (selectedBlock > 0) {
stack.setSelectedValue(selectedBlock);
public void switchToNormalContext() { comm.sendDatagramPacket(new BlockColorMessage(player.getId(), selectedBlock));
switchToContext(normalContext); }
} }
}
public void switchToChattingContext() {
switchToContext(chattingContext);
}
public void switchToExitMenuContext() {
switchToContext(exitMenuContext);
}
public NormalContext getNormalContext() {
return normalContext;
}
public ChattingContext getChattingContext() {
return chattingContext;
}
public ExitMenuContext getExitMenuContext() {
return exitMenuContext;
}
public boolean isNormalContextActive() {
return normalContext.equals(activeContext);
}
public boolean isChattingContextActive() {
return chattingContext.equals(activeContext);
}
public boolean isExitMenuContextActive() {
return exitMenuContext.equals(activeContext);
}
public Client getClient() {
return client;
}
public CommunicationHandler getComm() {
return comm;
}
public long getWindowId() {
return windowId;
} }
} }

View File

@ -1,16 +0,0 @@
package nl.andrewl.aos2_client.control;
import org.lwjgl.glfw.GLFWCharCallbackI;
public class PlayerCharacterInputCallback implements GLFWCharCallbackI {
private final InputHandler inputHandler;
public PlayerCharacterInputCallback(InputHandler inputHandler) {
this.inputHandler = inputHandler;
}
@Override
public void invoke(long window, int codepoint) {
inputHandler.getActiveContext().charInput(window, codepoint);
}
}

View File

@ -13,10 +13,9 @@ public class PlayerInputKeyCallback implements GLFWKeyCallbackI {
@Override @Override
public void invoke(long window, int key, int scancode, int action, int mods) { public void invoke(long window, int key, int scancode, int action, int mods) {
switch (action) { if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
case GLFW_PRESS -> inputHandler.getActiveContext().keyPress(window, key, mods); glfwSetWindowShouldClose(window, true);
case GLFW_RELEASE -> inputHandler.getActiveContext().keyRelease(window, key, mods); }
case GLFW_REPEAT -> inputHandler.getActiveContext().keyRepeat(window, key, mods); inputHandler.updateInputState(window);
}
} }
} }

View File

@ -2,9 +2,6 @@ package nl.andrewl.aos2_client.control;
import org.lwjgl.glfw.GLFWMouseButtonCallbackI; import org.lwjgl.glfw.GLFWMouseButtonCallbackI;
import static org.lwjgl.glfw.GLFW.GLFW_PRESS;
import static org.lwjgl.glfw.GLFW.GLFW_RELEASE;
/** /**
* Callback that's called when the player clicks with their mouse. * Callback that's called when the player clicks with their mouse.
*/ */
@ -17,9 +14,6 @@ public class PlayerInputMouseClickCallback implements GLFWMouseButtonCallbackI {
@Override @Override
public void invoke(long window, int button, int action, int mods) { public void invoke(long window, int button, int action, int mods) {
switch (action) { inputHandler.updateInputState(window);
case GLFW_PRESS -> inputHandler.getActiveContext().mouseButtonPress(window, button, mods);
case GLFW_RELEASE -> inputHandler.getActiveContext().mouseButtonRelease(window, button, mods);
}
} }
} }

View File

@ -1,16 +1,29 @@
package nl.andrewl.aos2_client.control; package nl.andrewl.aos2_client.control;
import nl.andrewl.aos2_client.Client;
import nl.andrewl.aos2_client.CommunicationHandler;
import nl.andrewl.aos_core.model.item.BlockItemStack;
import nl.andrewl.aos_core.net.client.BlockColorMessage;
import org.lwjgl.glfw.GLFWScrollCallbackI; import org.lwjgl.glfw.GLFWScrollCallbackI;
public class PlayerInputMouseScrollCallback implements GLFWScrollCallbackI { public class PlayerInputMouseScrollCallback implements GLFWScrollCallbackI {
private final InputHandler inputHandler; private final Client client;
private final CommunicationHandler comm;
public PlayerInputMouseScrollCallback(InputHandler inputHandler) { public PlayerInputMouseScrollCallback(Client client, CommunicationHandler comm) {
this.inputHandler = inputHandler; this.client = client;
this.comm = comm;
} }
@Override @Override
public void invoke(long window, double xoffset, double yoffset) { public void invoke(long window, double xoffset, double yoffset) {
inputHandler.getActiveContext().mouseScroll(window, xoffset, yoffset); if (client.getMyPlayer().getInventory().getSelectedItemStack() instanceof BlockItemStack stack) {
if (yoffset < 0) {
stack.setSelectedValue((byte) (stack.getSelectedValue() - 1));
} else if (yoffset > 0) {
stack.setSelectedValue((byte) (stack.getSelectedValue() + 1));
}
comm.sendDatagramPacket(new BlockColorMessage(client.getMyPlayer().getId(), stack.getSelectedValue()));
}
} }
} }

View File

@ -1,17 +1,66 @@
package nl.andrewl.aos2_client.control; package nl.andrewl.aos2_client.control;
import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos2_client.Client;
import nl.andrewl.aos2_client.CommunicationHandler;
import nl.andrewl.aos2_client.config.ClientConfig;
import nl.andrewl.aos_core.net.client.ClientOrientationState;
import org.lwjgl.glfw.GLFWCursorPosCallbackI; import org.lwjgl.glfw.GLFWCursorPosCallbackI;
import java.util.concurrent.ForkJoinPool;
import static org.lwjgl.glfw.GLFW.glfwGetCursorPos;
/**
* Callback that's called when the player's cursor position updates. This means
* the player is looking around.
*/
public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI { public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI {
/**
* The number of milliseconds to wait before sending orientation updates,
* to prevent overloading the server.
*/
private static final int ORIENTATION_UPDATE_LIMIT = 20;
private final InputHandler inputHandler; private final ClientConfig.InputConfig config;
private final Client client;
private final Camera camera;
private final CommunicationHandler comm;
private float lastMouseCursorX;
private float lastMouseCursorY;
private long lastOrientationUpdateSentAt = 0L;
public PlayerViewCursorCallback(InputHandler inputHandler) { public PlayerViewCursorCallback(ClientConfig.InputConfig config, Client client, Camera cam, CommunicationHandler comm) {
this.inputHandler = inputHandler; this.config = config;
this.client = client;
this.camera = cam;
this.comm = comm;
} }
@Override @Override
public void invoke(long window, double xpos, double ypos) { public void invoke(long window, double xpos, double ypos) {
inputHandler.getActiveContext().mouseCursorPos(window, xpos, ypos); double[] xb = new double[1];
double[] yb = new double[1];
glfwGetCursorPos(window, xb, yb);
float x = (float) xb[0];
float y = (float) yb[0];
float dx = x - lastMouseCursorX;
float dy = y - lastMouseCursorY;
lastMouseCursorX = x;
lastMouseCursorY = y;
client.getMyPlayer().setOrientation(
client.getMyPlayer().getOrientation().x - dx * config.mouseSensitivity,
client.getMyPlayer().getOrientation().y - dy * config.mouseSensitivity
);
camera.setOrientationToPlayer(client.getMyPlayer());
long now = System.currentTimeMillis();
if (lastOrientationUpdateSentAt + ORIENTATION_UPDATE_LIMIT < now) {
ForkJoinPool.commonPool().submit(() -> comm.sendDatagramPacket(new ClientOrientationState(
client.getMyPlayer().getId(),
client.getMyPlayer().getOrientation().x,
client.getMyPlayer().getOrientation().y
)));
lastOrientationUpdateSentAt = now;
}
} }
} }

View File

@ -1,88 +0,0 @@
package nl.andrewl.aos2_client.control.context;
import nl.andrewl.aos2_client.control.InputContext;
import nl.andrewl.aos2_client.control.InputHandler;
import nl.andrewl.aos2_client.util.WindowUtils;
import nl.andrewl.aos_core.net.client.ChatWrittenMessage;
import static org.lwjgl.glfw.GLFW.*;
public class ChattingContext implements InputContext {
private static final int MAX_LENGTH = 120;
private final InputHandler inputHandler;
private final StringBuffer chatBuffer = new StringBuffer(MAX_LENGTH);
public ChattingContext(InputHandler inputHandler) {
this.inputHandler = inputHandler;
}
public void appendToChat(int codePoint) {
appendToChat(Character.toString(codePoint));
}
public void appendToChat(String content) {
if (chatBuffer.length() + content.length() > MAX_LENGTH) return;
chatBuffer.append(content);
}
private void deleteFromChat() {
if (chatBuffer.length() > 0) {
chatBuffer.deleteCharAt(chatBuffer.length() - 1);
}
}
private void clearChatBuffer() {
chatBuffer.delete(0, chatBuffer.length());
}
private void sendChat() {
String text = chatBuffer.toString().trim();
if (!text.isBlank()) {
inputHandler.getComm().sendMessage(new ChatWrittenMessage(text));
}
inputHandler.switchToNormalContext();
}
public String getChatBufferText() {
return new String(chatBuffer);
}
@Override
public void onEnable() {
clearChatBuffer();
if (inputHandler.getClient().getConfig().display.captureCursor) {
WindowUtils.freeCursor(inputHandler.getWindowId());
}
}
@Override
public void onDisable() {
clearChatBuffer();
if (inputHandler.getClient().getConfig().display.captureCursor) {
WindowUtils.captureCursor(inputHandler.getWindowId());
}
}
@Override
public void keyPress(long window, int key, int mods) {
switch (key) {
case GLFW_KEY_BACKSPACE -> deleteFromChat();
case GLFW_KEY_ENTER -> sendChat();
case GLFW_KEY_ESCAPE -> inputHandler.switchToNormalContext();
}
}
@Override
public void keyRepeat(long window, int key, int mods) {
switch (key) {
case GLFW_KEY_BACKSPACE -> deleteFromChat();
}
}
@Override
public void charInput(long window, int codePoint) {
appendToChat(codePoint);
}
}

View File

@ -1,38 +0,0 @@
package nl.andrewl.aos2_client.control.context;
import nl.andrewl.aos2_client.control.InputContext;
import nl.andrewl.aos2_client.control.InputHandler;
import nl.andrewl.aos2_client.util.WindowUtils;
import static org.lwjgl.glfw.GLFW.GLFW_KEY_ESCAPE;
import static org.lwjgl.glfw.GLFW.glfwSetWindowShouldClose;
public class ExitMenuContext implements InputContext {
private final InputHandler inputHandler;
public ExitMenuContext(InputHandler inputHandler) {
this.inputHandler = inputHandler;
}
@Override
public void onEnable() {
if (inputHandler.getClient().getConfig().display.captureCursor) {
WindowUtils.freeCursor(inputHandler.getWindowId());
}
}
@Override
public void onDisable() {
if (inputHandler.getClient().getConfig().display.captureCursor) {
WindowUtils.captureCursor(inputHandler.getWindowId());
}
}
@Override
public void keyPress(long window, int key, int mods) {
switch (key) {
case GLFW_KEY_ESCAPE -> glfwSetWindowShouldClose(window, true);
default -> inputHandler.switchToNormalContext();
}
}
}

View File

@ -1,287 +0,0 @@
package nl.andrewl.aos2_client.control.context;
import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos2_client.control.InputContext;
import nl.andrewl.aos2_client.control.InputHandler;
import nl.andrewl.aos2_client.util.WindowUtils;
import nl.andrewl.aos_core.model.item.BlockItemStack;
import nl.andrewl.aos_core.model.item.GunItemStack;
import nl.andrewl.aos_core.model.item.ItemStack;
import nl.andrewl.aos_core.model.world.Hit;
import nl.andrewl.aos_core.net.client.BlockColorMessage;
import nl.andrewl.aos_core.net.client.ClientInputState;
import nl.andrewl.aos_core.net.client.ClientOrientationState;
import java.util.concurrent.ForkJoinPool;
import static org.lwjgl.glfw.GLFW.*;
/**
* The normal input context that occurs when the player is active in the game.
* This includes moving around, interacting with their inventory, moving their
* view, and so on.
*/
public class NormalContext implements InputContext {
/**
* The number of milliseconds to wait before sending orientation updates,
* to prevent overloading the server.
*/
private static final int ORIENTATION_UPDATE_LIMIT = 20;
private final InputHandler inputHandler;
private final Camera camera;
private ClientInputState lastInputState = null;
private boolean forward;
private boolean backward;
private boolean left;
private boolean right;
private boolean jumping;
private boolean crouching;
private boolean sprinting;
private boolean hitting;
private boolean interacting;
private boolean reloading;
private int selectedInventoryIndex;
private boolean debugEnabled;
private float lastMouseCursorX;
private float lastMouseCursorY;
private long lastOrientationUpdateSentAt = 0L;
public NormalContext(InputHandler inputHandler, Camera camera) {
this.inputHandler = inputHandler;
this.camera = camera;
}
public void updateInputState() {
var comm = inputHandler.getComm();
ClientInputState currentInputState = new ClientInputState(
comm.getClientId(),
forward, backward, left, right,
jumping, crouching, sprinting,
hitting, interacting, reloading,
selectedInventoryIndex
);
if (!currentInputState.equals(lastInputState)) {
comm.sendDatagramPacket(currentInputState);
lastInputState = currentInputState;
}
}
public void resetInputState() {
forward = false;
backward = false;
left = false;
right = false;
jumping = false;
crouching = false;
sprinting = false;
hitting = false;
interacting = false;
reloading = false;
var size = WindowUtils.getSize(inputHandler.getWindowId());
lastMouseCursorX = size.first() / 2f;
lastMouseCursorY = size.second() / 2f;
updateInputState();
}
public void setForward(boolean forward) {
this.forward = forward;
updateInputState();
}
public void setBackward(boolean backward) {
this.backward = backward;
updateInputState();
}
public void setLeft(boolean left) {
this.left = left;
updateInputState();
}
public void setRight(boolean right) {
this.right = right;
updateInputState();
}
public void setJumping(boolean jumping) {
this.jumping = jumping;
updateInputState();
}
public void setCrouching(boolean crouching) {
this.crouching = crouching;
updateInputState();
}
public void setSprinting(boolean sprinting) {
this.sprinting = sprinting;
updateInputState();
}
public void setHitting(boolean hitting) {
this.hitting = hitting;
updateInputState();
}
public void setInteracting(boolean interacting) {
this.interacting = interacting;
updateInputState();
}
public void setReloading(boolean reloading) {
this.reloading = reloading;
updateInputState();
}
public void setSelectedInventoryIndex(int selectedInventoryIndex) {
this.selectedInventoryIndex = selectedInventoryIndex;
updateInputState();
}
public void toggleDebugEnabled() {
this.debugEnabled = !debugEnabled;
}
public void enableChatting() {
inputHandler.switchToChattingContext();
}
@Override
public void onEnable() {
resetInputState();
}
@Override
public void onDisable() {
resetInputState();
}
@Override
public void keyPress(long window, int key, int mods) {
switch (key) {
case GLFW_KEY_W -> setForward(true);
case GLFW_KEY_A -> setLeft(true);
case GLFW_KEY_S -> setBackward(true);
case GLFW_KEY_D -> setRight(true);
case GLFW_KEY_SPACE -> setJumping(true);
case GLFW_KEY_LEFT_CONTROL -> setCrouching(true);
case GLFW_KEY_LEFT_SHIFT -> setSprinting(true);
case GLFW_KEY_R -> setReloading(true);
case GLFW_KEY_1 -> setSelectedInventoryIndex(0);
case GLFW_KEY_2 -> setSelectedInventoryIndex(1);
case GLFW_KEY_3 -> setSelectedInventoryIndex(2);
case GLFW_KEY_4 -> setSelectedInventoryIndex(3);
case GLFW_KEY_F3 -> toggleDebugEnabled();
case GLFW_KEY_ESCAPE -> inputHandler.switchToExitMenuContext();
}
}
@Override
public void keyRelease(long window, int key, int mods) {
switch (key) {
case GLFW_KEY_W -> setForward(false);
case GLFW_KEY_A -> setLeft(false);
case GLFW_KEY_S -> setBackward(false);
case GLFW_KEY_D -> setRight(false);
case GLFW_KEY_SPACE -> setJumping(false);
case GLFW_KEY_LEFT_CONTROL -> setCrouching(false);
case GLFW_KEY_LEFT_SHIFT -> setSprinting(false);
case GLFW_KEY_R -> setReloading(false);
case GLFW_KEY_T -> enableChatting();
case GLFW_KEY_SLASH -> {
enableChatting();
inputHandler.getChattingContext().appendToChat("/");
}
}
}
@Override
public void mouseButtonPress(long window, int button, int mods) {
switch (button) {
case GLFW_MOUSE_BUTTON_1 -> setHitting(true);
case GLFW_MOUSE_BUTTON_2 -> setInteracting(true);
case GLFW_MOUSE_BUTTON_3 -> pickBlock();
}
}
@Override
public void mouseButtonRelease(long window, int button, int mods) {
switch (button) {
case GLFW_MOUSE_BUTTON_1 -> setHitting(false);
case GLFW_MOUSE_BUTTON_2 -> setInteracting(false);
}
}
@Override
public void mouseScroll(long window, double xOffset, double yOffset) {
var player = inputHandler.getClient().getMyPlayer();
ItemStack stack = player.getInventory().getSelectedItemStack();
if (stack instanceof BlockItemStack blockStack) {
if (yOffset < 0) {
blockStack.setSelectedValue((byte) (blockStack.getSelectedValue() - 1));
} else if (yOffset > 0) {
blockStack.setSelectedValue((byte) (blockStack.getSelectedValue() + 1));
}
inputHandler.getComm().sendDatagramPacket(new BlockColorMessage(player.getId(), blockStack.getSelectedValue()));
}
}
@Override
public void mouseCursorPos(long window, double xPos, double yPos) {
double[] xb = new double[1];
double[] yb = new double[1];
glfwGetCursorPos(window, xb, yb);
float x = (float) xb[0];
float y = (float) yb[0];
float dx = x - lastMouseCursorX;
float dy = y - lastMouseCursorY;
lastMouseCursorX = x;
lastMouseCursorY = y;
var client = inputHandler.getClient();
float trueSensitivity = inputHandler.getClient().getConfig().input.mouseSensitivity;
if (isScopeEnabled()) trueSensitivity *= 0.1f;
client.getMyPlayer().setOrientation(
client.getMyPlayer().getOrientation().x - dx * trueSensitivity,
client.getMyPlayer().getOrientation().y - dy * trueSensitivity
);
camera.setOrientationToPlayer(client.getMyPlayer());
long now = System.currentTimeMillis();
if (lastOrientationUpdateSentAt + ORIENTATION_UPDATE_LIMIT < now) {
ForkJoinPool.commonPool().submit(() -> inputHandler.getComm().sendDatagramPacket(ClientOrientationState.fromPlayer(client.getMyPlayer())));
lastOrientationUpdateSentAt = now;
}
}
public void pickBlock() {
var client = inputHandler.getClient();
var comm = inputHandler.getComm();
var player = client.getMyPlayer();
if (player.getInventory().getSelectedItemStack() instanceof BlockItemStack stack) {
Hit hit = client.getWorld().getLookingAtPos(player.getEyePosition(), player.getViewVector(), 50);
if (hit != null) {
byte selectedBlock = client.getWorld().getBlockAt(hit.pos().x, hit.pos().y, hit.pos().z);
if (selectedBlock > 0) {
stack.setSelectedValue(selectedBlock);
comm.sendDatagramPacket(new BlockColorMessage(player.getId(), selectedBlock));
}
}
}
}
public boolean isScopeEnabled() {
return interacting &&
inputHandler.getClient().getMyPlayer().getInventory().getSelectedItemStack() instanceof GunItemStack;
}
public boolean isDebugEnabled() {
return debugEnabled;
}
}

View File

@ -1,7 +1,6 @@
package nl.andrewl.aos2_client.model; package nl.andrewl.aos2_client.model;
import nl.andrewl.aos2_client.Camera; import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos2_client.control.InputHandler;
import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.item.Inventory; import nl.andrewl.aos_core.model.item.Inventory;
import org.joml.Matrix3f; import org.joml.Matrix3f;
@ -43,16 +42,12 @@ public class ClientPlayer extends Player {
this.health = health; this.health = health;
} }
public void updateHeldItemTransform(Camera cam, InputHandler inputHandler) { public void updateHeldItemTransform(Camera cam) {
heldItemTransform.identity() heldItemTransform.identity()
.translate(cam.getPosition()) .translate(cam.getPosition())
.rotate((float) (cam.getOrientation().x + Math.PI), Camera.UP) .rotate((float) (cam.getOrientation().x + Math.PI), Camera.UP)
.rotate(-cam.getOrientation().y + (float) Math.PI / 2, Camera.RIGHT); .rotate(-cam.getOrientation().y + (float) Math.PI / 2, Camera.RIGHT)
if (inputHandler.isNormalContextActive() && inputHandler.getNormalContext().isScopeEnabled()) { .translate(-0.35f, -0.4f, 0.5f);
heldItemTransform.translate(0, -0.12f, 0);
} else {
heldItemTransform.translate(-0.35f, -0.4f, 0.5f);
}
heldItemTransform.get(heldItemTransformData); heldItemTransform.get(heldItemTransformData);
heldItemTransform.normal(heldItemNormalTransform); heldItemTransform.normal(heldItemNormalTransform);

View File

@ -1,10 +1,8 @@
package nl.andrewl.aos2_client.model; package nl.andrewl.aos2_client.model;
import nl.andrewl.aos2_client.Camera; import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos2_client.Client;
import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.item.ItemTypes; import nl.andrewl.aos_core.model.item.ItemTypes;
import nl.andrewl.aos_core.net.client.PlayerJoinMessage;
import org.joml.Matrix3f; import org.joml.Matrix3f;
import org.joml.Matrix4f; import org.joml.Matrix4f;
import org.joml.Vector3f; import org.joml.Vector3f;
@ -101,18 +99,4 @@ public class OtherPlayer extends Player {
public float[] getHeldItemNormalTransformData() { public float[] getHeldItemNormalTransformData() {
return heldItemNormalTransformData; return heldItemNormalTransformData;
} }
public static OtherPlayer fromJoinMessage(PlayerJoinMessage msg, Client client) {
OtherPlayer op = new OtherPlayer(msg.id(), msg.username());
if (msg.teamId() != -1 && client.getTeams().containsKey(msg.teamId())) {
op.setTeam(client.getTeams().get(msg.teamId()));
}
op.getPosition().set(msg.px(), msg.py(), msg.pz());
op.getVelocity().set(msg.vx(), msg.vy(), msg.vz());
op.getOrientation().set(msg.ox(), msg.oy());
op.setHeldItemId(msg.selectedItemId());
op.setSelectedBlockValue(msg.selectedBlockValue());
op.setMode(msg.mode());
return op;
}
} }

View File

@ -3,23 +3,20 @@ package nl.andrewl.aos2_client.render;
import nl.andrewl.aos2_client.Camera; import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos2_client.Client; import nl.andrewl.aos2_client.Client;
import nl.andrewl.aos2_client.config.ClientConfig; import nl.andrewl.aos2_client.config.ClientConfig;
import nl.andrewl.aos2_client.control.*;
import nl.andrewl.aos2_client.model.ClientPlayer; import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos2_client.render.chunk.ChunkRenderer; import nl.andrewl.aos2_client.render.chunk.ChunkRenderer;
import nl.andrewl.aos2_client.render.gui.GuiRenderer; import nl.andrewl.aos2_client.render.gui.GuiRenderer;
import nl.andrewl.aos2_client.render.model.Model; import nl.andrewl.aos2_client.render.model.Model;
import nl.andrewl.aos_core.model.PlayerMode;
import nl.andrewl.aos_core.model.Team; import nl.andrewl.aos_core.model.Team;
import nl.andrewl.aos_core.model.item.BlockItemStack; import nl.andrewl.aos_core.model.item.BlockItemStack;
import nl.andrewl.aos_core.model.item.Inventory;
import nl.andrewl.aos_core.model.item.ItemTypes; import nl.andrewl.aos_core.model.item.ItemTypes;
import org.joml.Matrix3f; import org.joml.Matrix3f;
import org.joml.Matrix4f; import org.joml.Matrix4f;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.lwjgl.glfw.Callbacks; import org.lwjgl.glfw.*;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
@ -33,43 +30,46 @@ import static org.lwjgl.opengl.GL46.*;
* OpenGL context exists. * OpenGL context exists.
*/ */
public class GameRenderer { public class GameRenderer {
private static final Logger log = LoggerFactory.getLogger(GameRenderer.class);
private static final float Z_NEAR = 0.01f; private static final float Z_NEAR = 0.01f;
private static final float Z_FAR = 500f; private static final float Z_FAR = 500f;
private final ClientConfig.DisplayConfig config; private final ClientConfig.DisplayConfig config;
private final ChunkRenderer chunkRenderer; private ChunkRenderer chunkRenderer;
private final GuiRenderer guiRenderer; private GuiRenderer guiRenderer;
private final ModelRenderer modelRenderer; private ModelRenderer modelRenderer;
private final Camera camera; private final Camera camera;
private final Client client; private final Client client;
private final InputHandler inputHandler;
// Standard models for various game components. // Standard models for various game components.
private final Model playerModel; private Model playerModel;
private final Model rifleModel; private Model rifleModel;
private final Model blockModel; private Model blockModel;
private final Model bulletModel; private Model bulletModel;
private final Model smgModel; private Model smgModel;
private final Model shotgunModel; private Model shotgunModel;
private final Model flagModel; private Model flagModel;
private final long windowHandle; private long windowHandle;
private final int screenWidth; private int screenWidth = 800;
private final int screenHeight; private int screenHeight = 600;
private final Matrix4f perspectiveTransform; private final Matrix4f perspectiveTransform;
private final float[] perspectiveTransformData = new float[16];
public GameRenderer(Client client, InputHandler inputHandler, Camera camera) { public GameRenderer(ClientConfig.DisplayConfig config, Client client) {
this.config = client.getConfig().display; this.config = config;
this.client = client; this.client = client;
this.inputHandler = inputHandler; this.camera = new Camera();
this.camera = camera;
camera.setToPlayer(client.getMyPlayer()); camera.setToPlayer(client.getMyPlayer());
this.perspectiveTransform = new Matrix4f(); this.perspectiveTransform = new Matrix4f();
}
// Initialize window! public void setupWindow(
GLFWCursorPosCallbackI viewCursorCallback,
GLFWKeyCallbackI inputKeyCallback,
GLFWMouseButtonCallbackI mouseButtonCallback,
GLFWScrollCallbackI scrollCallback
) {
GLFWErrorCallback.createPrint(System.err).set(); GLFWErrorCallback.createPrint(System.err).set();
if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW."); if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW.");
glfwDefaultWindowHints(); glfwDefaultWindowHints();
@ -79,6 +79,7 @@ public class GameRenderer {
long monitorId = glfwGetPrimaryMonitor(); long monitorId = glfwGetPrimaryMonitor();
GLFWVidMode primaryMonitorSettings = glfwGetVideoMode(monitorId); GLFWVidMode primaryMonitorSettings = glfwGetVideoMode(monitorId);
if (primaryMonitorSettings == null) throw new IllegalStateException("Could not get information about the primary monitory."); if (primaryMonitorSettings == null) throw new IllegalStateException("Could not get information about the primary monitory.");
log.debug("Primary monitor settings: Width: {}, Height: {}, FOV: {}", primaryMonitorSettings.width(), primaryMonitorSettings.height(), config.fov);
if (config.fullscreen) { if (config.fullscreen) {
screenWidth = primaryMonitorSettings.width(); screenWidth = primaryMonitorSettings.width();
screenHeight = primaryMonitorSettings.height(); screenHeight = primaryMonitorSettings.height();
@ -89,22 +90,19 @@ public class GameRenderer {
windowHandle = glfwCreateWindow(screenWidth, screenHeight, "Ace of Shades 2", 0, 0); windowHandle = glfwCreateWindow(screenWidth, screenHeight, "Ace of Shades 2", 0, 0);
} }
if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window."); if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window.");
inputHandler.setWindowId(windowHandle); log.debug("Initialized GLFW window.");
// Setup callbacks. // Setup callbacks.
glfwSetKeyCallback(windowHandle, new PlayerInputKeyCallback(inputHandler)); glfwSetKeyCallback(windowHandle, inputKeyCallback);
glfwSetCursorPosCallback(windowHandle, new PlayerViewCursorCallback(inputHandler)); glfwSetCursorPosCallback(windowHandle, viewCursorCallback);
glfwSetMouseButtonCallback(windowHandle, new PlayerInputMouseClickCallback(inputHandler)); glfwSetMouseButtonCallback(windowHandle, mouseButtonCallback);
glfwSetScrollCallback(windowHandle, new PlayerInputMouseScrollCallback(inputHandler)); glfwSetScrollCallback(windowHandle, scrollCallback);
glfwSetCharCallback(windowHandle, new PlayerCharacterInputCallback(inputHandler));
glfwSetWindowFocusCallback(windowHandle, (window, focused) -> {
if (!focused) inputHandler.switchToExitMenuContext();
});
if (config.captureCursor) { if (config.captureCursor) {
glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
} }
glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
glfwSetCursorPos(windowHandle, 0, 0); glfwSetCursorPos(windowHandle, 0, 0);
log.debug("Set up window callbacks.");
glfwMakeContextCurrent(windowHandle); glfwMakeContextCurrent(windowHandle);
glfwSwapInterval(1); glfwSwapInterval(1);
@ -112,18 +110,21 @@ public class GameRenderer {
GL.createCapabilities(); GL.createCapabilities();
// GLUtil.setupDebugMessageCallback(System.out); // GLUtil.setupDebugMessageCallback(System.out);
glClearColor(0.1f, 0.1f, 0.1f, 0.0f); glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glEnable(GL_CULL_FACE); glEnable(GL_CULL_FACE);
glEnable(GL_DEPTH_TEST); glEnable(GL_DEPTH_TEST);
glCullFace(GL_BACK); glCullFace(GL_BACK);
log.debug("Initialized OpenGL context.");
this.chunkRenderer = new ChunkRenderer(); this.chunkRenderer = new ChunkRenderer();
log.debug("Initialized chunk renderer.");
try { try {
this.guiRenderer = new GuiRenderer(); this.guiRenderer = new GuiRenderer();
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
log.debug("Initialized GUI renderer.");
this.modelRenderer = new ModelRenderer(); this.modelRenderer = new ModelRenderer();
try { try {
@ -137,7 +138,8 @@ public class GameRenderer {
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
updatePerspective(config.fov); log.debug("Initialized model renderer.");
updatePerspective();
} }
public float getAspectRatio() { public float getAspectRatio() {
@ -147,17 +149,18 @@ public class GameRenderer {
/** /**
* Updates the rendering perspective used to render the game. * Updates the rendering perspective used to render the game.
*/ */
public void updatePerspective(float fov) { private void updatePerspective() {
float fovRad = (float) Math.toRadians(fov); float fovRad = (float) Math.toRadians(config.fov);
if (fovRad >= Math.PI) { if (fovRad >= Math.PI) {
fovRad = (float) (Math.PI - 0.01f); fovRad = (float) (Math.PI - 0.01f);
} else if (fovRad <= 0) { } else if (fovRad <= 0) {
fovRad = 0.01f; fovRad = 0.01f;
} }
perspectiveTransform.setPerspective(fovRad, getAspectRatio(), Z_NEAR, Z_FAR); perspectiveTransform.setPerspective(fovRad, getAspectRatio(), Z_NEAR, Z_FAR);
perspectiveTransform.get(perspectiveTransformData); float[] data = new float[16];
if (chunkRenderer != null) chunkRenderer.setPerspective(perspectiveTransformData); perspectiveTransform.get(data);
if (modelRenderer != null) modelRenderer.setPerspective(perspectiveTransformData); if (chunkRenderer != null) chunkRenderer.setPerspective(data);
if (modelRenderer != null) modelRenderer.setPerspective(data);
} }
public boolean windowShouldClose() { public boolean windowShouldClose() {
@ -174,24 +177,16 @@ public class GameRenderer {
public void draw() { public void draw() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
chunkRenderer.draw(camera, client.getWorld().getChunkMeshesToDraw());
ClientPlayer myPlayer = client.getMyPlayer(); ClientPlayer myPlayer = client.getMyPlayer();
Inventory inv = myPlayer.getInventory();
if (inputHandler.isNormalContextActive() && inputHandler.getNormalContext().isScopeEnabled()) {
updatePerspective(15);
} else {
updatePerspective(config.fov);
}
myPlayer.updateHeldItemTransform(camera, client.getInputHandler());
chunkRenderer.draw(camera, client.getWorld().getChunkMeshesToDraw());
// Draw models. Use one texture at a time for efficiency. // Draw models. Use one texture at a time for efficiency.
modelRenderer.start(camera.getViewTransformData()); modelRenderer.start(camera.getViewTransformData());
myPlayer.updateHeldItemTransform(camera);
playerModel.bind(); playerModel.bind();
for (var player : client.getPlayers().values()) { for (var player : client.getPlayers().values()) {
if (player.getMode() == PlayerMode.SPECTATOR) continue;
if (player.getTeam() != null) { if (player.getTeam() != null) {
modelRenderer.setAspectColor(player.getTeam().getColor()); modelRenderer.setAspectColor(player.getTeam().getColor());
} else { } else {
@ -203,33 +198,30 @@ public class GameRenderer {
// Render guns! // Render guns!
rifleModel.bind(); rifleModel.bind();
if (inv.getSelectedItemStack() != null && inv.getSelectedItemStack().getType().getId() == ItemTypes.RIFLE.getId()) { if (myPlayer.getInventory().getSelectedItemStack().getType().getId() == ItemTypes.RIFLE.getId()) {
modelRenderer.render(rifleModel, myPlayer.getHeldItemTransformData(), myPlayer.getHeldItemNormalTransformData()); modelRenderer.render(rifleModel, myPlayer.getHeldItemTransformData(), myPlayer.getHeldItemNormalTransformData());
} }
for (var player : client.getPlayers().values()) { for (var player : client.getPlayers().values()) {
if (player.getMode() == PlayerMode.SPECTATOR) continue;
if (player.getHeldItemId() == ItemTypes.RIFLE.getId()) { if (player.getHeldItemId() == ItemTypes.RIFLE.getId()) {
modelRenderer.render(rifleModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData()); modelRenderer.render(rifleModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData());
} }
} }
rifleModel.unbind(); rifleModel.unbind();
smgModel.bind(); smgModel.bind();
if (inv.getSelectedItemStack() != null && inv.getSelectedItemStack().getType().getId() == ItemTypes.AK_47.getId()) { if (myPlayer.getInventory().getSelectedItemStack().getType().getId() == ItemTypes.AK_47.getId()) {
modelRenderer.render(smgModel, myPlayer.getHeldItemTransformData(), myPlayer.getHeldItemNormalTransformData()); modelRenderer.render(smgModel, myPlayer.getHeldItemTransformData(), myPlayer.getHeldItemNormalTransformData());
} }
for (var player : client.getPlayers().values()) { for (var player : client.getPlayers().values()) {
if (player.getMode() == PlayerMode.SPECTATOR) continue;
if (player.getHeldItemId() == ItemTypes.AK_47.getId()) { if (player.getHeldItemId() == ItemTypes.AK_47.getId()) {
modelRenderer.render(smgModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData()); modelRenderer.render(smgModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData());
} }
} }
smgModel.unbind(); smgModel.unbind();
shotgunModel.bind(); shotgunModel.bind();
if (inv.getSelectedItemStack() != null && inv.getSelectedItemStack().getType().getId() == ItemTypes.WINCHESTER.getId()) { if (myPlayer.getInventory().getSelectedItemStack().getType().getId() == ItemTypes.WINCHESTER.getId()) {
modelRenderer.render(shotgunModel, myPlayer.getHeldItemTransformData(), myPlayer.getHeldItemNormalTransformData()); modelRenderer.render(shotgunModel, myPlayer.getHeldItemTransformData(), myPlayer.getHeldItemNormalTransformData());
} }
for (var player : client.getPlayers().values()) { for (var player : client.getPlayers().values()) {
if (player.getMode() == PlayerMode.SPECTATOR) continue;
if (player.getHeldItemId() == ItemTypes.WINCHESTER.getId()) { if (player.getHeldItemId() == ItemTypes.WINCHESTER.getId()) {
modelRenderer.render(shotgunModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData()); modelRenderer.render(shotgunModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData());
} }
@ -237,14 +229,13 @@ public class GameRenderer {
shotgunModel.unbind(); shotgunModel.unbind();
blockModel.bind(); blockModel.bind();
if (inv.getSelectedItemStack() != null && inv.getSelectedItemStack().getType().getId() == ItemTypes.BLOCK.getId()) { if (myPlayer.getInventory().getSelectedItemStack().getType().getId() == ItemTypes.BLOCK.getId()) {
BlockItemStack stack = (BlockItemStack) myPlayer.getInventory().getSelectedItemStack(); BlockItemStack stack = (BlockItemStack) myPlayer.getInventory().getSelectedItemStack();
modelRenderer.setAspectColor(client.getWorld().getPalette().getColor(stack.getSelectedValue())); modelRenderer.setAspectColor(client.getWorld().getPalette().getColor(stack.getSelectedValue()));
modelRenderer.render(blockModel, myPlayer.getHeldItemTransformData(), myPlayer.getHeldItemNormalTransformData()); modelRenderer.render(blockModel, myPlayer.getHeldItemTransformData(), myPlayer.getHeldItemNormalTransformData());
} }
modelRenderer.setAspectColor(new Vector3f(0.5f, 0.5f, 0.5f)); modelRenderer.setAspectColor(new Vector3f(0.5f, 0.5f, 0.5f));
for (var player : client.getPlayers().values()) { for (var player : client.getPlayers().values()) {
if (player.getMode() == PlayerMode.SPECTATOR) continue;
if (player.getHeldItemId() == ItemTypes.BLOCK.getId()) { if (player.getHeldItemId() == ItemTypes.BLOCK.getId()) {
modelRenderer.setAspectColor(client.getWorld().getPalette().getColor(player.getSelectedBlockValue())); modelRenderer.setAspectColor(client.getWorld().getPalette().getColor(player.getSelectedBlockValue()));
modelRenderer.render(blockModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData()); modelRenderer.render(blockModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData());
@ -268,7 +259,7 @@ public class GameRenderer {
flagModel.bind(); flagModel.bind();
for (Team team : client.getTeams().values()) { for (Team team : client.getTeams().values()) {
modelTransform.identity() modelTransform.identity()
.translate(team.getSpawnPoint().x() - 0.25f, team.getSpawnPoint().y(), team.getSpawnPoint().z() - 0.25f); .translate(team.getSpawnPoint());
modelTransform.normal(normalTransform); modelTransform.normal(normalTransform);
modelRenderer.setAspectColor(team.getColor()); modelRenderer.setAspectColor(team.getColor());
modelRenderer.render(flagModel, modelTransform, normalTransform); modelRenderer.render(flagModel, modelTransform, normalTransform);
@ -280,7 +271,7 @@ public class GameRenderer {
// GUI rendering // GUI rendering
guiRenderer.start(); guiRenderer.start();
guiRenderer.drawNameplates(myPlayer, camera.getViewTransformData(), perspectiveTransform.get(new float[16])); guiRenderer.drawNameplates(myPlayer, camera.getViewTransformData(), perspectiveTransform.get(new float[16]));
guiRenderer.drawNvg(screenWidth, screenHeight, client); guiRenderer.drawNvg(screenWidth, screenHeight, myPlayer, client.getChat());
guiRenderer.end(); guiRenderer.end();
glfwSwapBuffers(windowHandle); glfwSwapBuffers(windowHandle);
@ -288,15 +279,15 @@ public class GameRenderer {
} }
public void freeWindow() { public void freeWindow() {
rifleModel.free(); if (rifleModel != null) rifleModel.free();
smgModel.free(); if (smgModel != null) smgModel.free();
flagModel.free(); if (flagModel != null) flagModel.free();
bulletModel.free(); if (bulletModel != null) bulletModel.free();
playerModel.free(); if (playerModel != null) playerModel.free();
blockModel.free(); if (blockModel != null) blockModel.free();
modelRenderer.free(); if (modelRenderer != null) modelRenderer.free();
guiRenderer.free(); if (guiRenderer != null) guiRenderer.free();
chunkRenderer.free(); if (chunkRenderer != null) chunkRenderer.free();
GL.destroy(); GL.destroy();
Callbacks.glfwFreeCallbacks(windowHandle); Callbacks.glfwFreeCallbacks(windowHandle);
glfwSetErrorCallback(null); glfwSetErrorCallback(null);

View File

@ -1,6 +0,0 @@
package nl.andrewl.aos2_client.render;
public record TransformData(
float[] tx,
float[] norm
) {}

View File

@ -2,6 +2,8 @@ package nl.andrewl.aos2_client.render.chunk;
import nl.andrewl.aos_core.model.world.Chunk; import nl.andrewl.aos_core.model.world.Chunk;
import nl.andrewl.aos_core.model.world.World; import nl.andrewl.aos_core.model.world.World;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.lwjgl.opengl.GL46.*; import static org.lwjgl.opengl.GL46.*;
@ -9,6 +11,8 @@ import static org.lwjgl.opengl.GL46.*;
* Represents a 3d mesh for a chunk. * Represents a 3d mesh for a chunk.
*/ */
public class ChunkMesh { public class ChunkMesh {
private static final Logger log = LoggerFactory.getLogger(ChunkMesh.class);
private final int vboId; private final int vboId;
private final int vaoId; private final int vaoId;
private final int eboId; private final int eboId;
@ -45,11 +49,20 @@ public class ChunkMesh {
* Generates and loads this chunk's mesh into the allocated OpenGL buffers. * Generates and loads this chunk's mesh into the allocated OpenGL buffers.
*/ */
private void loadMesh(ChunkMeshGenerator meshGenerator) { private void loadMesh(ChunkMeshGenerator meshGenerator) {
// long start = System.nanoTime();
var meshData = meshGenerator.generateMesh(chunk, world); var meshData = meshGenerator.generateMesh(chunk, world);
// double dur = (System.nanoTime() - start) / 1_000_000.0;
this.indexCount = meshData.indexBuffer().limit(); this.indexCount = meshData.indexBuffer().limit();
// log.debug(
// "Generated mesh for chunk ({}, {}, {}) in {} ms. {} vertices and {} indices.",
// chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z,
// dur,
// meshData.vertexBuffer().limit() / 9, indexCount
// );
glBindBuffer(GL_ARRAY_BUFFER, vboId); glBindBuffer(GL_ARRAY_BUFFER, vboId);
glBufferData(GL_ARRAY_BUFFER, meshData.vertexBuffer(), GL_STATIC_DRAW); glBufferData(GL_ARRAY_BUFFER, meshData.vertexBuffer(), GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, meshData.indexBuffer(), GL_STATIC_DRAW); glBufferData(GL_ELEMENT_ARRAY_BUFFER, meshData.indexBuffer(), GL_STATIC_DRAW);
} }

View File

@ -1,19 +1,16 @@
package nl.andrewl.aos2_client.render.gui; package nl.andrewl.aos2_client.render.gui;
import nl.andrewl.aos2_client.Camera; import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos2_client.Client; import nl.andrewl.aos2_client.model.Chat;
import nl.andrewl.aos2_client.model.ClientPlayer; import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos2_client.model.OtherPlayer; import nl.andrewl.aos2_client.model.OtherPlayer;
import nl.andrewl.aos2_client.render.ShaderProgram; import nl.andrewl.aos2_client.render.ShaderProgram;
import nl.andrewl.aos2_client.sound.SoundSource;
import nl.andrewl.aos_core.FileUtils; import nl.andrewl.aos_core.FileUtils;
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.item.BlockItem; import nl.andrewl.aos_core.model.item.BlockItem;
import nl.andrewl.aos_core.model.item.BlockItemStack; import nl.andrewl.aos_core.model.item.BlockItemStack;
import nl.andrewl.aos_core.model.item.Gun; import nl.andrewl.aos_core.model.item.Gun;
import nl.andrewl.aos_core.model.item.GunItemStack; import nl.andrewl.aos_core.model.item.GunItemStack;
import nl.andrewl.aos_core.model.world.Hit;
import org.joml.Matrix4f; import org.joml.Matrix4f;
import org.lwjgl.BufferUtils; import org.lwjgl.BufferUtils;
import org.lwjgl.nanovg.NVGColor; import org.lwjgl.nanovg.NVGColor;
@ -197,12 +194,10 @@ public class GuiRenderer {
glUniform1i(namePlateTextureSamplerUniform, 0); glUniform1i(namePlateTextureSamplerUniform, 0);
glUniformMatrix4fv(namePlateViewTransformUniform, false, viewTransformData); glUniformMatrix4fv(namePlateViewTransformUniform, false, viewTransformData);
glUniformMatrix4fv(namePlatePerspectiveTransformUniform, false, perspectiveTransformData); glUniformMatrix4fv(namePlatePerspectiveTransformUniform, false, perspectiveTransformData);
// Show nameplates from farther away if we're in creative/spectator.
float nameplateRadius = myPlayer.getMode() == PlayerMode.NORMAL ? 50 : 200;
for (var entry : playerNamePlates.entrySet()) { for (var entry : playerNamePlates.entrySet()) {
OtherPlayer player = entry.getKey(); OtherPlayer player = entry.getKey();
// There are some scenarios where we skip rendering the name. // Skip rendering far-away nameplates.
if (player.getPosition().distance(myPlayer.getPosition()) > nameplateRadius || player.getMode() == PlayerMode.SPECTATOR) continue; if (player.getPosition().distance(myPlayer.getPosition()) > 50) continue;
GuiTexture texture = entry.getValue(); GuiTexture texture = entry.getValue();
float aspectRatio = (float) texture.getHeight() / (float) texture.getWidth(); float aspectRatio = (float) texture.getHeight() / (float) texture.getWidth();
transformMatrix.identity() transformMatrix.identity()
@ -221,24 +216,14 @@ public class GuiRenderer {
shaderProgram.use(); shaderProgram.use();
} }
public void drawNvg(float width, float height, Client client) { public void drawNvg(float width, float height, ClientPlayer player, Chat chat) {
nvgBeginFrame(vgId, width, height, width / height); nvgBeginFrame(vgId, width, height, width / height);
nvgSave(vgId); nvgSave(vgId);
boolean scopeEnabled = client.getInputHandler().getNormalContext().isScopeEnabled(); drawCrosshair(width, height);
PlayerMode mode = client.getMyPlayer().getMode(); drawChat(width, height, chat);
drawCrosshair(width, height, scopeEnabled); drawHealthBar(width, height, player);
drawHeldItemStackInfo(width, height, client.getMyPlayer()); drawHeldItemStackInfo(width, height, player);
if (!scopeEnabled) {
drawChat(width, height, client);
if (mode == PlayerMode.NORMAL) drawHealthBar(width, height, client.getMyPlayer());
}
if (client.getInputHandler().getNormalContext().isDebugEnabled()) {
drawDebugInfo(width, height, client);
}
if (client.getInputHandler().isExitMenuContextActive()) {
drawExitMenu(width, height);
}
nvgRestore(vgId); nvgRestore(vgId);
nvgEndFrame(vgId); nvgEndFrame(vgId);
@ -263,40 +248,34 @@ public class GuiRenderer {
shaderProgram.free(); shaderProgram.free();
} }
private void drawCrosshair(float w, float h, boolean scopeEnabled) { private void drawCrosshair(float w, float h) {
float cx = w / 2f; float cx = w / 2f;
float cy = h / 2f; float cy = h / 2f;
float size = 20f;
if (scopeEnabled) {
size = 3f;
nvgStrokeColor(vgId, GuiUtils.rgba(1, 0, 0, 0.5f, colorA));
} else {
nvgStrokeColor(vgId, GuiUtils.rgba(1, 1, 1, 0.25f, colorA));
}
nvgStrokeColor(vgId, GuiUtils.rgba(1, 1, 1, 0.25f, colorA));
nvgBeginPath(vgId); nvgBeginPath(vgId);
nvgMoveTo(vgId, cx - size / 2, cy); nvgMoveTo(vgId, cx - 10, cy);
nvgLineTo(vgId, cx + size / 2, cy); nvgLineTo(vgId, cx + 10, cy);
nvgMoveTo(vgId, cx, cy - size / 2); nvgMoveTo(vgId, cx, cy - 10);
nvgLineTo(vgId, cx, cy + size / 2); nvgLineTo(vgId, cx, cy + 10);
nvgStroke(vgId); nvgStroke(vgId);
} }
private void drawHealthBar(float w, float h, ClientPlayer player) { private void drawHealthBar(float w, float h, ClientPlayer player) {
nvgFillColor(vgId, GuiUtils.rgba(0.6f, 0, 0, 1, colorA)); nvgFillColor(vgId, GuiUtils.rgba(1, 0, 0, 1, colorA));
nvgBeginPath(vgId); nvgBeginPath(vgId);
nvgRect(vgId, w - 170, h - 100, 100, 20); nvgRect(vgId, 20, h - 60, 100, 20);
nvgFill(vgId); nvgFill(vgId);
nvgFillColor(vgId, GuiUtils.rgba(0, 0.6f, 0, 1, colorA)); nvgFillColor(vgId, GuiUtils.rgba(0, 1, 0, 1, colorA));
nvgBeginPath(vgId); nvgBeginPath(vgId);
nvgRect(vgId, w - 170, h - 100, 100 * player.getHealth(), 20); nvgRect(vgId, 20, h - 60, 100 * player.getHealth(), 20);
nvgFill(vgId); nvgFill(vgId);
nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA)); nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA));
nvgFontSize(vgId, 12f); nvgFontSize(vgId, 12f);
nvgFontFaceId(vgId, jetbrainsMonoFont); nvgFontFaceId(vgId, jetbrainsMonoFont);
nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
nvgText(vgId, w - 165, h - 95, String.format("%.2f / 1.00", player.getHealth())); nvgText(vgId, 20, h - 30, String.format("%.2f / 1.00 HP", player.getHealth()));
} }
private void drawHeldItemStackInfo(float w, float h, ClientPlayer player) { private void drawHeldItemStackInfo(float w, float h, ClientPlayer player) {
@ -337,81 +316,34 @@ public class GuiRenderer {
nvgFontFaceId(vgId, jetbrainsMonoFont); nvgFontFaceId(vgId, jetbrainsMonoFont);
nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
nvgText(vgId, w - 140, h - 30, String.format("%d / %d Blocks", stack.getAmount(), block.getMaxAmount())); nvgText(vgId, w - 140, h - 30, String.format("%d / %d Blocks", stack.getAmount(), block.getMaxAmount()));
nvgText(vgId, w - 140, h - 14, String.format("Selected value: %d", stack.getSelectedValue()));
} }
private void drawChat(float w, float h, Client client) { private void drawChat(float w, float h, Chat chat) {
var chat = client.getChat(); float chatWidth = w / 3;
float chatHeight = h / 4;
nvgFillColor(vgId, GuiUtils.rgba(0, 0, 0, 0.25f, colorA));
nvgBeginPath(vgId);
nvgRect(vgId, 0, 0, chatWidth, chatHeight);
nvgFill(vgId);
nvgFontSize(vgId, 12f); nvgFontSize(vgId, 12f);
nvgFontFaceId(vgId, jetbrainsMonoFont); nvgFontFaceId(vgId, jetbrainsMonoFont);
nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
float y = h - 16 - 12; float y = chatHeight - 12;
for (var msg : chat.getMessages()) { for (var msg : chat.getMessages()) {
if (msg.author().equals("_ANNOUNCE")) { if (msg.author().equals("_ANNOUNCE")) {
nvgFillColor(vgId, GuiUtils.rgba(0.7f, 0, 0, 1, colorA)); nvgFillColor(vgId, GuiUtils.rgba(0.7f, 0, 0, 1, colorA));
nvgText(vgId, 5, y, msg.message()); nvgText(vgId, 5, y, msg.message());
} else if (msg.author().equals("_PRIVATE")) { } else if (msg.author().equals("_PRIVATE")) {
nvgFillColor(vgId, GuiUtils.rgba(0.6f, 0.6f, 0.6f, 1, colorA)); nvgFillColor(vgId, GuiUtils.rgba(0.3f, 0.3f, 0.3f, 1, colorA));
nvgText(vgId, 5, y, msg.message()); nvgText(vgId, 5, y, msg.message());
} else { } else {
nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA)); nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA));
nvgText(vgId, 5, y, msg.author() + ": " + msg.message()); nvgText(vgId, 5, y, msg.author() + ": " + msg.message());
} }
y -= 16; y -= 16;
} }
var input = client.getInputHandler();
if (input.isChattingContextActive()) {
nvgFillColor(vgId, GuiUtils.rgba(0, 0, 0, 0.5f, colorA));
nvgBeginPath(vgId);
nvgRect(vgId, 0, h - 16, w, 16);
nvgFill(vgId);
nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA));
nvgText(vgId, 5, h - 14, "> " + input.getChattingContext().getChatBufferText() + "_");
}
}
private void drawDebugInfo(float w, float h, Client client) {
float y = h / 4 + 10;
nvgFontSize(vgId, 12f);
nvgFontFaceId(vgId, jetbrainsMonoFont);
nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA));
var pos = client.getMyPlayer().getPosition();
nvgText(vgId, 5, y, String.format("Pos: x=%.3f, y=%.3f, z=%.3f", pos.x, pos.y, pos.z));
y += 12;
var vel = client.getMyPlayer().getVelocity();
nvgText(vgId, 5, y, String.format("Vel: x=%.3f, y=%.3f, z=%.3f, speed=%.3f", vel.x, vel.y, vel.z, vel.length()));
y += 12;
var view = client.getMyPlayer().getOrientation();
nvgText(vgId, 5, y, String.format("View: horizontal=%.3f, vertical=%.3f", Math.toDegrees(view.x), Math.toDegrees(view.y)));
y += 12;
var soundSources = client.getSoundManager().getSources();
int activeCount = (int) soundSources.stream().filter(SoundSource::isPlaying).count();
nvgText(vgId, 5, y, String.format("Sounds: %d / %d playing", activeCount, soundSources.size()));
y += 12;
nvgText(vgId, 5, y, String.format("Projectiles: %d", client.getProjectiles().size()));
y += 12;
nvgText(vgId, 5, y, String.format("Players: %d", client.getPlayers().size()));
y += 12;
nvgText(vgId, 5, y, String.format("Chunks: %d", client.getWorld().getChunkMap().size()));
y += 12;
Hit hit = client.getWorld().getLookingAtPos(client.getMyPlayer().getEyePosition(), client.getMyPlayer().getViewVector(), 50);
if (hit != null) {
nvgText(vgId, 5, y, String.format("Looking at: x=%d, y=%d, z=%d", hit.pos().x, hit.pos().y, hit.pos().z));
}
}
private void drawExitMenu(float width, float height) {
nvgFillColor(vgId, GuiUtils.rgba(0, 0, 0, 0.5f, colorA));
nvgBeginPath(vgId);
nvgRect(vgId, 0, 0, width, height);
nvgFill(vgId);
nvgFontSize(vgId, 12f);
nvgFontFaceId(vgId, jetbrainsMonoFont);
nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA));
nvgText(vgId, width / 2f, height / 2f, "Press ESC to quit. Press any other key to return to the game.");
} }
} }

View File

@ -1,16 +1,19 @@
package nl.andrewl.aos2_client.sound; package nl.andrewl.aos2_client.sound;
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.world.World;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.lwjgl.openal.AL; import org.lwjgl.openal.AL;
import org.lwjgl.openal.ALC; import org.lwjgl.openal.ALC;
import org.lwjgl.openal.ALCCapabilities; import org.lwjgl.openal.ALCCapabilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import java.util.*; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
@ -21,6 +24,8 @@ import static org.lwjgl.openal.ALC10.*;
* Main class for managing the OpenAL audio interface. * Main class for managing the OpenAL audio interface.
*/ */
public class SoundManager { public class SoundManager {
private static final Logger log = LoggerFactory.getLogger(SoundManager.class);
private static final int SOURCE_COUNT = 32; private static final int SOURCE_COUNT = 32;
private final long alContext; private final long alContext;
@ -75,8 +80,6 @@ public class SoundManager {
load("block_break_1", "sound/m_block_break_1.wav"); load("block_break_1", "sound/m_block_break_1.wav");
load("block_place_1", "sound/m_block_place_1.wav"); load("block_place_1", "sound/m_block_place_1.wav");
load("chat", "sound/chat.wav"); load("chat", "sound/chat.wav");
load("hit_1", "sound/m_hit_1.wav");
load("hit_2", "sound/m_hit_2.wav");
} }
public void load(String name, String resource) { public void load(String name, String resource) {
@ -99,7 +102,7 @@ public class SoundManager {
public void play(String soundName, float gain, Vector3f position, Vector3f velocity) { public void play(String soundName, float gain, Vector3f position, Vector3f velocity) {
Integer bufferId = getSoundBuffer(soundName); Integer bufferId = getSoundBuffer(soundName);
if (bufferId == null) { if (bufferId == null) {
System.err.printf("Attempted to play unknown sound \"%s\".%n", soundName); log.warn("Attempted to play unknown sound \"{}\"", soundName);
} else { } else {
SoundSource source = getNextAvailableSoundSource(); SoundSource source = getNextAvailableSoundSource();
if (source != null) { if (source != null) {
@ -108,7 +111,7 @@ public class SoundManager {
source.setGain(gain); source.setGain(gain);
source.play(bufferId); source.play(bufferId);
} else { } else {
System.err.printf("No sound sources available to play sound \"%s\".%n", soundName); log.warn("Couldn't get an available sound source to play sound \"{}\"", soundName);
} }
} }
} }
@ -117,9 +120,8 @@ public class SoundManager {
play(soundName, gain, position, new Vector3f(0, 0, 0)); play(soundName, gain, position, new Vector3f(0, 0, 0));
} }
public void playWalkingSounds(Player player, World world, long now) { public void playWalkingSounds(Player player, long now) {
// Don't play sounds for players who are still, non-normal mode, or not on the ground. if (player.getVelocity().length() <= 0) return;
if (player.getVelocity().length() <= 0 || player.getMode() != PlayerMode.NORMAL || !player.isGrounded(world)) return;
long lastSoundAt = lastPlayerWalkingSounds.computeIfAbsent(player, p -> 0L); long lastSoundAt = lastPlayerWalkingSounds.computeIfAbsent(player, p -> 0L);
long delay = 500; // Delay in ms between footfalls. long delay = 500; // Delay in ms between footfalls.
if (player.getVelocity().length() > 5) delay -= 150; if (player.getVelocity().length() > 5) delay -= 150;
@ -131,10 +133,6 @@ public class SoundManager {
} }
} }
public Collection<SoundSource> getSources() {
return Collections.unmodifiableCollection(availableSources);
}
private SoundSource getNextAvailableSoundSource() { private SoundSource getNextAvailableSoundSource() {
for (var source : availableSources) { for (var source : availableSources) {
if (!source.isPlaying()) return source; if (!source.isPlaying()) return source;

View File

@ -1,29 +0,0 @@
package nl.andrewl.aos2_client.util;
import nl.andrewl.aos_core.Pair;
import org.lwjgl.BufferUtils;
import java.nio.IntBuffer;
import static org.lwjgl.glfw.GLFW.*;
public class WindowUtils {
public static Pair<Integer, Integer> getSize(long id) {
IntBuffer wBuf = BufferUtils.createIntBuffer(1);
IntBuffer hBuf = BufferUtils.createIntBuffer(1);
glfwGetWindowSize(id, wBuf, hBuf);
return new Pair<>(wBuf.get(0), hBuf.get(0));
}
public static void captureCursor(long id) {
glfwSetInputMode(id, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
var size = WindowUtils.getSize(id);
glfwSetCursorPos(id, size.first() / 2.0, size.second() / 2.0);
}
public static void freeCursor(long id) {
glfwSetInputMode(id, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
var size = WindowUtils.getSize(id);
glfwSetCursorPos(id, size.first() / 2.0, size.second() / 2.0);
}
}

View File

@ -1,10 +0,0 @@
# Ace of Shades 2 Client Configuration
# Settings for input.
input:
mouseSensitivity: 0.005
# Settings for display.
display:
fullscreen: true
captureCursor: true
fov: 80

View File

@ -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.5.0</version> <version>1.1.0</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@ -47,6 +47,18 @@
<artifactId>snakeyaml</artifactId> <artifactId>snakeyaml</artifactId>
<version>1.30</version> <version>1.30</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.18.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->

View File

@ -26,37 +26,28 @@ public final class Net {
private static final Serializer serializer = new Serializer(); private static final Serializer serializer = new Serializer();
static { static {
int i = 1; serializer.registerType(1, ConnectRequestMessage.class);
// Basic protocol messages. serializer.registerType(2, ConnectAcceptMessage.class);
serializer.registerType(i++, ConnectRequestMessage.class); serializer.registerType(3, ConnectRejectMessage.class);
serializer.registerType(i++, ConnectAcceptMessage.class); serializer.registerType(4, DatagramInit.class);
serializer.registerType(i++, ConnectRejectMessage.class); serializer.registerType(5, ChunkHashMessage.class);
serializer.registerType(i++, DatagramInit.class); serializer.registerType(6, ChunkDataMessage.class);
serializer.registerType(7, ChunkUpdateMessage.class);
// World messages. serializer.registerType(8, ClientInputState.class);
serializer.registerType(i++, ChunkHashMessage.class); serializer.registerType(9, ClientOrientationState.class);
serializer.registerType(i++, ChunkDataMessage.class); serializer.registerType(10, PlayerUpdateMessage.class);
serializer.registerType(i++, ChunkUpdateMessage.class); serializer.registerType(11, PlayerJoinMessage.class);
serializer.registerType(i++, ProjectileMessage.class); serializer.registerType(12, PlayerLeaveMessage.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(i++, new InventorySerializer()); serializer.registerTypeSerializer(13, new InventorySerializer());
serializer.registerTypeSerializer(i++, new ItemStackSerializer()); serializer.registerTypeSerializer(14, new ItemStackSerializer());
serializer.registerType(15, InventorySelectedStackMessage.class);
serializer.registerType(i++, SoundMessage.class); serializer.registerType(16, 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);
} }
public static ExtendedDataInputStream getInputStream(InputStream in) { public static ExtendedDataInputStream getInputStream(InputStream in) {

View File

@ -1,6 +1,5 @@
package nl.andrewl.aos_core.config; package nl.andrewl.aos_core.config;
import nl.andrewl.aos_core.FileUtils;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import java.io.IOException; import java.io.IOException;
@ -20,11 +19,10 @@ public final class Config {
* @param paths The paths to load from. * @param paths The paths to load from.
* @param fallback A default configuration object to use if no config could * @param fallback A default configuration object to use if no config could
* be loaded from any of the paths. * be loaded from any of the paths.
* @param defaultConfigFile The default config file resource to save.
* @return The configuration object. * @return The configuration object.
* @param <T> The type of the configuration object. * @param <T> The type of the configuration object.
*/ */
public static <T> T loadConfig(Class<T> configType, List<Path> paths, T fallback, String defaultConfigFile) { public static <T> T loadConfig(Class<T> configType, List<Path> paths, T fallback) {
for (var path : paths) { for (var path : paths) {
if (Files.exists(path) && Files.isRegularFile(path) && Files.isReadable(path)) { if (Files.exists(path) && Files.isRegularFile(path) && Files.isReadable(path)) {
try (var reader = Files.newBufferedReader(path)) { try (var reader = Files.newBufferedReader(path)) {
@ -34,21 +32,27 @@ public final class Config {
} }
} }
} }
Path outputPath = paths.size() > 0 ? paths.get(0) : Path.of("config.yaml");
try (var writer = Files.newBufferedWriter(outputPath)) {
writer.write(FileUtils.readClasspathFile(defaultConfigFile));
} catch (IOException e) {
e.printStackTrace();
}
return fallback; return fallback;
} }
public static <T> T loadConfig(Class<T> configType, List<Path> paths) {
var cfg = loadConfig(configType, paths, null);
if (cfg == null) {
throw new RuntimeException("Could not load config from any of the supplied paths.");
}
return cfg;
}
public static <T> T loadConfig(Class<T> configType, T fallback, Path... paths) {
return loadConfig(configType, List.of(paths), fallback);
}
public static List<Path> getCommonConfigPaths() { public static List<Path> getCommonConfigPaths() {
List<Path> paths = new ArrayList<>(); List<Path> paths = new ArrayList<>();
paths.add(Path.of("config.yaml"));
paths.add(Path.of("config.yml"));
paths.add(Path.of("configuration.yaml")); paths.add(Path.of("configuration.yaml"));
paths.add(Path.of("configuration.yml")); paths.add(Path.of("configuration.yml"));
paths.add(Path.of("config.yaml"));
paths.add(Path.of("config.yml"));
paths.add(Path.of("cfg.yaml")); paths.add(Path.of("cfg.yaml"));
paths.add(Path.of("cfg.yml")); paths.add(Path.of("cfg.yml"));
return paths; return paths;

View File

@ -1,10 +1,10 @@
package nl.andrewl.aos_core.model; package nl.andrewl.aos_core.model;
import nl.andrewl.aos_core.Directions;
import nl.andrewl.aos_core.MathUtils; import nl.andrewl.aos_core.MathUtils;
import nl.andrewl.aos_core.model.world.World;
import org.joml.*;
import org.joml.Math; import org.joml.Math;
import org.joml.Vector2f;
import org.joml.Vector3f;
import org.joml.Vector3i;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -75,13 +75,7 @@ public class Player {
*/ */
protected Team team; protected Team team;
/** public Player(int id, String username, Team team) {
* The mode that the player is in, which dictates how they can move and/or
* interact with the world.
*/
protected PlayerMode mode;
public Player(int id, String username, Team team, PlayerMode mode) {
this.position = new Vector3f(); this.position = new Vector3f();
this.velocity = new Vector3f(); this.velocity = new Vector3f();
this.orientation = new Vector2f(); this.orientation = new Vector2f();
@ -89,11 +83,10 @@ public class Player {
this.id = id; this.id = id;
this.username = username; this.username = username;
this.team = team; this.team = team;
this.mode = mode;
} }
public Player(int id, String username) { public Player(int id, String username) {
this(id, username, null, PlayerMode.NORMAL); this(id, username, null);
} }
public Vector3f getPosition() { public Vector3f getPosition() {
@ -146,14 +139,6 @@ public class Player {
this.team = team; this.team = team;
} }
public PlayerMode getMode() {
return mode;
}
public void setMode(PlayerMode mode) {
this.mode = mode;
}
public Vector3f getViewVector() { public Vector3f getViewVector() {
return viewVector; return viewVector;
} }
@ -183,48 +168,6 @@ public class Player {
return crouching ? HEIGHT_CROUCH : HEIGHT; return crouching ? HEIGHT_CROUCH : HEIGHT;
} }
/**
* Gets a transformation that transforms a position to the position of the
* player's held gun.
* @return The gun transform.
*/
public Matrix4f getHeldItemTransform() {
return new Matrix4f()
.translate(position)
.rotate(orientation.x + (float) Math.PI, Directions.UPf)
.translate(-0.35f, getEyeHeight() - 0.4f, 0.35f);
}
/**
* Gets the list of all spaces occupied by a player's position, in the
* horizontal XZ plane. This can be between 1 and 4 spaces, depending on
* if the player's position is overlapping with a few blocks.
* @param pos The position.
* @return The list of 2d positions occupied.
*/
private List<Vector2i> getHorizontalSpaceOccupied(Vector3f pos) {
// Get the list of 2d x,z coordinates that we overlap with.
List<Vector2i> points = new ArrayList<>(4); // Due to the size of radius, there can only be a max of 4 blocks.
int minX = (int) Math.floor(pos.x - RADIUS);
int minZ = (int) Math.floor(pos.z - RADIUS);
int maxX = (int) Math.floor(pos.x + RADIUS);
int maxZ = (int) Math.floor(pos.z + RADIUS);
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
points.add(new Vector2i(x, z));
}
}
return points;
}
public boolean isGrounded(World world) {
// Player must be flat on the top of a block.
if (Math.floor(position.y) != position.y) return false;
// Check to see if there's a block under any of the spaces the player is over.
return getHorizontalSpaceOccupied(position).stream()
.anyMatch(point -> world.getBlockAt(point.x, position.y - 0.1f, point.y) != 0);
}
public List<Vector3i> getBlockSpaceOccupied() { public List<Vector3i> getBlockSpaceOccupied() {
float playerBodyMinZ = position.z - RADIUS; float playerBodyMinZ = position.z - RADIUS;
float playerBodyMaxZ = position.z + RADIUS; float playerBodyMaxZ = position.z + RADIUS;

View File

@ -1,27 +0,0 @@
package nl.andrewl.aos_core.model;
/**
* Represents the different modes that a player can be in.
* <ul>
* <li>
* In normal mode, the player acts as a usual competitive player that
* has an inventory, can shoot weapons, and must traverse the world by
* walking around.
* </li>
* <li>
* In creative mode, the player can fly, but still collides with the
* world's objects. The player also has unlimited ammunition and
* blocks.
* </li>
* <li>
* In spectator mode, the player can fly freely throughout the world,
* limited by nothing. The player can't interact with the world in any
* way other than simply observing it through sight and sound.
* </li>
* </ul>
*/
public enum PlayerMode {
NORMAL,
CREATIVE,
SPECTATOR
}

View File

@ -31,7 +31,6 @@ public class Inventory {
} }
public ItemStack getSelectedItemStack() { public ItemStack getSelectedItemStack() {
if (itemStacks.isEmpty()) return null;
return itemStacks.get(selectedIndex); return itemStacks.get(selectedIndex);
} }
@ -48,13 +47,6 @@ public class Inventory {
return Optional.empty(); return Optional.empty();
} }
public int getIndex(ItemStack stack) {
for (int i = 0; i < itemStacks.size(); i++) {
if (itemStacks.get(i).equals(stack)) return i;
}
return -1;
}
public byte getSelectedBlockValue() { public byte getSelectedBlockValue() {
for (var stack : itemStacks) { for (var stack : itemStacks) {
if (stack instanceof BlockItemStack b) { if (stack instanceof BlockItemStack b) {
@ -63,9 +55,4 @@ public class Inventory {
} }
return 1; return 1;
} }
public void clear() {
itemStacks.clear();
selectedIndex = -1;
}
} }

View File

@ -14,7 +14,7 @@ public class Ak47 extends Gun {
0.1f, 0.1f,
1.2f, 1.2f,
0.4f, 0.4f,
0.1f, 30f,
true true
); );
} }

View File

@ -7,14 +7,14 @@ public class Rifle extends Gun {
super( super(
id, id,
"Rifle", "Rifle",
6, 5,
8, 8,
1, 1,
0.98f, 0.97f,
0.8f, 0.8f,
2.5f, 2.5f,
0.8f, 0.8f,
0.2f, 50f,
false false
); );
} }

View File

@ -7,14 +7,14 @@ public class Winchester extends Gun {
super( super(
id, id,
"Winchester", "Winchester",
10,
6, 6,
4, 4,
4,
0.85f, 0.85f,
0.75f, 0.75f,
2.5f, 2.5f,
0.3f, 0.3f,
0.33f, 60f,
false false
); );
} }

View File

@ -17,7 +17,7 @@ import java.util.Map;
* that players can interact in. * that players can interact in.
*/ */
public class World { public class World {
private static final float DELTA = 0.001f; private static final float DELTA = 0.01f;
protected final Map<Vector3ic, Chunk> chunkMap = new HashMap<>(); protected final Map<Vector3ic, Chunk> chunkMap = new HashMap<>();
protected ColorPalette palette; protected ColorPalette palette;
@ -142,12 +142,6 @@ public class World {
return chunkMap.values().stream().mapToInt(c -> c.getPosition().z * Chunk.SIZE + Chunk.SIZE - 1).max().orElse(0); return chunkMap.values().stream().mapToInt(c -> c.getPosition().z * Chunk.SIZE + Chunk.SIZE - 1).max().orElse(0);
} }
public boolean containsPoint(Vector3i pos) {
return pos.x >= getMinX() && pos.x < getMaxX() &&
pos.y >= getMinY() && pos.y < getMaxY() &&
pos.z >= getMinZ() && pos.z < getMaxZ();
}
/** /**
* Clears all data from the world. * Clears all data from the world.
*/ */
@ -170,12 +164,8 @@ public class World {
public Hit getLookingAtPos(Vector3f eyePos, Vector3f eyeDir, float limit) { public Hit getLookingAtPos(Vector3f eyePos, Vector3f eyeDir, float limit) {
if (eyeDir.lengthSquared() == 0 || limit <= 0) return null; if (eyeDir.lengthSquared() == 0 || limit <= 0) return null;
Vector3f pos = new Vector3f(eyePos); Vector3f pos = new Vector3f(eyePos);
Vector3f previousPos = new Vector3f();
while (pos.distance(eyePos) < limit) { while (pos.distance(eyePos) < limit) {
previousPos.set(pos);
stepToNextBlock(pos, eyeDir); stepToNextBlock(pos, eyeDir);
// If for some reason we couldn't advance to the next block, exit null, so we don't infinitely loop.
if (pos.equals(previousPos)) return null;
if (getBlockAt(pos) > 0) { if (getBlockAt(pos) > 0) {
Vector3i hitPos = new Vector3i( Vector3i hitPos = new Vector3i(
(int) Math.floor(pos.x), (int) Math.floor(pos.x),
@ -244,7 +234,7 @@ public class World {
// Testing code! // Testing code!
if (diff == 0) { if (diff == 0) {
System.out.printf("n = %.8f, nextValue = %.8f, floor(n) - DELTA = %.8f%n", n, nextValue, Math.floor(n) - DELTA); System.out.printf("n = %.8f, nextValue = %.8f, floor(n) - DELTA = %.8f%n", n, nextValue, Math.floor(n) - DELTA);
return Float.MAX_VALUE; throw new RuntimeException("EEK");
} }
return Math.abs(diff / dir); return Math.abs(diff / dir);
} }

View File

@ -1,6 +1,5 @@
package nl.andrewl.aos_core.net.client; package nl.andrewl.aos_core.net.client;
import nl.andrewl.aos_core.model.Player;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
/** /**
@ -12,8 +11,4 @@ import nl.andrewl.record_net.Message;
public record ClientOrientationState( public record ClientOrientationState(
int clientId, int clientId,
float x, float y float x, float y
) implements Message { ) implements Message {}
public static ClientOrientationState fromPlayer(Player player) {
return new ClientOrientationState(player.getId(), player.getOrientation().x, player.getOrientation().y);
}
}

View File

@ -1,11 +0,0 @@
package nl.andrewl.aos_core.net.client;
import nl.andrewl.record_net.Message;
/**
* A message that the server sends to clients, to tell them to update their
* player's orientation according to a recoil event.
*/
public record ClientRecoilMessage(
float dx, float dy
) implements Message {}

View File

@ -1,7 +1,6 @@
package nl.andrewl.aos_core.net.client; package nl.andrewl.aos_core.net.client;
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.item.ItemTypes; import nl.andrewl.aos_core.model.item.ItemTypes;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
@ -16,15 +15,13 @@ public record PlayerJoinMessage(
float ox, float oy, float ox, float oy,
boolean crouching, boolean crouching,
int selectedItemId, int selectedItemId,
byte selectedBlockValue, byte selectedBlockValue
PlayerMode mode
) implements Message { ) implements Message {
public Player toPlayer() { public Player toPlayer() {
Player p = new Player(id, username); Player p = new Player(id, username);
p.getPosition().set(px, py, pz); p.getPosition().set(px, py, pz);
p.getVelocity().set(vx, vy, vz); p.getVelocity().set(vx, vy, vz);
p.getOrientation().set(ox, oy); p.getOrientation().set(ox, oy);
p.setMode(mode);
return p; return p;
} }
} }

View File

@ -1,13 +0,0 @@
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 {}

View File

@ -1,7 +1,6 @@
package nl.andrewl.aos_core.net.client; package nl.andrewl.aos_core.net.client;
import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.PlayerMode;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
/** /**
@ -17,8 +16,7 @@ public record PlayerUpdateMessage(
float vx, float vy, float vz, float vx, float vy, float vz,
float ox, float oy, float ox, float oy,
boolean crouching, boolean crouching,
int selectedItemId, int selectedItemId
PlayerMode mode
) implements Message { ) implements Message {
public void apply(Player p) { public void apply(Player p) {
@ -26,6 +24,5 @@ public record PlayerUpdateMessage(
p.getVelocity().set(vx, vy, vz); p.getVelocity().set(vx, vy, vz);
p.getOrientation().set(ox, oy); p.getOrientation().set(ox, oy);
p.setCrouching(crouching); p.setCrouching(crouching);
p.setMode(mode);
} }
} }

View File

@ -2,10 +2,4 @@ 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 {}

View File

@ -4,6 +4,6 @@ appender.console.name = STDOUT
appender.console.layout.type = PatternLayout appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n appender.console.layout.pattern = [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n
rootLogger.level = info rootLogger.level = debug
rootLogger.appenderRefs = stdout rootLogger.appenderRefs = stdout
rootLogger.appenderRef.stdout.ref = STDOUT rootLogger.appenderRef.stdout.ref = STDOUT

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,30 +0,0 @@
#!/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

View File

@ -1,42 +0,0 @@
# 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!"

View File

@ -11,9 +11,8 @@
<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.2</javafx.version> <javafx.version>18.0.1</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>
@ -27,12 +26,6 @@
<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>
@ -54,46 +47,6 @@
<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>

View File

@ -4,10 +4,6 @@ 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;
} }

View File

@ -1,4 +0,0 @@
package nl.andrewl.aos2_launcher;
public class EditProfileController {
}

View File

@ -1,70 +0,0 @@
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();
});
}
}

View File

@ -7,40 +7,22 @@ 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);
addStylesheet(scene, "/font/fonts.css"); scene.getStylesheets().add(Launcher.class.getResource("/styles.css").toExternalForm());
addStylesheet(scene, "/styles.css");
stage.setScene(scene); stage.setScene(scene);
stage.setTitle("Ace of Shades - Launcher"); stage.setTitle("Ace of Shades 2 - 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);
} }

View File

@ -1,140 +1,11 @@
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.control.*; import javafx.scene.layout.TilePane;
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 void initialize() { public TilePane profilesTilePane;
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));
}
} }

View File

@ -1,74 +0,0 @@
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());
}
});
}
}

View File

@ -1,134 +0,0 @@
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";
}
}

View File

@ -1,177 +0,0 @@
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.");
}
}

View File

@ -1,11 +0,0 @@
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
) {}

View File

@ -1,84 +0,0 @@
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());
}
}

View File

@ -1,155 +0,0 @@
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;
}
}

View File

@ -1,8 +0,0 @@
package nl.andrewl.aos2_launcher.model;
public interface ProgressReporter {
void enableProgress();
void disableProgress();
void setActionText(String text);
void setProgress(double progress);
}

View File

@ -1,84 +0,0 @@
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;
}
}

View File

@ -1,74 +0,0 @@
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);
}
}
}
}

View File

@ -1,90 +0,0 @@
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;
}
}
}

View File

@ -1,96 +0,0 @@
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());
}
}

View File

@ -1,111 +0,0 @@
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);
}
}
}

View File

@ -1,38 +0,0 @@
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;
}
}

View File

@ -1,33 +0,0 @@
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;
}
}

View File

@ -1,29 +0,0 @@
<?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>

View File

@ -1,27 +0,0 @@
@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";
}

View File

@ -1,50 +1,24 @@
<?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 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"> <VBox xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/16"
<TabPane tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS"> maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0"
<Tab text="Profiles"> fx:controller="nl.andrewl.aos2_launcher.MainViewController"
<VBox> >
<HBox alignment="CENTER" styleClass="button-bar" VBox.vgrow="NEVER"> <MenuBar>
<Button onAction="#addProfile" text="Add Profile" /> <Menu mnemonicParsing="false" text="File">
<Button fx:id="editProfileButton" onAction="#editProfile" text="Edit Profile" /> <MenuItem mnemonicParsing="false" text="Exit"/>
<Button fx:id="removeProfileButton" onAction="#removeProfile" text="Remove Profile" /> </Menu>
</HBox> <Menu mnemonicParsing="false" text="Profiles">
<ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS"> <MenuItem mnemonicParsing="false" text="New Profile"/>
<VBox fx:id="profilesVBox" styleClass="banner-list" /> </Menu>
<Menu mnemonicParsing="false" text="Help">
<MenuItem mnemonicParsing="false" text="About"/>
</Menu>
</MenuBar>
<ScrollPane VBox.vgrow="ALWAYS">
<TilePane fx:id="profilesTilePane"/>
</ScrollPane> </ScrollPane>
</VBox> </VBox>
</Tab>
<Tab text="Servers">
<VBox>
<HBox alignment="CENTER" styleClass="button-bar" VBox.vgrow="NEVER">
<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>

View File

@ -1,31 +0,0 @@
<?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>

View File

@ -1,22 +0,0 @@
.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;
}

View File

@ -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.5.0</version> <version>1.1.0</version>
<modules> <modules>
<module>core</module> <module>core</module>
<module>server</module> <module>server</module>

33
registry/.gitignore vendored
View File

@ -1,33 +0,0 @@
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.

View File

@ -1,2 +0,0 @@
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

View File

@ -1,34 +0,0 @@
# 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.

View File

@ -1,5 +0,0 @@
#!/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

316
registry/mvnw vendored
View File

@ -1,316 +0,0 @@
#!/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 "$@"

188
registry/mvnw.cmd vendored
View File

@ -1,188 +0,0 @@
@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%

View File

@ -1,150 +0,0 @@
<?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>

View File

@ -1,15 +0,0 @@
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);
}
}

View File

@ -1,33 +0,0 @@
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();
}
}

View File

@ -1,89 +0,0 @@
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());
}
}
}

View File

@ -1,21 +0,0 @@
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);
}
}

View File

@ -1,36 +0,0 @@
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);
});
}
}

View File

@ -1,9 +0,0 @@
package nl.andrewl.aos2registryapi.dto;
public record ServerInfoPayload (
int port,
String name,
String description,
int maxPlayers,
int currentPlayers
) {}

View File

@ -1,11 +0,0 @@
package nl.andrewl.aos2registryapi.dto;
public record ServerInfoResponse (
String host,
int port,
String name,
String description,
int maxPlayers,
int currentPlayers,
long lastUpdatedAt
) {}

View File

@ -1,3 +0,0 @@
package nl.andrewl.aos2registryapi.model;
public record ServerIdentifier(String host, int port) {}

View File

@ -1,59 +0,0 @@
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;
}
}

Some files were not shown because too many files have changed in this diff Show More