Added better arena management system, and prepared for ship physics engine in core.
This commit is contained in:
parent
1fc23a1101
commit
102f1c7b35
|
@ -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<ShipComponent> components;
|
||||||
|
|
||||||
|
private final Collection<Panel> panels;
|
||||||
|
private final Collection<Gun> guns;
|
||||||
|
private final Collection<Cockpit> 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<ShipComponent> getComponents() {
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Panel> getPanels() {
|
||||||
|
return panels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Gun> getGuns() {
|
||||||
|
return guns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Cockpit> getCockpits() {
|
||||||
|
return cockpits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color getPrimaryColor() {
|
||||||
|
return primaryColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getMass() {
|
||||||
|
float m = 0;
|
||||||
|
for (var c : components) {
|
||||||
|
m += c.getMass();
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ShipComponent> components;
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<ShipComponent> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
|
@ -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<ShipComponent> {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Vec2F> points;
|
||||||
|
|
||||||
|
public List<Vec2F> getPoints() {
|
||||||
|
return points;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Gun> {
|
||||||
|
@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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package nl.andrewl.starship_arena.core.net;
|
||||||
|
|
||||||
|
import nl.andrewl.record_net.Message;
|
||||||
|
|
||||||
|
public record ArenaStatus (
|
||||||
|
String currentStage
|
||||||
|
) implements Message {}
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
<version>2.6.6</version>
|
<version>2.6.7</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>nl.andrewl.starship-arena</groupId>
|
<groupId>nl.andrewl.starship-arena</groupId>
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.22</version>
|
<version>1.18.24</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
|
@ -4,10 +4,11 @@ import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import nl.andrewl.record_net.Message;
|
import nl.andrewl.record_net.Message;
|
||||||
import nl.andrewl.record_net.Serializer;
|
import nl.andrewl.record_net.Serializer;
|
||||||
import nl.andrewl.starship_arena.core.net.ClientConnectRequest;
|
import nl.andrewl.starship_arena.core.net.*;
|
||||||
import nl.andrewl.starship_arena.core.net.ClientConnectResponse;
|
import nl.andrewl.starship_arena.server.control.ClientNetManager;
|
||||||
import nl.andrewl.starship_arena.server.data.ArenaStore;
|
import nl.andrewl.starship_arena.server.data.ArenaStore;
|
||||||
import nl.andrewl.starship_arena.server.model.Arena;
|
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.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
|
@ -17,6 +18,8 @@ import javax.annotation.PreDestroy;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.*;
|
import java.net.*;
|
||||||
import java.util.UUID;
|
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
|
* The socket gateway is the central point at which all clients connect for any
|
||||||
|
@ -31,11 +34,15 @@ public class SocketGateway implements Runnable {
|
||||||
static {
|
static {
|
||||||
SERIALIZER.registerType(1, ClientConnectRequest.class);
|
SERIALIZER.registerType(1, ClientConnectRequest.class);
|
||||||
SERIALIZER.registerType(2, ClientConnectResponse.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 ServerSocket serverSocket;
|
||||||
private final DatagramSocket serverUdpSocket;
|
private final DatagramSocket serverUdpSocket;
|
||||||
private final ArenaStore arenaStore;
|
private final ArenaStore arenaStore;
|
||||||
|
private final ExecutorService connectionProcessingExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
@Value("${starship-arena.gateway.host}") @Getter
|
@Value("${starship-arena.gateway.host}") @Getter
|
||||||
private String host;
|
private String host;
|
||||||
|
@ -81,7 +88,7 @@ public class SocketGateway implements Runnable {
|
||||||
while (!serverSocket.isClosed()) {
|
while (!serverSocket.isClosed()) {
|
||||||
try {
|
try {
|
||||||
Socket clientSocket = serverSocket.accept();
|
Socket clientSocket = serverSocket.accept();
|
||||||
new Thread(() -> processIncomingConnection(clientSocket)).start();
|
connectionProcessingExecutor.submit(() -> processIncomingConnection(clientSocket));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (!e.getMessage().equalsIgnoreCase("Socket closed")) e.printStackTrace();
|
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
|
* Logic to do to initialize a client TCP connection, which involves getting
|
||||||
* some basic information about the connection, such as which arena the
|
* 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.
|
* handle further communication with the client.
|
||||||
* @param clientSocket The socket to the client.
|
* @param clientSocket The socket to the client.
|
||||||
*/
|
*/
|
||||||
|
@ -106,16 +113,16 @@ public class SocketGateway implements Runnable {
|
||||||
var oa = arenaStore.getById(arenaId);
|
var oa = arenaStore.getById(arenaId);
|
||||||
if (oa.isPresent()) {
|
if (oa.isPresent()) {
|
||||||
Arena arena = oa.get();
|
Arena arena = oa.get();
|
||||||
ClientManager clientManager = new ClientManager(arena, clientSocket, clientId);
|
Client client = new Client(clientId, arena, clientSocket);
|
||||||
arena.registerClient(clientManager);
|
arena.registerClient(client);
|
||||||
SERIALIZER.writeMessage(new ClientConnectResponse(true, clientId));
|
SERIALIZER.writeMessage(new ClientConnectResponse(true, clientId), clientSocket.getOutputStream());
|
||||||
clientSocket.setSoTimeout(0); // Reset timeout to infinity after successful initialization.
|
clientSocket.setSoTimeout(0); // Reset timeout to infinity after successful initialization.
|
||||||
clientManager.start();
|
new Thread(client.getNetManager()).start();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If the connection wasn't valid, return a no-success response.
|
// 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();
|
clientSocket.close();
|
||||||
} catch (SocketTimeoutException e) {
|
} catch (SocketTimeoutException e) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -28,4 +28,9 @@ public class ArenasController {
|
||||||
public ArenaResponse createArena(@RequestBody ArenaCreationPayload payload) {
|
public ArenaResponse createArena(@RequestBody ArenaCreationPayload payload) {
|
||||||
return arenaStore.registerArena(payload);
|
return arenaStore.registerArena(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(path = "/{arenaId}/start")
|
||||||
|
public void startArena(@PathVariable String arenaId) {
|
||||||
|
arenaStore.startArena(arenaId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.record_net.Message;
|
||||||
import nl.andrewl.starship_arena.core.net.ChatSend;
|
import nl.andrewl.starship_arena.core.net.ChatSend;
|
||||||
import nl.andrewl.starship_arena.server.model.Arena;
|
import nl.andrewl.starship_arena.core.net.ChatSent;
|
||||||
import nl.andrewl.starship_arena.server.model.ChatMessage;
|
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.net.Socket;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static nl.andrewl.starship_arena.server.SocketGateway.SERIALIZER;
|
import static nl.andrewl.starship_arena.server.SocketGateway.SERIALIZER;
|
||||||
|
|
||||||
public class ClientManager extends Thread {
|
/**
|
||||||
@Getter
|
* A runnable that manages sending and receiving from a client application.
|
||||||
private final UUID clientId;
|
*/
|
||||||
@Getter
|
public class ClientNetManager implements Runnable {
|
||||||
private String clientUsername;
|
private final Client client;
|
||||||
private final Arena arena;
|
|
||||||
|
|
||||||
private final Socket clientSocket;
|
private final Socket clientSocket;
|
||||||
private final InputStream in;
|
private final InputStream in;
|
||||||
private final OutputStream out;
|
private final OutputStream out;
|
||||||
private final DataInputStream dIn;
|
|
||||||
private final DataOutputStream dOut;
|
|
||||||
|
|
||||||
public ClientManager(Arena arena, Socket clientSocket, UUID id) throws IOException {
|
public ClientNetManager(Client client, Socket clientSocket) throws IOException {
|
||||||
this.arena = arena;
|
this.client = client;
|
||||||
this.clientSocket = clientSocket;
|
this.clientSocket = clientSocket;
|
||||||
this.clientId = id;
|
|
||||||
this.in = clientSocket.getInputStream();
|
this.in = clientSocket.getInputStream();
|
||||||
this.out = clientSocket.getOutputStream();
|
this.out = clientSocket.getOutputStream();
|
||||||
this.dIn = new DataInputStream(clientSocket.getInputStream());
|
|
||||||
this.dOut = new DataOutputStream(clientSocket.getOutputStream());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void send(byte[] data) throws IOException {
|
public void send(byte[] data) throws IOException {
|
||||||
|
@ -56,7 +50,7 @@ public class ClientManager extends Thread {
|
||||||
try {
|
try {
|
||||||
Message msg = SERIALIZER.readMessage(in);
|
Message msg = SERIALIZER.readMessage(in);
|
||||||
if (msg instanceof ChatSend cs) {
|
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) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
|
@ -1,16 +1,29 @@
|
||||||
package nl.andrewl.starship_arena.server.data;
|
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.ArenaCreationPayload;
|
||||||
import nl.andrewl.starship_arena.server.api.dto.ArenaResponse;
|
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.Arena;
|
||||||
|
import nl.andrewl.starship_arena.server.model.ArenaStage;
|
||||||
import org.springframework.http.HttpStatus;
|
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.stereotype.Service;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that manages the set of all active arenas.
|
||||||
|
*/
|
||||||
@Service
|
@Service
|
||||||
|
@EnableScheduling
|
||||||
|
@Slf4j
|
||||||
public class ArenaStore {
|
public class ArenaStore {
|
||||||
private final Map<UUID, Arena> arenas = new ConcurrentHashMap<>();
|
private final Map<UUID, Arena> arenas = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@ -40,4 +53,30 @@ public class ArenaStore {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid arena id.");
|
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<UUID> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,45 @@
|
||||||
package nl.andrewl.starship_arena.server.model;
|
package nl.andrewl.starship_arena.server.model;
|
||||||
|
|
||||||
import nl.andrewl.starship_arena.core.net.ChatSent;
|
import lombok.Getter;
|
||||||
import nl.andrewl.starship_arena.server.ClientManager;
|
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.time.Instant;
|
||||||
import java.util.List;
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
|
||||||
|
|
||||||
import static nl.andrewl.starship_arena.server.SocketGateway.SERIALIZER;
|
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Slf4j
|
||||||
public class Arena {
|
public class Arena {
|
||||||
private final UUID id;
|
private final UUID id;
|
||||||
private final Instant createdAt;
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|
||||||
private ArenaStage currentStage = ArenaStage.STAGING;
|
private final Instant createdAt;
|
||||||
|
|
||||||
private final Map<UUID, ClientManager> clients = new ConcurrentHashMap<>();
|
private Instant startedAt;
|
||||||
private final List<ChatMessage> chatMessages = new CopyOnWriteArrayList<>();
|
private Instant battleStartsAt;
|
||||||
|
private Instant battleEndsAt;
|
||||||
|
private Instant closesAt;
|
||||||
|
|
||||||
|
private ArenaStage currentStage = ArenaStage.PRE_STAGING;
|
||||||
|
|
||||||
|
private final Map<UUID, Client> clients = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final ArenaNetManager netManager;
|
||||||
|
@Setter
|
||||||
|
private ArenaUpdater updater;
|
||||||
|
|
||||||
public Arena(String name) {
|
public Arena(String name) {
|
||||||
this.id = UUID.randomUUID();
|
this.id = UUID.randomUUID();
|
||||||
this.createdAt = Instant.now();
|
this.createdAt = Instant.now();
|
||||||
|
this.netManager = new ArenaNetManager(this);
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,45 +47,47 @@ public class Arena {
|
||||||
this("Unnamed Arena");
|
this("Unnamed Arena");
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID getId() {
|
public Set<Client> getClients() {
|
||||||
return id;
|
return new HashSet<>(clients.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Instant getCreatedAt() {
|
public void advanceStage() {
|
||||||
return createdAt;
|
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() {
|
private void start() {
|
||||||
return name;
|
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() {
|
private void close() {
|
||||||
return currentStage;
|
for (var client : clients.values()) {
|
||||||
|
client.getNetManager().shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerClient(ClientManager clientManager) {
|
public void registerClient(Client client) {
|
||||||
if (clients.containsKey(clientManager.getClientId())) {
|
if (clients.containsKey(client.getId())) {
|
||||||
clientManager.shutdown();
|
client.getNetManager().shutdown();
|
||||||
} else {
|
} 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
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package nl.andrewl.starship_arena.server.model;
|
package nl.andrewl.starship_arena.server.model;
|
||||||
|
|
||||||
public enum ArenaStage {
|
public enum ArenaStage {
|
||||||
|
PRE_STAGING,
|
||||||
STAGING,
|
STAGING,
|
||||||
BATTLE,
|
BATTLE,
|
||||||
ANALYSIS
|
ANALYSIS,
|
||||||
|
CLOSED
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue