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
.gitignoreREADME.md
client
core
design
launcher
pom.xml
registry

6
.gitignore vendored
View File

@ -1,13 +1,7 @@
.idea/
target/
client-builds/
client.yaml
server.yaml
# Ignore the ./config directory so that developers can put their config files
# there for server and client apps.
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).
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.
5. Run the game again to join the server and start playing!
### 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.
5. Run the game by double-clicking the `aos2-client` JAR file, or enter `java -jar aos2-client-{version}.jar` in a terminal.
## 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
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>
<artifactId>ace-of-shades-2</artifactId>
<groupId>nl.andrewl</groupId>
<version>1.5.0</version>
<version>1.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -52,14 +52,6 @@ public class Camera {
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) {
orientation.set(p.getOrientation());
}

View File

@ -1,14 +1,14 @@
package nl.andrewl.aos2_client;
import nl.andrewl.aos2_client.config.ClientConfig;
import nl.andrewl.aos2_client.config.ConnectConfig;
import nl.andrewl.aos2_client.control.InputHandler;
import nl.andrewl.aos2_client.control.*;
import nl.andrewl.aos2_client.model.Chat;
import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos2_client.model.OtherPlayer;
import nl.andrewl.aos2_client.render.GameRenderer;
import nl.andrewl.aos2_client.sound.SoundManager;
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.Team;
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.record_net.Message;
import org.joml.Vector3f;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class Client implements Runnable {
public final ConnectConfig connectConfig;
public final ClientConfig config;
private static final Logger log = LoggerFactory.getLogger(Client.class);
public static final double FPS = 60;
private final ClientConfig config;
private final CommunicationHandler communicationHandler;
private final InputHandler inputHandler;
private final Camera camera;
private GameRenderer gameRenderer;
private SoundManager soundManager;
private long lastPlayerUpdate = 0;
@ -43,19 +43,15 @@ public class Client implements Runnable {
private final Map<Integer, Projectile> projectiles;
private final Map<Integer, Team> teams;
private final Chat chat;
private final Queue<Runnable> mainThreadActions;
public Client(ClientConfig config, ConnectConfig connectConfig) {
public Client(ClientConfig config) {
this.config = config;
this.connectConfig = connectConfig;
this.camera = new Camera();
this.players = new ConcurrentHashMap<>();
this.teams = new ConcurrentHashMap<>();
this.projectiles = new ConcurrentHashMap<>();
this.communicationHandler = new CommunicationHandler(this);
this.inputHandler = new InputHandler(this, communicationHandler, camera);
this.inputHandler = new InputHandler(this, communicationHandler);
this.chat = new Chat();
this.mainThreadActions = new ConcurrentLinkedQueue<>();
}
public ClientConfig getConfig() {
@ -80,28 +76,30 @@ public class Client implements Runnable {
try {
communicationHandler.establishConnection();
} 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;
}
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();
log.debug("Sound system initialized.");
long lastFrameAt = System.currentTimeMillis();
while (!gameRenderer.windowShouldClose() && !communicationHandler.isDone()) {
long now = System.currentTimeMillis();
float dt = (now - lastFrameAt) / 1000f;
world.processQueuedChunkUpdates();
while (!mainThreadActions.isEmpty()) {
mainThreadActions.remove().run();
}
soundManager.updateListener(myPlayer.getPosition(), myPlayer.getVelocity());
gameRenderer.getCamera().interpolatePosition(dt);
interpolatePlayers(now, dt);
interpolateProjectiles(dt);
soundManager.playWalkingSounds(myPlayer, world, now);
soundManager.playWalkingSounds(myPlayer, now);
gameRenderer.draw();
lastFrameAt = now;
}
@ -120,17 +118,15 @@ public class Client implements Runnable {
communicationHandler.sendMessage(new ChunkHashMessage(u.cx(), u.cy(), u.cz(), -1));
}
} else if (msg instanceof PlayerUpdateMessage playerUpdate) {
runLater(() -> {
if (playerUpdate.clientId() == myPlayer.getId() && playerUpdate.timestamp() > lastPlayerUpdate) {
myPlayer.getPosition().set(playerUpdate.px(), playerUpdate.py(), playerUpdate.pz());
myPlayer.getVelocity().set(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz());
myPlayer.setCrouching(playerUpdate.crouching());
myPlayer.setMode(playerUpdate.mode());
if (gameRenderer != null) {
gameRenderer.getCamera().setToPlayer(myPlayer);
}
if (soundManager != null) {
soundManager.updateListener(myPlayer.getEyePosition(), myPlayer.getVelocity());
soundManager.updateListener(myPlayer.getPosition(), myPlayer.getVelocity());
}
lastPlayerUpdate = playerUpdate.timestamp();
} else {
@ -141,9 +137,8 @@ public class Client implements Runnable {
p.updateModelTransform();
}
}
});
} else if (msg instanceof ClientInventoryMessage inventoryMessage) {
runLater(() -> myPlayer.setInventory(inventoryMessage.inv()));
myPlayer.setInventory(inventoryMessage.inv());
} else if (msg instanceof InventorySelectedStackMessage selectedStackMessage) {
myPlayer.getInventory().setSelectedIndex(selectedStackMessage.index());
} else if (msg instanceof ItemStackMessage itemStackMessage) {
@ -154,20 +149,19 @@ public class Client implements Runnable {
player.setSelectedBlockValue(blockColorMessage.block());
}
} else if (msg instanceof PlayerJoinMessage joinMessage) {
runLater(() -> {
OtherPlayer op = OtherPlayer.fromJoinMessage(joinMessage, this);
players.put(op.getId(), op);
});
} 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);
Player p = joinMessage.toPlayer();
OtherPlayer op = new OtherPlayer(p.getId(), p.getUsername());
if (joinMessage.teamId() != -1) {
op.setTeam(teams.get(joinMessage.teamId()));
}
});
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) {
if (soundManager != null) {
soundManager.play(
@ -178,7 +172,6 @@ public class Client implements Runnable {
);
}
} else if (msg instanceof ProjectileMessage pm) {
runLater(() -> {
Projectile p = projectiles.get(pm.id());
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());
@ -190,22 +183,13 @@ public class Client implements Runnable {
projectiles.remove(p.getId());
}
}
});
} else if (msg instanceof ClientHealthMessage healthMessage) {
myPlayer.setHealth(healthMessage.health());
} else if (msg instanceof ChatMessage chatMessage) {
chat.chatReceived(chatMessage);
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;
}
public InputHandler getInputHandler() {
return inputHandler;
}
public CommunicationHandler getCommunicationHandler() {
return communicationHandler;
}
public Map<Integer, Team> getTeams() {
return teams;
}
@ -241,17 +217,13 @@ public class Client implements Runnable {
return chat;
}
public SoundManager getSoundManager() {
return soundManager;
}
public void interpolatePlayers(long now, float dt) {
Vector3f movement = new Vector3f();
for (var player : players.values()) {
movement.set(player.getVelocity()).mul(dt);
player.getPosition().add(movement);
player.updateModelTransform();
soundManager.playWalkingSounds(player, world, now);
soundManager.playWalkingSounds(player, now);
}
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 {
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();
configPaths.add(0, Path.of("client.yaml")); // Add this first so we create client.yaml if needed.
if (args.length > 3) {
configPaths.add(Path.of(args[3].trim()));
if (args.length > 0) {
configPaths.add(Path.of(args[0].trim()));
}
ClientConfig clientConfig = Config.loadConfig(ClientConfig.class, configPaths, new ClientConfig(), "default-config.yaml");
Client client = new Client(clientConfig, connectCfg);
ClientConfig clientConfig = Config.loadConfig(ClientConfig.class, configPaths, new ClientConfig());
Client client = new Client(clientConfig);
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.OtherPlayer;
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.item.ItemStack;
import nl.andrewl.aos_core.model.world.World;
import nl.andrewl.aos_core.model.world.WorldIO;
import nl.andrewl.aos_core.net.TcpReceiver;
import nl.andrewl.aos_core.net.UdpReceiver;
import nl.andrewl.aos_core.net.*;
import nl.andrewl.aos_core.net.connect.ConnectAcceptMessage;
import nl.andrewl.aos_core.net.connect.ConnectRejectMessage;
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.ExtendedDataOutputStream;
import org.joml.Vector3f;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.DatagramPacket;
@ -31,6 +31,8 @@ import java.net.Socket;
* methods for sending messages and processing those we receive.
*/
public class CommunicationHandler {
private static final Logger log = LoggerFactory.getLogger(CommunicationHandler.class);
private final Client client;
private Socket socket;
private DatagramSocket datagramSocket;
@ -47,14 +49,16 @@ public class CommunicationHandler {
if (socket != null && !socket.isClosed()) {
socket.close();
}
InetAddress address = InetAddress.getByName(client.connectConfig.host());
System.out.printf("Connecting to server at %s, port %d, with username \"%s\"...%n", address, client.connectConfig.port(), client.connectConfig.username());
InetAddress address = InetAddress.getByName(client.getConfig().serverHost);
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);
in = Net.getInputStream(socket.getInputStream());
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);
socket.setSoTimeout(0);
if (response instanceof ConnectRejectMessage rejectMessage) {
@ -62,9 +66,12 @@ public class CommunicationHandler {
}
if (response instanceof ConnectAcceptMessage acceptMessage) {
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();
log.debug("Initial data received.");
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 UdpReceiver(datagramSocket, (msg, packet) -> client.onMessageReceived(msg))).start();
} else {
@ -131,6 +138,7 @@ public class CommunicationHandler {
if (!connectionEstablished) {
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() {
@ -167,13 +175,13 @@ public class CommunicationHandler {
OtherPlayer player = new OtherPlayer(in.readInt(), in.readString());
int teamId = in.readInt();
if (teamId != -1) player.setTeam(client.getTeams().get(teamId));
System.out.println(teamId);
player.getPosition().set(in.readFloat(), in.readFloat(), in.readFloat());
player.getVelocity().set(in.readFloat(), in.readFloat(), in.readFloat());
player.getOrientation().set(in.readFloat(), in.readFloat());
player.setCrouching(in.readBoolean());
player.setHeldItemId(in.readInt());
player.setSelectedBlockValue(in.readByte());
player.setMode(PlayerMode.values()[in.readInt()]);
client.getPlayers().put(player.getId(), player);
}
@ -189,6 +197,5 @@ public class CommunicationHandler {
int teamId = in.readInt();
if (teamId != -1) client.getMyPlayer().setTeam(client.getTeams().get(teamId));
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;
public class ClientConfig {
public String serverHost = "localhost";
public int serverPort = 25565;
public String username = "player";
public InputConfig input = new InputConfig();
public DisplayConfig display = new DisplayConfig();

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;
import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos2_client.Client;
import nl.andrewl.aos2_client.CommunicationHandler;
import nl.andrewl.aos2_client.control.context.ChattingContext;
import nl.andrewl.aos2_client.control.context.ExitMenuContext;
import nl.andrewl.aos2_client.control.context.NormalContext;
import nl.andrewl.aos2_client.model.ClientPlayer;
import nl.andrewl.aos_core.model.item.BlockItemStack;
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.
@ -14,83 +17,65 @@ public class InputHandler {
private final Client client;
private final CommunicationHandler comm;
private long windowId;
private ClientInputState lastInputState = null;
private final NormalContext normalContext;
private final ChattingContext chattingContext;
private final ExitMenuContext exitMenuContext;
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 InputContext activeContext;
public InputHandler(Client client, CommunicationHandler comm, Camera cam) {
public InputHandler(Client client, CommunicationHandler comm) {
this.client = client;
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) {
this.windowId = windowId;
public void updateInputState(long window) {
// 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() {
return activeContext;
}
ClientPlayer player = client.getMyPlayer();
private void switchToContext(InputContext newContext) {
if (newContext.equals(activeContext)) return;
activeContext.onDisable();
newContext.onEnable();
activeContext = newContext;
}
public void switchToNormalContext() {
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;
// Check for "pick block" functionality.
if (glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_3) == GLFW_PRESS && 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));
}
}
}
}
}

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
public void invoke(long window, int key, int scancode, int action, int mods) {
switch (action) {
case GLFW_PRESS -> inputHandler.getActiveContext().keyPress(window, key, mods);
case GLFW_RELEASE -> inputHandler.getActiveContext().keyRelease(window, key, mods);
case GLFW_REPEAT -> inputHandler.getActiveContext().keyRepeat(window, key, mods);
}
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
glfwSetWindowShouldClose(window, true);
}
inputHandler.updateInputState(window);
}
}

View File

@ -2,9 +2,6 @@ package nl.andrewl.aos2_client.control;
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.
*/
@ -17,9 +14,6 @@ public class PlayerInputMouseClickCallback implements GLFWMouseButtonCallbackI {
@Override
public void invoke(long window, int button, int action, int mods) {
switch (action) {
case GLFW_PRESS -> inputHandler.getActiveContext().mouseButtonPress(window, button, mods);
case GLFW_RELEASE -> inputHandler.getActiveContext().mouseButtonRelease(window, button, mods);
}
inputHandler.updateInputState(window);
}
}

View File

@ -1,16 +1,29 @@
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;
public class PlayerInputMouseScrollCallback implements GLFWScrollCallbackI {
private final InputHandler inputHandler;
private final Client client;
private final CommunicationHandler comm;
public PlayerInputMouseScrollCallback(InputHandler inputHandler) {
this.inputHandler = inputHandler;
public PlayerInputMouseScrollCallback(Client client, CommunicationHandler comm) {
this.client = client;
this.comm = comm;
}
@Override
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;
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 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 {
/**
* 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) {
this.inputHandler = inputHandler;
public PlayerViewCursorCallback(ClientConfig.InputConfig config, Client client, Camera cam, CommunicationHandler comm) {
this.config = config;
this.client = client;
this.camera = cam;
this.comm = comm;
}
@Override
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;
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.item.Inventory;
import org.joml.Matrix3f;
@ -43,16 +42,12 @@ public class ClientPlayer extends Player {
this.health = health;
}
public void updateHeldItemTransform(Camera cam, InputHandler inputHandler) {
public void updateHeldItemTransform(Camera cam) {
heldItemTransform.identity()
.translate(cam.getPosition())
.rotate((float) (cam.getOrientation().x + Math.PI), Camera.UP)
.rotate(-cam.getOrientation().y + (float) Math.PI / 2, Camera.RIGHT);
if (inputHandler.isNormalContextActive() && inputHandler.getNormalContext().isScopeEnabled()) {
heldItemTransform.translate(0, -0.12f, 0);
} else {
heldItemTransform.translate(-0.35f, -0.4f, 0.5f);
}
.rotate(-cam.getOrientation().y + (float) Math.PI / 2, Camera.RIGHT)
.translate(-0.35f, -0.4f, 0.5f);
heldItemTransform.get(heldItemTransformData);
heldItemTransform.normal(heldItemNormalTransform);

View File

@ -1,10 +1,8 @@
package nl.andrewl.aos2_client.model;
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.item.ItemTypes;
import nl.andrewl.aos_core.net.client.PlayerJoinMessage;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
@ -101,18 +99,4 @@ public class OtherPlayer extends Player {
public float[] getHeldItemNormalTransformData() {
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.Client;
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.render.chunk.ChunkRenderer;
import nl.andrewl.aos2_client.render.gui.GuiRenderer;
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.item.BlockItemStack;
import nl.andrewl.aos_core.model.item.Inventory;
import nl.andrewl.aos_core.model.item.ItemTypes;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.lwjgl.glfw.Callbacks;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.GL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
@ -33,43 +30,46 @@ import static org.lwjgl.opengl.GL46.*;
* OpenGL context exists.
*/
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_FAR = 500f;
private final ClientConfig.DisplayConfig config;
private final ChunkRenderer chunkRenderer;
private final GuiRenderer guiRenderer;
private final ModelRenderer modelRenderer;
private ChunkRenderer chunkRenderer;
private GuiRenderer guiRenderer;
private ModelRenderer modelRenderer;
private final Camera camera;
private final Client client;
private final InputHandler inputHandler;
// Standard models for various game components.
private final Model playerModel;
private final Model rifleModel;
private final Model blockModel;
private final Model bulletModel;
private final Model smgModel;
private final Model shotgunModel;
private final Model flagModel;
private Model playerModel;
private Model rifleModel;
private Model blockModel;
private Model bulletModel;
private Model smgModel;
private Model shotgunModel;
private Model flagModel;
private final long windowHandle;
private final int screenWidth;
private final int screenHeight;
private long windowHandle;
private int screenWidth = 800;
private int screenHeight = 600;
private final Matrix4f perspectiveTransform;
private final float[] perspectiveTransformData = new float[16];
public GameRenderer(Client client, InputHandler inputHandler, Camera camera) {
this.config = client.getConfig().display;
public GameRenderer(ClientConfig.DisplayConfig config, Client client) {
this.config = config;
this.client = client;
this.inputHandler = inputHandler;
this.camera = camera;
this.camera = new Camera();
camera.setToPlayer(client.getMyPlayer());
this.perspectiveTransform = new Matrix4f();
}
// Initialize window!
public void setupWindow(
GLFWCursorPosCallbackI viewCursorCallback,
GLFWKeyCallbackI inputKeyCallback,
GLFWMouseButtonCallbackI mouseButtonCallback,
GLFWScrollCallbackI scrollCallback
) {
GLFWErrorCallback.createPrint(System.err).set();
if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW.");
glfwDefaultWindowHints();
@ -79,6 +79,7 @@ public class GameRenderer {
long monitorId = glfwGetPrimaryMonitor();
GLFWVidMode primaryMonitorSettings = glfwGetVideoMode(monitorId);
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) {
screenWidth = primaryMonitorSettings.width();
screenHeight = primaryMonitorSettings.height();
@ -89,22 +90,19 @@ public class GameRenderer {
windowHandle = glfwCreateWindow(screenWidth, screenHeight, "Ace of Shades 2", 0, 0);
}
if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window.");
inputHandler.setWindowId(windowHandle);
log.debug("Initialized GLFW window.");
// Setup callbacks.
glfwSetKeyCallback(windowHandle, new PlayerInputKeyCallback(inputHandler));
glfwSetCursorPosCallback(windowHandle, new PlayerViewCursorCallback(inputHandler));
glfwSetMouseButtonCallback(windowHandle, new PlayerInputMouseClickCallback(inputHandler));
glfwSetScrollCallback(windowHandle, new PlayerInputMouseScrollCallback(inputHandler));
glfwSetCharCallback(windowHandle, new PlayerCharacterInputCallback(inputHandler));
glfwSetWindowFocusCallback(windowHandle, (window, focused) -> {
if (!focused) inputHandler.switchToExitMenuContext();
});
glfwSetKeyCallback(windowHandle, inputKeyCallback);
glfwSetCursorPosCallback(windowHandle, viewCursorCallback);
glfwSetMouseButtonCallback(windowHandle, mouseButtonCallback);
glfwSetScrollCallback(windowHandle, scrollCallback);
if (config.captureCursor) {
glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
}
glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
glfwSetCursorPos(windowHandle, 0, 0);
log.debug("Set up window callbacks.");
glfwMakeContextCurrent(windowHandle);
glfwSwapInterval(1);
@ -112,18 +110,21 @@ public class GameRenderer {
GL.createCapabilities();
// 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_DEPTH_TEST);
glCullFace(GL_BACK);
log.debug("Initialized OpenGL context.");
this.chunkRenderer = new ChunkRenderer();
log.debug("Initialized chunk renderer.");
try {
this.guiRenderer = new GuiRenderer();
} catch (IOException e) {
throw new RuntimeException(e);
}
log.debug("Initialized GUI renderer.");
this.modelRenderer = new ModelRenderer();
try {
@ -137,7 +138,8 @@ public class GameRenderer {
} catch (IOException e) {
throw new RuntimeException(e);
}
updatePerspective(config.fov);
log.debug("Initialized model renderer.");
updatePerspective();
}
public float getAspectRatio() {
@ -147,17 +149,18 @@ public class GameRenderer {
/**
* Updates the rendering perspective used to render the game.
*/
public void updatePerspective(float fov) {
float fovRad = (float) Math.toRadians(fov);
private void updatePerspective() {
float fovRad = (float) Math.toRadians(config.fov);
if (fovRad >= Math.PI) {
fovRad = (float) (Math.PI - 0.01f);
} else if (fovRad <= 0) {
fovRad = 0.01f;
}
perspectiveTransform.setPerspective(fovRad, getAspectRatio(), Z_NEAR, Z_FAR);
perspectiveTransform.get(perspectiveTransformData);
if (chunkRenderer != null) chunkRenderer.setPerspective(perspectiveTransformData);
if (modelRenderer != null) modelRenderer.setPerspective(perspectiveTransformData);
float[] data = new float[16];
perspectiveTransform.get(data);
if (chunkRenderer != null) chunkRenderer.setPerspective(data);
if (modelRenderer != null) modelRenderer.setPerspective(data);
}
public boolean windowShouldClose() {
@ -174,24 +177,16 @@ public class GameRenderer {
public void draw() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
chunkRenderer.draw(camera, client.getWorld().getChunkMeshesToDraw());
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.
modelRenderer.start(camera.getViewTransformData());
myPlayer.updateHeldItemTransform(camera);
playerModel.bind();
for (var player : client.getPlayers().values()) {
if (player.getMode() == PlayerMode.SPECTATOR) continue;
if (player.getTeam() != null) {
modelRenderer.setAspectColor(player.getTeam().getColor());
} else {
@ -203,33 +198,30 @@ public class GameRenderer {
// Render guns!
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());
}
for (var player : client.getPlayers().values()) {
if (player.getMode() == PlayerMode.SPECTATOR) continue;
if (player.getHeldItemId() == ItemTypes.RIFLE.getId()) {
modelRenderer.render(rifleModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData());
}
}
rifleModel.unbind();
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());
}
for (var player : client.getPlayers().values()) {
if (player.getMode() == PlayerMode.SPECTATOR) continue;
if (player.getHeldItemId() == ItemTypes.AK_47.getId()) {
modelRenderer.render(smgModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData());
}
}
smgModel.unbind();
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());
}
for (var player : client.getPlayers().values()) {
if (player.getMode() == PlayerMode.SPECTATOR) continue;
if (player.getHeldItemId() == ItemTypes.WINCHESTER.getId()) {
modelRenderer.render(shotgunModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData());
}
@ -237,14 +229,13 @@ public class GameRenderer {
shotgunModel.unbind();
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();
modelRenderer.setAspectColor(client.getWorld().getPalette().getColor(stack.getSelectedValue()));
modelRenderer.render(blockModel, myPlayer.getHeldItemTransformData(), myPlayer.getHeldItemNormalTransformData());
}
modelRenderer.setAspectColor(new Vector3f(0.5f, 0.5f, 0.5f));
for (var player : client.getPlayers().values()) {
if (player.getMode() == PlayerMode.SPECTATOR) continue;
if (player.getHeldItemId() == ItemTypes.BLOCK.getId()) {
modelRenderer.setAspectColor(client.getWorld().getPalette().getColor(player.getSelectedBlockValue()));
modelRenderer.render(blockModel, player.getHeldItemTransformData(), player.getHeldItemNormalTransformData());
@ -268,7 +259,7 @@ public class GameRenderer {
flagModel.bind();
for (Team team : client.getTeams().values()) {
modelTransform.identity()
.translate(team.getSpawnPoint().x() - 0.25f, team.getSpawnPoint().y(), team.getSpawnPoint().z() - 0.25f);
.translate(team.getSpawnPoint());
modelTransform.normal(normalTransform);
modelRenderer.setAspectColor(team.getColor());
modelRenderer.render(flagModel, modelTransform, normalTransform);
@ -280,7 +271,7 @@ public class GameRenderer {
// GUI rendering
guiRenderer.start();
guiRenderer.drawNameplates(myPlayer, camera.getViewTransformData(), perspectiveTransform.get(new float[16]));
guiRenderer.drawNvg(screenWidth, screenHeight, client);
guiRenderer.drawNvg(screenWidth, screenHeight, myPlayer, client.getChat());
guiRenderer.end();
glfwSwapBuffers(windowHandle);
@ -288,15 +279,15 @@ public class GameRenderer {
}
public void freeWindow() {
rifleModel.free();
smgModel.free();
flagModel.free();
bulletModel.free();
playerModel.free();
blockModel.free();
modelRenderer.free();
guiRenderer.free();
chunkRenderer.free();
if (rifleModel != null) rifleModel.free();
if (smgModel != null) smgModel.free();
if (flagModel != null) flagModel.free();
if (bulletModel != null) bulletModel.free();
if (playerModel != null) playerModel.free();
if (blockModel != null) blockModel.free();
if (modelRenderer != null) modelRenderer.free();
if (guiRenderer != null) guiRenderer.free();
if (chunkRenderer != null) chunkRenderer.free();
GL.destroy();
Callbacks.glfwFreeCallbacks(windowHandle);
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.World;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.lwjgl.opengl.GL46.*;
@ -9,6 +11,8 @@ import static org.lwjgl.opengl.GL46.*;
* Represents a 3d mesh for a chunk.
*/
public class ChunkMesh {
private static final Logger log = LoggerFactory.getLogger(ChunkMesh.class);
private final int vboId;
private final int vaoId;
private final int eboId;
@ -45,11 +49,20 @@ public class ChunkMesh {
* Generates and loads this chunk's mesh into the allocated OpenGL buffers.
*/
private void loadMesh(ChunkMeshGenerator meshGenerator) {
// long start = System.nanoTime();
var meshData = meshGenerator.generateMesh(chunk, world);
// double dur = (System.nanoTime() - start) / 1_000_000.0;
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);
glBufferData(GL_ARRAY_BUFFER, meshData.vertexBuffer(), GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, meshData.indexBuffer(), GL_STATIC_DRAW);
}

View File

@ -1,19 +1,16 @@
package nl.andrewl.aos2_client.render.gui;
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.OtherPlayer;
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.model.Player;
import nl.andrewl.aos_core.model.PlayerMode;
import nl.andrewl.aos_core.model.item.BlockItem;
import nl.andrewl.aos_core.model.item.BlockItemStack;
import nl.andrewl.aos_core.model.item.Gun;
import nl.andrewl.aos_core.model.item.GunItemStack;
import nl.andrewl.aos_core.model.world.Hit;
import org.joml.Matrix4f;
import org.lwjgl.BufferUtils;
import org.lwjgl.nanovg.NVGColor;
@ -197,12 +194,10 @@ public class GuiRenderer {
glUniform1i(namePlateTextureSamplerUniform, 0);
glUniformMatrix4fv(namePlateViewTransformUniform, false, viewTransformData);
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()) {
OtherPlayer player = entry.getKey();
// There are some scenarios where we skip rendering the name.
if (player.getPosition().distance(myPlayer.getPosition()) > nameplateRadius || player.getMode() == PlayerMode.SPECTATOR) continue;
// Skip rendering far-away nameplates.
if (player.getPosition().distance(myPlayer.getPosition()) > 50) continue;
GuiTexture texture = entry.getValue();
float aspectRatio = (float) texture.getHeight() / (float) texture.getWidth();
transformMatrix.identity()
@ -221,24 +216,14 @@ public class GuiRenderer {
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);
nvgSave(vgId);
boolean scopeEnabled = client.getInputHandler().getNormalContext().isScopeEnabled();
PlayerMode mode = client.getMyPlayer().getMode();
drawCrosshair(width, height, scopeEnabled);
drawHeldItemStackInfo(width, height, client.getMyPlayer());
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);
}
drawCrosshair(width, height);
drawChat(width, height, chat);
drawHealthBar(width, height, player);
drawHeldItemStackInfo(width, height, player);
nvgRestore(vgId);
nvgEndFrame(vgId);
@ -263,40 +248,34 @@ public class GuiRenderer {
shaderProgram.free();
}
private void drawCrosshair(float w, float h, boolean scopeEnabled) {
private void drawCrosshair(float w, float h) {
float cx = w / 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);
nvgMoveTo(vgId, cx - size / 2, cy);
nvgLineTo(vgId, cx + size / 2, cy);
nvgMoveTo(vgId, cx, cy - size / 2);
nvgLineTo(vgId, cx, cy + size / 2);
nvgMoveTo(vgId, cx - 10, cy);
nvgLineTo(vgId, cx + 10, cy);
nvgMoveTo(vgId, cx, cy - 10);
nvgLineTo(vgId, cx, cy + 10);
nvgStroke(vgId);
}
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);
nvgRect(vgId, w - 170, h - 100, 100, 20);
nvgRect(vgId, 20, h - 60, 100, 20);
nvgFill(vgId);
nvgFillColor(vgId, GuiUtils.rgba(0, 0.6f, 0, 1, colorA));
nvgFillColor(vgId, GuiUtils.rgba(0, 1, 0, 1, colorA));
nvgBeginPath(vgId);
nvgRect(vgId, w - 170, h - 100, 100 * player.getHealth(), 20);
nvgRect(vgId, 20, h - 60, 100 * player.getHealth(), 20);
nvgFill(vgId);
nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA));
nvgFontSize(vgId, 12f);
nvgFontFaceId(vgId, jetbrainsMonoFont);
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) {
@ -337,81 +316,34 @@ public class GuiRenderer {
nvgFontFaceId(vgId, jetbrainsMonoFont);
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 - 14, String.format("Selected value: %d", stack.getSelectedValue()));
}
private void drawChat(float w, float h, Client client) {
var chat = client.getChat();
private void drawChat(float w, float h, Chat chat) {
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);
nvgFontFaceId(vgId, jetbrainsMonoFont);
nvgTextAlign(vgId, NVG_ALIGN_LEFT | NVG_ALIGN_TOP);
float y = h - 16 - 12;
float y = chatHeight - 12;
for (var msg : chat.getMessages()) {
if (msg.author().equals("_ANNOUNCE")) {
nvgFillColor(vgId, GuiUtils.rgba(0.7f, 0, 0, 1, colorA));
nvgText(vgId, 5, y, msg.message());
} 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());
} else {
nvgFillColor(vgId, GuiUtils.rgba(1, 1, 1, 1, colorA));
nvgText(vgId, 5, y, msg.author() + ": " + msg.message());
}
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;
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.lwjgl.openal.AL;
import org.lwjgl.openal.ALC;
import org.lwjgl.openal.ALCCapabilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
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.ThreadLocalRandom;
@ -21,6 +24,8 @@ import static org.lwjgl.openal.ALC10.*;
* Main class for managing the OpenAL audio interface.
*/
public class SoundManager {
private static final Logger log = LoggerFactory.getLogger(SoundManager.class);
private static final int SOURCE_COUNT = 32;
private final long alContext;
@ -75,8 +80,6 @@ public class SoundManager {
load("block_break_1", "sound/m_block_break_1.wav");
load("block_place_1", "sound/m_block_place_1.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) {
@ -99,7 +102,7 @@ public class SoundManager {
public void play(String soundName, float gain, Vector3f position, Vector3f velocity) {
Integer bufferId = getSoundBuffer(soundName);
if (bufferId == null) {
System.err.printf("Attempted to play unknown sound \"%s\".%n", soundName);
log.warn("Attempted to play unknown sound \"{}\"", soundName);
} else {
SoundSource source = getNextAvailableSoundSource();
if (source != null) {
@ -108,7 +111,7 @@ public class SoundManager {
source.setGain(gain);
source.play(bufferId);
} 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));
}
public void playWalkingSounds(Player player, World world, long now) {
// Don't play sounds for players who are still, non-normal mode, or not on the ground.
if (player.getVelocity().length() <= 0 || player.getMode() != PlayerMode.NORMAL || !player.isGrounded(world)) return;
public void playWalkingSounds(Player player, long now) {
if (player.getVelocity().length() <= 0) return;
long lastSoundAt = lastPlayerWalkingSounds.computeIfAbsent(player, p -> 0L);
long delay = 500; // Delay in ms between footfalls.
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() {
for (var source : availableSources) {
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>
<artifactId>ace-of-shades-2</artifactId>
<groupId>nl.andrewl</groupId>
<version>1.5.0</version>
<version>1.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -47,6 +47,18 @@
<artifactId>snakeyaml</artifactId>
<version>1.30</version>
</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 -->

View File

@ -26,37 +26,28 @@ public final class Net {
private static final Serializer serializer = new Serializer();
static {
int i = 1;
// Basic protocol messages.
serializer.registerType(i++, ConnectRequestMessage.class);
serializer.registerType(i++, ConnectAcceptMessage.class);
serializer.registerType(i++, ConnectRejectMessage.class);
serializer.registerType(i++, DatagramInit.class);
// World messages.
serializer.registerType(i++, ChunkHashMessage.class);
serializer.registerType(i++, ChunkDataMessage.class);
serializer.registerType(i++, ChunkUpdateMessage.class);
serializer.registerType(i++, ProjectileMessage.class);
// Player/client messages.
serializer.registerType(i++, ClientInputState.class);
serializer.registerType(i++, ClientOrientationState.class);
serializer.registerType(i++, ClientHealthMessage.class);
serializer.registerType(i++, PlayerUpdateMessage.class);
serializer.registerType(i++, PlayerJoinMessage.class);
serializer.registerType(i++, PlayerLeaveMessage.class);
serializer.registerType(i++, PlayerTeamUpdateMessage.class);
serializer.registerType(i++, BlockColorMessage.class);
serializer.registerType(i++, InventorySelectedStackMessage.class);
serializer.registerType(i++, ChatMessage.class);
serializer.registerType(i++, ChatWrittenMessage.class);
serializer.registerType(i++, ClientRecoilMessage.class);
serializer.registerType(1, ConnectRequestMessage.class);
serializer.registerType(2, ConnectAcceptMessage.class);
serializer.registerType(3, ConnectRejectMessage.class);
serializer.registerType(4, DatagramInit.class);
serializer.registerType(5, ChunkHashMessage.class);
serializer.registerType(6, ChunkDataMessage.class);
serializer.registerType(7, ChunkUpdateMessage.class);
serializer.registerType(8, ClientInputState.class);
serializer.registerType(9, ClientOrientationState.class);
serializer.registerType(10, PlayerUpdateMessage.class);
serializer.registerType(11, PlayerJoinMessage.class);
serializer.registerType(12, PlayerLeaveMessage.class);
// Separate serializers for client inventory messages.
serializer.registerTypeSerializer(i++, new InventorySerializer());
serializer.registerTypeSerializer(i++, new ItemStackSerializer());
serializer.registerType(i++, SoundMessage.class);
serializer.registerTypeSerializer(13, new InventorySerializer());
serializer.registerTypeSerializer(14, new ItemStackSerializer());
serializer.registerType(15, InventorySelectedStackMessage.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) {

View File

@ -1,6 +1,5 @@
package nl.andrewl.aos_core.config;
import nl.andrewl.aos_core.FileUtils;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
@ -20,11 +19,10 @@ public final class Config {
* @param paths The paths to load from.
* @param fallback A default configuration object to use if no config could
* be loaded from any of the paths.
* @param defaultConfigFile The default config file resource to save.
* @return 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) {
if (Files.exists(path) && Files.isRegularFile(path) && Files.isReadable(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;
}
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() {
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.yml"));
paths.add(Path.of("config.yaml"));
paths.add(Path.of("config.yml"));
paths.add(Path.of("cfg.yaml"));
paths.add(Path.of("cfg.yml"));
return paths;

View File

@ -1,10 +1,10 @@
package nl.andrewl.aos_core.model;
import nl.andrewl.aos_core.Directions;
import nl.andrewl.aos_core.MathUtils;
import nl.andrewl.aos_core.model.world.World;
import org.joml.*;
import org.joml.Math;
import org.joml.Vector2f;
import org.joml.Vector3f;
import org.joml.Vector3i;
import java.util.ArrayList;
import java.util.List;
@ -75,13 +75,7 @@ public class Player {
*/
protected 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) {
public Player(int id, String username, Team team) {
this.position = new Vector3f();
this.velocity = new Vector3f();
this.orientation = new Vector2f();
@ -89,11 +83,10 @@ public class Player {
this.id = id;
this.username = username;
this.team = team;
this.mode = mode;
}
public Player(int id, String username) {
this(id, username, null, PlayerMode.NORMAL);
this(id, username, null);
}
public Vector3f getPosition() {
@ -146,14 +139,6 @@ public class Player {
this.team = team;
}
public PlayerMode getMode() {
return mode;
}
public void setMode(PlayerMode mode) {
this.mode = mode;
}
public Vector3f getViewVector() {
return viewVector;
}
@ -183,48 +168,6 @@ public class Player {
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() {
float playerBodyMinZ = 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() {
if (itemStacks.isEmpty()) return null;
return itemStacks.get(selectedIndex);
}
@ -48,13 +47,6 @@ public class Inventory {
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() {
for (var stack : itemStacks) {
if (stack instanceof BlockItemStack b) {
@ -63,9 +55,4 @@ public class Inventory {
}
return 1;
}
public void clear() {
itemStacks.clear();
selectedIndex = -1;
}
}

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ import java.util.Map;
* that players can interact in.
*/
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 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);
}
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.
*/
@ -170,12 +164,8 @@ public class World {
public Hit getLookingAtPos(Vector3f eyePos, Vector3f eyeDir, float limit) {
if (eyeDir.lengthSquared() == 0 || limit <= 0) return null;
Vector3f pos = new Vector3f(eyePos);
Vector3f previousPos = new Vector3f();
while (pos.distance(eyePos) < limit) {
previousPos.set(pos);
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) {
Vector3i hitPos = new Vector3i(
(int) Math.floor(pos.x),
@ -244,7 +234,7 @@ public class World {
// Testing code!
if (diff == 0) {
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);
}

View File

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

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;
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.record_net.Message;
@ -16,15 +15,13 @@ public record PlayerJoinMessage(
float ox, float oy,
boolean crouching,
int selectedItemId,
byte selectedBlockValue,
PlayerMode mode
byte selectedBlockValue
) implements Message {
public Player toPlayer() {
Player p = new Player(id, username);
p.getPosition().set(px, py, pz);
p.getVelocity().set(vx, vy, vz);
p.getOrientation().set(ox, oy);
p.setMode(mode);
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;
import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.PlayerMode;
import nl.andrewl.record_net.Message;
/**
@ -17,8 +16,7 @@ public record PlayerUpdateMessage(
float vx, float vy, float vz,
float ox, float oy,
boolean crouching,
int selectedItemId,
PlayerMode mode
int selectedItemId
) implements Message {
public void apply(Player p) {
@ -26,6 +24,5 @@ public record PlayerUpdateMessage(
p.getVelocity().set(vx, vy, vz);
p.getOrientation().set(ox, oy);
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;
/**
* 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 {}
public record ConnectRequestMessage(String username) implements Message {}

View File

@ -4,6 +4,6 @@ appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
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.appenderRef.stdout.ref = STDOUT

Binary file not shown.

Before

(image error) Size: 26 KiB

After

(image error) Size: 29 KiB

Binary file not shown.

Before

(image error) Size: 31 KiB

Binary file not shown.

Before

(image error) 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>
<maven.compiler.source>18</maven.compiler.source>
<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>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
@ -27,12 +26,6 @@
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</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>
<build>
@ -54,46 +47,6 @@
<target>17</target>
</configuration>
</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>
</build>

View File

@ -4,10 +4,6 @@ module aos2_launcher {
requires javafx.graphics;
requires javafx.fxml;
requires java.net.http;
requires com.google.gson;
exports nl.andrewl.aos2_launcher to javafx.graphics;
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 java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* The main starting point for the launcher app.
*/
public class Launcher extends Application {
public static final Path BASE_DIR = Path.of(System.getProperty("user.home"), ".ace-of-shades");
public static final Path VERSIONS_DIR = BASE_DIR.resolve("versions");
public static final Path PROFILES_FILE = BASE_DIR.resolve("profiles.json");
public static final Path PROFILES_DIR = BASE_DIR.resolve("profiles");
public static final Path JRE_PATH = BASE_DIR.resolve("jre");
@Override
public void start(Stage stage) throws IOException {
if (!Files.exists(BASE_DIR)) Files.createDirectory(BASE_DIR);
if (!Files.exists(VERSIONS_DIR)) Files.createDirectory(VERSIONS_DIR);
if (!Files.exists(PROFILES_DIR)) Files.createDirectory(PROFILES_DIR);
FXMLLoader loader = new FXMLLoader(Launcher.class.getResource("/main_view.fxml"));
Parent rootNode = loader.load();
Scene scene = new Scene(rootNode);
addStylesheet(scene, "/font/fonts.css");
addStylesheet(scene, "/styles.css");
scene.getStylesheets().add(Launcher.class.getResource("/styles.css").toExternalForm());
stage.setScene(scene);
stage.setTitle("Ace of Shades - Launcher");
stage.setTitle("Ace of Shades 2 - Launcher");
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) {
launch(args);
}

View File

@ -1,140 +1,11 @@
package nl.andrewl.aos2_launcher;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Window;
import nl.andrewl.aos2_launcher.model.Profile;
import nl.andrewl.aos2_launcher.model.ProfileSet;
import nl.andrewl.aos2_launcher.model.ProgressReporter;
import nl.andrewl.aos2_launcher.model.Server;
import nl.andrewl.aos2_launcher.view.EditProfileDialog;
import nl.andrewl.aos2_launcher.view.ElementList;
import nl.andrewl.aos2_launcher.view.ProfileView;
import nl.andrewl.aos2_launcher.view.ServerView;
import java.util.ArrayList;
public class MainViewController implements ProgressReporter {
@FXML public Button playButton;
@FXML public Button editProfileButton;
@FXML public Button removeProfileButton;
@FXML public VBox profilesVBox;
private ElementList<Profile, ProfileView> profilesList;
@FXML public VBox serversVBox;
private ElementList<Server, ServerView> serversList;
@FXML public VBox progressVBox;
@FXML public Label progressLabel;
@FXML public ProgressBar progressBar;
@FXML public TextField registryUrlField;
private final ProfileSet profileSet = new ProfileSet();
private ServersFetcher serversFetcher;
import javafx.scene.layout.TilePane;
public class MainViewController {
@FXML
public void initialize() {
profilesList = new ElementList<>(profilesVBox, ProfileView::new, ProfileView.class, ProfileView::getProfile);
profileSet.selectedProfileProperty().addListener((observable, oldValue, newValue) -> profileSet.save());
// A hack since we can't bind the profilesList's elements to the profileSet's.
profileSet.getProfiles().addListener((ListChangeListener<? super Profile>) c -> {
var selected = profileSet.getSelectedProfile();
profilesList.clear();
profilesList.addAll(profileSet.getProfiles());
profilesList.selectElement(selected);
});
profileSet.loadOrCreateStandardFile();
profilesList.selectElement(profileSet.getSelectedProfile());
profileSet.selectedProfileProperty().bind(profilesList.selectedElementProperty());
public TilePane profilesTilePane;
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"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?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">
<TabPane tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
<Tab text="Profiles">
<VBox>
<HBox alignment="CENTER" styleClass="button-bar" VBox.vgrow="NEVER">
<Button onAction="#addProfile" text="Add Profile" />
<Button fx:id="editProfileButton" onAction="#editProfile" text="Edit Profile" />
<Button fx:id="removeProfileButton" onAction="#removeProfile" text="Remove Profile" />
</HBox>
<ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS">
<VBox fx:id="profilesVBox" styleClass="banner-list" />
<VBox xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/16"
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0"
fx:controller="nl.andrewl.aos2_launcher.MainViewController"
>
<MenuBar>
<Menu mnemonicParsing="false" text="File">
<MenuItem mnemonicParsing="false" text="Exit"/>
</Menu>
<Menu mnemonicParsing="false" text="Profiles">
<MenuItem mnemonicParsing="false" text="New Profile"/>
</Menu>
<Menu mnemonicParsing="false" text="Help">
<MenuItem mnemonicParsing="false" text="About"/>
</Menu>
</MenuBar>
<ScrollPane VBox.vgrow="ALWAYS">
<TilePane fx:id="profilesTilePane"/>
</ScrollPane>
</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>
<artifactId>ace-of-shades-2</artifactId>
<packaging>pom</packaging>
<version>1.5.0</version>
<version>1.1.0</version>
<modules>
<module>core</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