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>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>2.6.6</version>
|
||||
<version>2.6.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>nl.andrewl.starship-arena</groupId>
|
||||
|
@ -31,7 +31,7 @@
|
|||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.22</version>
|
||||
<version>1.18.24</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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();
|
|
@ -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<UUID, Arena> 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<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;
|
||||
|
||||
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<UUID, ClientManager> clients = new ConcurrentHashMap<>();
|
||||
private final List<ChatMessage> chatMessages = new CopyOnWriteArrayList<>();
|
||||
private Instant startedAt;
|
||||
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) {
|
||||
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<Client> 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) {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package nl.andrewl.starship_arena.server.model;
|
||||
|
||||
public enum ArenaStage {
|
||||
PRE_STAGING,
|
||||
STAGING,
|
||||
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