Added better arena management system, and prepared for ship physics engine in core.

This commit is contained in:
Andrew Lalis 2022-05-01 12:06:48 +02:00
parent 1fc23a1101
commit 102f1c7b35
21 changed files with 593 additions and 72 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 {
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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()
);
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
package nl.andrewl.starship_arena.core.net;
import nl.andrewl.record_net.Message;
public record ArenaStatus (
String currentStage
) implements Message {}

View File

@ -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];
}
}

View File

@ -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>

View File

@ -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 {

View File

@ -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);
}
} }

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();

View File

@ -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);
}
}
} }

View File

@ -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) {

View File

@ -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
} }

View File

@ -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);
}
}