From 102f1c7b35cd2f3652d6bac4c5e3049f84e4c04d Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sun, 1 May 2022 12:06:48 +0200 Subject: [PATCH] Added better arena management system, and prepared for ship physics engine in core. --- .../starship_arena/core/model/Ship.java | 73 +++++++++++++++ .../starship_arena/core/model/ShipModel.java | 68 ++++++++++++++ .../core/model/ship/Cockpit.java | 7 ++ .../model/ship/ComponentDeserializer.java | 24 +++++ .../core/model/ship/GeometricComponent.java | 16 ++++ .../starship_arena/core/model/ship/Gun.java | 53 +++++++++++ .../core/model/ship/GunDeserializer.java | 22 +++++ .../starship_arena/core/model/ship/Panel.java | 8 ++ .../core/model/ship/ShipComponent.java | 27 ++++++ .../starship_arena/core/net/ArenaStatus.java | 7 ++ .../core/physics/PhysObject.java | 29 ++++++ server/pom.xml | 4 +- .../starship_arena/server/SocketGateway.java | 25 +++-- .../server/api/ArenasController.java | 5 + .../server/control/ArenaNetManager.java | 41 ++++++++ .../server/control/ArenaUpdater.java | 65 +++++++++++++ .../ClientNetManager.java} | 34 +++---- .../server/data/ArenaStore.java | 39 ++++++++ .../starship_arena/server/model/Arena.java | 93 +++++++++++-------- .../server/model/ArenaStage.java | 4 +- .../starship_arena/server/model/Client.java | 21 +++++ 21 files changed, 593 insertions(+), 72 deletions(-) create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/model/Ship.java create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/model/ShipModel.java create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Cockpit.java create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/model/ship/ComponentDeserializer.java create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/model/ship/GeometricComponent.java create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Gun.java create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/model/ship/GunDeserializer.java create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Panel.java create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/model/ship/ShipComponent.java create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/net/ArenaStatus.java create mode 100644 core/src/main/java/nl/andrewl/starship_arena/core/physics/PhysObject.java create mode 100644 server/src/main/java/nl/andrewl/starship_arena/server/control/ArenaNetManager.java create mode 100644 server/src/main/java/nl/andrewl/starship_arena/server/control/ArenaUpdater.java rename server/src/main/java/nl/andrewl/starship_arena/server/{ClientManager.java => control/ClientNetManager.java} (53%) create mode 100644 server/src/main/java/nl/andrewl/starship_arena/server/model/Client.java diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/model/Ship.java b/core/src/main/java/nl/andrewl/starship_arena/core/model/Ship.java new file mode 100644 index 0000000..a84c366 --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/model/Ship.java @@ -0,0 +1,73 @@ +package nl.andrewl.starship_arena.core.model; + +import nl.andrewl.starship_arena.core.model.ship.Cockpit; +import nl.andrewl.starship_arena.core.model.ship.Gun; +import nl.andrewl.starship_arena.core.model.ship.Panel; +import nl.andrewl.starship_arena.core.model.ship.ShipComponent; +import nl.andrewl.starship_arena.core.physics.PhysObject; +import nl.andrewl.starship_arena.core.util.ResourceUtils; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Collection; + +public class Ship extends PhysObject { + private final String modelName; + private final Collection components; + + private final Collection panels; + private final Collection guns; + private final Collection cockpits; + + private Color primaryColor = Color.GRAY; + + private Ship(ShipModel model) { + this.modelName = model.getName(); + this.components = model.getComponents(); + this.panels = new ArrayList<>(); + this.guns = new ArrayList<>(); + this.cockpits = new ArrayList<>(); + for (var c : components) { + c.setShip(this); + if (c instanceof Panel p) panels.add(p); + if (c instanceof Gun g) guns.add(g); + if (c instanceof Cockpit cp) cockpits.add(cp); + } + } + + public Ship(String modelResource) { + this(ShipModel.load(ResourceUtils.getString(modelResource))); + } + + public String getModelName() { + return modelName; + } + + public Collection getComponents() { + return components; + } + + public Collection getPanels() { + return panels; + } + + public Collection getGuns() { + return guns; + } + + public Collection getCockpits() { + return cockpits; + } + + public Color getPrimaryColor() { + return primaryColor; + } + + public float getMass() { + float m = 0; + for (var c : components) { + m += c.getMass(); + } + return m; + } +} diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/model/ShipModel.java b/core/src/main/java/nl/andrewl/starship_arena/core/model/ShipModel.java new file mode 100644 index 0000000..bd5f2e1 --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/model/ShipModel.java @@ -0,0 +1,68 @@ +package nl.andrewl.starship_arena.core.model; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import nl.andrewl.starship_arena.core.model.ship.*; + +import java.util.Collection; + +public class ShipModel { + private String name; + private Collection components; + + public String getName() { + return name; + } + + public Collection getComponents() { + return components; + } + + /** + * Normalizes the geometric properties of the components such that the ship + * model's components are centered around (0, 0). + * TODO: Consider scaling? + */ + private void normalizeComponents() { + float minX = Float.MAX_VALUE; + float maxX = Float.MIN_VALUE; + float minY = Float.MAX_VALUE; + float maxY = Float.MIN_VALUE; + for (var c : components) { + if (c instanceof GeometricComponent g) { + for (var p : g.getPoints()) { + minX = Math.min(minX, p.x); + maxX = Math.max(maxX, p.x); + minY = Math.min(minY, p.y); + maxY = Math.max(maxY, p.y); + } + } + } + final float width = maxX - minX; + final float height = maxY - minY; + final float offsetX = -minX - width / 2; + final float offsetY = -minY - height / 2; + // Shift all components to the top-left. + for (var c : components) { + if (c instanceof GeometricComponent g) { + for (var p : g.getPoints()) { + p.x += offsetX; + p.y += offsetY; + } + } else if (c instanceof Gun g) { + g.getLocation().x += offsetX; + g.getLocation().y += offsetY; + } + } + } + + public static ShipModel load(String json) { + Gson gson = new GsonBuilder() + .registerTypeAdapter(ShipComponent.class, new ComponentDeserializer()) + .registerTypeAdapter(Gun.class, new GunDeserializer()) + .create(); + ShipModel model = gson.fromJson(json, ShipModel.class); + model.normalizeComponents(); + return model; + } +} diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Cockpit.java b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Cockpit.java new file mode 100644 index 0000000..08735b9 --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Cockpit.java @@ -0,0 +1,7 @@ +package nl.andrewl.starship_arena.core.model.ship; + +/** + * A cockpit represents the control point of the ship. + */ +public class Cockpit extends GeometricComponent { +} diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/ComponentDeserializer.java b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/ComponentDeserializer.java new file mode 100644 index 0000000..653f93b --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/ComponentDeserializer.java @@ -0,0 +1,24 @@ +package nl.andrewl.starship_arena.core.model.ship; + +import com.google.gson.*; + +import java.lang.reflect.Type; + +/** + * Custom deserializer that's used to deserialize components based on their + * "type" property. + */ +public class ComponentDeserializer implements JsonDeserializer { + @Override + public ShipComponent deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext ctx) throws JsonParseException { + JsonObject obj = jsonElement.getAsJsonObject(); + String componentTypeName = obj.get("type").getAsString(); + Type componentType = switch (componentTypeName) { + case "panel" -> Panel.class; + case "cockpit" -> Cockpit.class; + case "gun" -> Gun.class; + default -> throw new JsonParseException("Invalid ship component type: " + componentTypeName); + }; + return ctx.deserialize(obj, componentType); + } +} diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/GeometricComponent.java b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/GeometricComponent.java new file mode 100644 index 0000000..7f58174 --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/GeometricComponent.java @@ -0,0 +1,16 @@ +package nl.andrewl.starship_arena.core.model.ship; + +import nl.andrewl.starship_arena.core.physics.Vec2F; + +import java.util.List; + +/** + * Represents a component of a ship that can be drawn as a geometric primitive. + */ +public abstract class GeometricComponent extends ShipComponent { + private List points; + + public List getPoints() { + return points; + } +} diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Gun.java b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Gun.java new file mode 100644 index 0000000..8a9e17d --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Gun.java @@ -0,0 +1,53 @@ +package nl.andrewl.starship_arena.core.model.ship; + +import nl.andrewl.starship_arena.core.physics.Vec2F; + +public class Gun extends ShipComponent { + private String name; + private Vec2F location; + private float rotation; + private float maxRotation; + private float minRotation; + private float barrelWidth; + private float barrelLength; + + public Gun() {} + + public Gun(String name, Vec2F location, float rotation, float maxRotation, float minRotation, float barrelWidth, float barrelLength) { + this.name = name; + this.location = location; + this.rotation = rotation; + this.maxRotation = maxRotation; + this.minRotation = minRotation; + this.barrelWidth = barrelWidth; + this.barrelLength = barrelLength; + } + + public String getName() { + return name; + } + + public Vec2F getLocation() { + return location; + } + + public float getRotation() { + return rotation; + } + + public float getMaxRotation() { + return maxRotation; + } + + public float getMinRotation() { + return minRotation; + } + + public float getBarrelWidth() { + return barrelWidth; + } + + public float getBarrelLength() { + return barrelLength; + } +} diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/GunDeserializer.java b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/GunDeserializer.java new file mode 100644 index 0000000..fc03cb5 --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/GunDeserializer.java @@ -0,0 +1,22 @@ +package nl.andrewl.starship_arena.core.model.ship; + +import com.google.gson.*; + +import java.awt.geom.Point2D; +import java.lang.reflect.Type; + +public class GunDeserializer implements JsonDeserializer { + @Override + public Gun deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext ctx) throws JsonParseException { + JsonObject obj = jsonElement.getAsJsonObject(); + return new Gun( + obj.get("name").getAsString(), + ctx.deserialize(obj.get("location"), Point2D.Float.class), + (float) Math.toRadians(obj.get("rotation").getAsFloat()), + (float) Math.toRadians(obj.get("maxRotation").getAsDouble()), + (float) Math.toRadians(obj.get("minRotation").getAsFloat()), + obj.get("barrelWidth").getAsFloat(), + obj.get("barrelLength").getAsFloat() + ); + } +} diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Panel.java b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Panel.java new file mode 100644 index 0000000..8b5e16b --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/Panel.java @@ -0,0 +1,8 @@ +package nl.andrewl.starship_arena.core.model.ship; + +/** + * A simple structural panel that makes up all or part of a ship's body. + */ +public class Panel extends GeometricComponent { + private String name; +} diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/ShipComponent.java b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/ShipComponent.java new file mode 100644 index 0000000..3829537 --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/model/ship/ShipComponent.java @@ -0,0 +1,27 @@ +package nl.andrewl.starship_arena.core.model.ship; + +import nl.andrewl.starship_arena.core.model.Ship; + +/** + * Represents the top-level component information for any part of a ship. + */ +public abstract class ShipComponent { + /** + * The ship that this component belongs to. + */ + private transient Ship ship; + + private float mass; + + public Ship getShip() { + return ship; + } + + public void setShip(Ship ship) { + this.ship = ship; + } + + public float getMass() { + return mass; + } +} diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/net/ArenaStatus.java b/core/src/main/java/nl/andrewl/starship_arena/core/net/ArenaStatus.java new file mode 100644 index 0000000..38acf94 --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/net/ArenaStatus.java @@ -0,0 +1,7 @@ +package nl.andrewl.starship_arena.core.net; + +import nl.andrewl.record_net.Message; + +public record ArenaStatus ( + String currentStage +) implements Message {} diff --git a/core/src/main/java/nl/andrewl/starship_arena/core/physics/PhysObject.java b/core/src/main/java/nl/andrewl/starship_arena/core/physics/PhysObject.java new file mode 100644 index 0000000..4299a42 --- /dev/null +++ b/core/src/main/java/nl/andrewl/starship_arena/core/physics/PhysObject.java @@ -0,0 +1,29 @@ +package nl.andrewl.starship_arena.core.physics; + +public abstract class PhysObject { + public Vec2F pos; + public Vec2F vel; + public float rotation; + public float rotationSpeed; + + public void setRotationNormalized(float rotation) { + while (rotation < 0) rotation += 2 * Math.PI; + while (rotation > 2 * Math.PI) rotation -= 2 * Math.PI; + this.rotation = rotation; + } + + public float[] toArray() { + return new float[]{ + pos.x, pos.y, vel.x, vel.y, rotation, rotationSpeed + }; + } + + public void fromArray(float[] values) { + pos.x = values[0]; + pos.y = values[1]; + vel.x = values[2]; + vel.y = values[3]; + rotation = values[4]; + rotationSpeed = values[5]; + } +} diff --git a/server/pom.xml b/server/pom.xml index dac8242..04f95e2 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -21,7 +21,7 @@ org.springframework.boot spring-boot-starter-web - 2.6.6 + 2.6.7 nl.andrewl.starship-arena @@ -31,7 +31,7 @@ org.projectlombok lombok - 1.18.22 + 1.18.24 provided diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java b/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java index d29c86c..96cec36 100644 --- a/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java +++ b/server/src/main/java/nl/andrewl/starship_arena/server/SocketGateway.java @@ -4,10 +4,11 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Serializer; -import nl.andrewl.starship_arena.core.net.ClientConnectRequest; -import nl.andrewl.starship_arena.core.net.ClientConnectResponse; +import nl.andrewl.starship_arena.core.net.*; +import nl.andrewl.starship_arena.server.control.ClientNetManager; import nl.andrewl.starship_arena.server.data.ArenaStore; import nl.andrewl.starship_arena.server.model.Arena; +import nl.andrewl.starship_arena.server.model.Client; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; @@ -17,6 +18,8 @@ import javax.annotation.PreDestroy; import java.io.IOException; import java.net.*; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * The socket gateway is the central point at which all clients connect for any @@ -31,11 +34,15 @@ public class SocketGateway implements Runnable { static { SERIALIZER.registerType(1, ClientConnectRequest.class); SERIALIZER.registerType(2, ClientConnectResponse.class); + SERIALIZER.registerType(3, ChatSend.class); + SERIALIZER.registerType(4, ChatSent.class); + SERIALIZER.registerType(5, ArenaStatus.class); } private final ServerSocket serverSocket; private final DatagramSocket serverUdpSocket; private final ArenaStore arenaStore; + private final ExecutorService connectionProcessingExecutor = Executors.newSingleThreadExecutor(); @Value("${starship-arena.gateway.host}") @Getter private String host; @@ -81,7 +88,7 @@ public class SocketGateway implements Runnable { while (!serverSocket.isClosed()) { try { Socket clientSocket = serverSocket.accept(); - new Thread(() -> processIncomingConnection(clientSocket)).start(); + connectionProcessingExecutor.submit(() -> processIncomingConnection(clientSocket)); } catch (IOException e) { if (!e.getMessage().equalsIgnoreCase("Socket closed")) e.printStackTrace(); } @@ -91,7 +98,7 @@ public class SocketGateway implements Runnable { /** * Logic to do to initialize a client TCP connection, which involves getting * some basic information about the connection, such as which arena the - * client is connecting to. A {@link ClientManager} is then started to + * client is connecting to. A {@link ClientNetManager} is then started to * handle further communication with the client. * @param clientSocket The socket to the client. */ @@ -106,16 +113,16 @@ public class SocketGateway implements Runnable { var oa = arenaStore.getById(arenaId); if (oa.isPresent()) { Arena arena = oa.get(); - ClientManager clientManager = new ClientManager(arena, clientSocket, clientId); - arena.registerClient(clientManager); - SERIALIZER.writeMessage(new ClientConnectResponse(true, clientId)); + Client client = new Client(clientId, arena, clientSocket); + arena.registerClient(client); + SERIALIZER.writeMessage(new ClientConnectResponse(true, clientId), clientSocket.getOutputStream()); clientSocket.setSoTimeout(0); // Reset timeout to infinity after successful initialization. - clientManager.start(); + new Thread(client.getNetManager()).start(); return; } } // If the connection wasn't valid, return a no-success response. - SERIALIZER.writeMessage(new ClientConnectResponse(false, null)); + SERIALIZER.writeMessage(new ClientConnectResponse(false, null), clientSocket.getOutputStream()); clientSocket.close(); } catch (SocketTimeoutException e) { try { diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/api/ArenasController.java b/server/src/main/java/nl/andrewl/starship_arena/server/api/ArenasController.java index 15e58cb..cc3258e 100644 --- a/server/src/main/java/nl/andrewl/starship_arena/server/api/ArenasController.java +++ b/server/src/main/java/nl/andrewl/starship_arena/server/api/ArenasController.java @@ -28,4 +28,9 @@ public class ArenasController { public ArenaResponse createArena(@RequestBody ArenaCreationPayload payload) { return arenaStore.registerArena(payload); } + + @PostMapping(path = "/{arenaId}/start") + public void startArena(@PathVariable String arenaId) { + arenaStore.startArena(arenaId); + } } diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/control/ArenaNetManager.java b/server/src/main/java/nl/andrewl/starship_arena/server/control/ArenaNetManager.java new file mode 100644 index 0000000..ae5fc20 --- /dev/null +++ b/server/src/main/java/nl/andrewl/starship_arena/server/control/ArenaNetManager.java @@ -0,0 +1,41 @@ +package nl.andrewl.starship_arena.server.control; + +import lombok.extern.slf4j.Slf4j; +import nl.andrewl.record_net.Message; +import nl.andrewl.starship_arena.server.model.Arena; + +import java.io.IOException; + +import static nl.andrewl.starship_arena.server.SocketGateway.SERIALIZER; + +/** + * A class that handles all the network operations involved in running an + * arena. + */ +@Slf4j +public class ArenaNetManager { + private final Arena arena; + + public ArenaNetManager(Arena arena) { + this.arena = arena; + } + + public void broadcast(Message msg) { + try { + byte[] data = SERIALIZER.writeMessage(msg); + broadcast(data); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void broadcast(byte[] data) { + for (var client : arena.getClients()) { + try { + client.getNetManager().send(data); + } catch (IOException e) { + log.error("Could not broadcast data to client: " + client, e); + } + } + } +} diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/control/ArenaUpdater.java b/server/src/main/java/nl/andrewl/starship_arena/server/control/ArenaUpdater.java new file mode 100644 index 0000000..55e579c --- /dev/null +++ b/server/src/main/java/nl/andrewl/starship_arena/server/control/ArenaUpdater.java @@ -0,0 +1,65 @@ +package nl.andrewl.starship_arena.server.control; + +import lombok.extern.slf4j.Slf4j; +import nl.andrewl.starship_arena.server.model.Arena; + +import java.time.Duration; +import java.time.Instant; + +/** + * A runnable that, when started, manages the state of an {@link Arena} + * throughout its lifecycle. + */ +@Slf4j +public class ArenaUpdater implements Runnable { + private final Arena arena; + + public ArenaUpdater(Arena arena) { + this.arena = arena; + arena.setUpdater(this); + } + + @Override + public void run() { + log.info("Starting arena."); + arena.advanceStage(); + log.info("Waiting for battle to start at {}", arena.getBattleStartsAt()); + doStaging(); + log.info("Starting battle."); + arena.advanceStage(); + doBattle(); + log.info("Battle done. Moving to analysis"); + arena.advanceStage(); + doAnalysis(); + log.info("Closing arena."); + arena.advanceStage(); + } + + private void doStaging() { + try { + long millisUntilBattle = Duration.between(Instant.now(), arena.getBattleStartsAt()).toMillis(); + Thread.sleep(millisUntilBattle); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private void doBattle() { + // TODO: actual physics updates! + try { + long millisUntilBattle = Duration.between(Instant.now(), arena.getBattleEndsAt()).toMillis(); + Thread.sleep(millisUntilBattle); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + private void doAnalysis() { + try { + long millisUntilBattle = Duration.between(Instant.now(), arena.getClosesAt()).toMillis(); + Thread.sleep(millisUntilBattle); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/ClientManager.java b/server/src/main/java/nl/andrewl/starship_arena/server/control/ClientNetManager.java similarity index 53% rename from server/src/main/java/nl/andrewl/starship_arena/server/ClientManager.java rename to server/src/main/java/nl/andrewl/starship_arena/server/control/ClientNetManager.java index 1e82457..92d0696 100644 --- a/server/src/main/java/nl/andrewl/starship_arena/server/ClientManager.java +++ b/server/src/main/java/nl/andrewl/starship_arena/server/control/ClientNetManager.java @@ -1,38 +1,32 @@ -package nl.andrewl.starship_arena.server; +package nl.andrewl.starship_arena.server.control; -import lombok.Getter; import nl.andrewl.record_net.Message; import nl.andrewl.starship_arena.core.net.ChatSend; -import nl.andrewl.starship_arena.server.model.Arena; -import nl.andrewl.starship_arena.server.model.ChatMessage; +import nl.andrewl.starship_arena.core.net.ChatSent; +import nl.andrewl.starship_arena.server.model.Client; -import java.io.*; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.Socket; -import java.util.UUID; import static nl.andrewl.starship_arena.server.SocketGateway.SERIALIZER; -public class ClientManager extends Thread { - @Getter - private final UUID clientId; - @Getter - private String clientUsername; - private final Arena arena; +/** + * A runnable that manages sending and receiving from a client application. + */ +public class ClientNetManager implements Runnable { + private final Client client; private final Socket clientSocket; private final InputStream in; private final OutputStream out; - private final DataInputStream dIn; - private final DataOutputStream dOut; - public ClientManager(Arena arena, Socket clientSocket, UUID id) throws IOException { - this.arena = arena; + public ClientNetManager(Client client, Socket clientSocket) throws IOException { + this.client = client; this.clientSocket = clientSocket; - this.clientId = id; this.in = clientSocket.getInputStream(); this.out = clientSocket.getOutputStream(); - this.dIn = new DataInputStream(clientSocket.getInputStream()); - this.dOut = new DataOutputStream(clientSocket.getOutputStream()); } public void send(byte[] data) throws IOException { @@ -56,7 +50,7 @@ public class ClientManager extends Thread { try { Message msg = SERIALIZER.readMessage(in); if (msg instanceof ChatSend cs) { - arena.chatSent(new ChatMessage(clientId, System.currentTimeMillis(), cs.msg())); + client.getArena().getNetManager().broadcast(new ChatSent(client.getId(), System.currentTimeMillis(), cs.msg())); } } catch (IOException e) { e.printStackTrace(); diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java b/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java index 5aa4d93..c9c1c51 100644 --- a/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java +++ b/server/src/main/java/nl/andrewl/starship_arena/server/data/ArenaStore.java @@ -1,16 +1,29 @@ package nl.andrewl.starship_arena.server.data; +import lombok.extern.slf4j.Slf4j; import nl.andrewl.starship_arena.server.api.dto.ArenaCreationPayload; import nl.andrewl.starship_arena.server.api.dto.ArenaResponse; +import nl.andrewl.starship_arena.server.control.ArenaUpdater; import nl.andrewl.starship_arena.server.model.Arena; +import nl.andrewl.starship_arena.server.model.ArenaStage; import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +/** + * Service that manages the set of all active arenas. + */ @Service +@EnableScheduling +@Slf4j public class ArenaStore { private final Map arenas = new ConcurrentHashMap<>(); @@ -40,4 +53,30 @@ public class ArenaStore { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid arena id."); } } + + public void startArena(String arenaId) { + Arena arena = arenas.get(UUID.fromString(arenaId)); + if (arena == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND); + if (arena.getCurrentStage() != ArenaStage.PRE_STAGING) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Arena is already started."); + } + new Thread(new ArenaUpdater(arena)).start(); + } + + @Scheduled(fixedRate = 10, timeUnit = TimeUnit.SECONDS) + public void cleanArenas() { + Set removalSet = new HashSet<>(); + final Instant cutoff = Instant.now().minus(5, ChronoUnit.MINUTES); + for (var arena : arenas.values()) { + if ( + (arena.getCurrentStage() == ArenaStage.CLOSED) || + (arena.getCurrentStage() == ArenaStage.PRE_STAGING && arena.getCreatedAt().isBefore(cutoff)) + ) { + removalSet.add(arena.getId()); + } + } + for (var id : removalSet) { + arenas.remove(id); + } + } } diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/model/Arena.java b/server/src/main/java/nl/andrewl/starship_arena/server/model/Arena.java index c7d8cff..e74519b 100644 --- a/server/src/main/java/nl/andrewl/starship_arena/server/model/Arena.java +++ b/server/src/main/java/nl/andrewl/starship_arena/server/model/Arena.java @@ -1,34 +1,45 @@ package nl.andrewl.starship_arena.server.model; -import nl.andrewl.starship_arena.core.net.ChatSent; -import nl.andrewl.starship_arena.server.ClientManager; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import nl.andrewl.starship_arena.core.net.ArenaStatus; +import nl.andrewl.starship_arena.server.control.ArenaNetManager; +import nl.andrewl.starship_arena.server.control.ArenaUpdater; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.time.Instant; -import java.util.List; +import java.time.temporal.ChronoUnit; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; - -import static nl.andrewl.starship_arena.server.SocketGateway.SERIALIZER; +@Getter +@Slf4j public class Arena { private final UUID id; - private final Instant createdAt; private final String name; - private ArenaStage currentStage = ArenaStage.STAGING; + private final Instant createdAt; - private final Map clients = new ConcurrentHashMap<>(); - private final List chatMessages = new CopyOnWriteArrayList<>(); + private Instant startedAt; + private Instant battleStartsAt; + private Instant battleEndsAt; + private Instant closesAt; + + private ArenaStage currentStage = ArenaStage.PRE_STAGING; + + private final Map clients = new ConcurrentHashMap<>(); + + private final ArenaNetManager netManager; + @Setter + private ArenaUpdater updater; public Arena(String name) { this.id = UUID.randomUUID(); this.createdAt = Instant.now(); + this.netManager = new ArenaNetManager(this); this.name = name; } @@ -36,45 +47,47 @@ public class Arena { this("Unnamed Arena"); } - public UUID getId() { - return id; + public Set getClients() { + return new HashSet<>(clients.values()); } - public Instant getCreatedAt() { - return createdAt; + public void advanceStage() { + if (currentStage == ArenaStage.PRE_STAGING) { + start(); + currentStage = ArenaStage.STAGING; + } else if (currentStage == ArenaStage.STAGING) { + currentStage = ArenaStage.BATTLE; + } else if (currentStage == ArenaStage.BATTLE) { + currentStage = ArenaStage.ANALYSIS; + } else if (currentStage == ArenaStage.ANALYSIS) { + close(); + currentStage = ArenaStage.CLOSED; + } + netManager.broadcast(new ArenaStatus(currentStage.name())); } - public String getName() { - return name; + private void start() { + this.startedAt = Instant.now(); + this.battleStartsAt = startedAt.plus(1, ChronoUnit.MINUTES); + this.battleEndsAt = battleStartsAt.plus(2, ChronoUnit.MINUTES); + this.closesAt = battleEndsAt.plus(1, ChronoUnit.MINUTES); } - public ArenaStage getCurrentStage() { - return currentStage; + private void close() { + for (var client : clients.values()) { + client.getNetManager().shutdown(); + } } - public void registerClient(ClientManager clientManager) { - if (clients.containsKey(clientManager.getClientId())) { - clientManager.shutdown(); + public void registerClient(Client client) { + if (clients.containsKey(client.getId())) { + client.getNetManager().shutdown(); } else { - clients.put(clientManager.getClientId(), clientManager); + clients.put(client.getId(), client); } } - public void chatSent(ChatMessage chat) throws IOException { - chatMessages.add(chat); - byte[] data = SERIALIZER.writeMessage(new ChatSent(chat.clientId(), chat.timestamp(), chat.message())); - broadcast(data); - } - private void broadcast(byte[] data) { - for (var cm : clients.values()) { - try { - cm.send(data); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } @Override public boolean equals(Object o) { diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/model/ArenaStage.java b/server/src/main/java/nl/andrewl/starship_arena/server/model/ArenaStage.java index a72aab3..abe640a 100644 --- a/server/src/main/java/nl/andrewl/starship_arena/server/model/ArenaStage.java +++ b/server/src/main/java/nl/andrewl/starship_arena/server/model/ArenaStage.java @@ -1,7 +1,9 @@ package nl.andrewl.starship_arena.server.model; public enum ArenaStage { + PRE_STAGING, STAGING, BATTLE, - ANALYSIS + ANALYSIS, + CLOSED } diff --git a/server/src/main/java/nl/andrewl/starship_arena/server/model/Client.java b/server/src/main/java/nl/andrewl/starship_arena/server/model/Client.java new file mode 100644 index 0000000..7b48cc5 --- /dev/null +++ b/server/src/main/java/nl/andrewl/starship_arena/server/model/Client.java @@ -0,0 +1,21 @@ +package nl.andrewl.starship_arena.server.model; + +import lombok.Getter; +import nl.andrewl.starship_arena.server.control.ClientNetManager; + +import java.io.IOException; +import java.net.Socket; +import java.util.UUID; + +@Getter +public class Client { + private final UUID id; + private final ClientNetManager netManager; + private final Arena arena; + + public Client(UUID id, Arena arena, Socket socket) throws IOException { + this.id = id; + this.arena = arena; + this.netManager = new ClientNetManager(this, socket); + } +}