Added better error handling, and prepared for arena ship management.
This commit is contained in:
parent
102f1c7b35
commit
92e60135fe
|
@ -29,7 +29,9 @@ import java.util.concurrent.Executors;
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SocketGateway implements Runnable {
|
public class SocketGateway implements Runnable {
|
||||||
public static final int INITIALIZATION_TIMEOUT = 1000;
|
/**
|
||||||
|
* The serializer that's used for communication between arenas and clients.
|
||||||
|
*/
|
||||||
public static final Serializer SERIALIZER = new Serializer();
|
public static final Serializer SERIALIZER = new Serializer();
|
||||||
static {
|
static {
|
||||||
SERIALIZER.registerType(1, ClientConnectRequest.class);
|
SERIALIZER.registerType(1, ClientConnectRequest.class);
|
||||||
|
@ -50,6 +52,8 @@ public class SocketGateway implements Runnable {
|
||||||
private short tcpPort;
|
private short tcpPort;
|
||||||
@Value("${starship-arena.gateway.udp-port}") @Getter
|
@Value("${starship-arena.gateway.udp-port}") @Getter
|
||||||
private short udpPort;
|
private short udpPort;
|
||||||
|
@Value("${starship-arena.gateway.init-timeout-ms}")
|
||||||
|
private int initTimeout;
|
||||||
|
|
||||||
public SocketGateway(ArenaStore arenaStore) throws IOException {
|
public SocketGateway(ArenaStore arenaStore) throws IOException {
|
||||||
this.serverSocket = new ServerSocket();
|
this.serverSocket = new ServerSocket();
|
||||||
|
@ -104,7 +108,7 @@ public class SocketGateway implements Runnable {
|
||||||
*/
|
*/
|
||||||
private void processIncomingConnection(Socket clientSocket) {
|
private void processIncomingConnection(Socket clientSocket) {
|
||||||
try {
|
try {
|
||||||
clientSocket.setSoTimeout(INITIALIZATION_TIMEOUT); // Set limited timeout so new connections don't waste resources.
|
clientSocket.setSoTimeout(initTimeout); // Set limited timeout so new connections don't waste resources.
|
||||||
Message msg = SERIALIZER.readMessage(clientSocket.getInputStream());
|
Message msg = SERIALIZER.readMessage(clientSocket.getInputStream());
|
||||||
if (msg instanceof ClientConnectRequest cm) {
|
if (msg instanceof ClientConnectRequest cm) {
|
||||||
UUID arenaId = cm.arenaId();
|
UUID arenaId = cm.arenaId();
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package nl.andrewl.starship_arena.server.api;
|
||||||
|
|
||||||
|
import nl.andrewl.starship_arena.server.api.dto.ErrorResponse;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class ApiExceptionHandler {
|
||||||
|
@ExceptionHandler(ResponseStatusException.class)
|
||||||
|
public ResponseEntity<ErrorResponse> handleRSE(ResponseStatusException e) {
|
||||||
|
return new ResponseEntity<>(new ErrorResponse(e.getRawStatusCode(), e.getReason()), e.getStatus());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package nl.andrewl.starship_arena.server.api.dto;
|
||||||
|
|
||||||
|
public record ErrorResponse(
|
||||||
|
int status,
|
||||||
|
String message
|
||||||
|
) {}
|
|
@ -6,6 +6,7 @@ import nl.andrewl.starship_arena.server.api.dto.ArenaResponse;
|
||||||
import nl.andrewl.starship_arena.server.control.ArenaUpdater;
|
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 nl.andrewl.starship_arena.server.model.ArenaStage;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
@ -25,10 +26,14 @@ import java.util.concurrent.TimeUnit;
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ArenaStore {
|
public class ArenaStore {
|
||||||
|
@Value("${starship-arena.max-arenas : 100}")
|
||||||
|
private int maxArenas;
|
||||||
|
|
||||||
private final Map<UUID, Arena> arenas = new ConcurrentHashMap<>();
|
private final Map<UUID, Arena> arenas = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public List<ArenaResponse> getArenas() {
|
public List<ArenaResponse> getArenas() {
|
||||||
return arenas.values().stream()
|
return arenas.values().stream()
|
||||||
|
.filter(Arena::isActive)
|
||||||
.sorted(Comparator.comparing(Arena::getCreatedAt))
|
.sorted(Comparator.comparing(Arena::getCreatedAt))
|
||||||
.map(ArenaResponse::new)
|
.map(ArenaResponse::new)
|
||||||
.toList();
|
.toList();
|
||||||
|
@ -38,25 +43,30 @@ public class ArenaStore {
|
||||||
return Optional.ofNullable(arenas.get(id));
|
return Optional.ofNullable(arenas.get(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Arena getOrThrow(String id) {
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(id);
|
||||||
|
Arena arena = arenas.get(uuid);
|
||||||
|
if (arena == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Arena with id \"" + id + "\" not found.");
|
||||||
|
return arena;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Malformed id. Should be a UUID.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ArenaResponse registerArena(ArenaCreationPayload payload) {
|
public ArenaResponse registerArena(ArenaCreationPayload payload) {
|
||||||
|
if (arenas.size() >= maxArenas) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Too many arenas.");
|
||||||
Arena arena = new Arena(payload.name());
|
Arena arena = new Arena(payload.name());
|
||||||
this.arenas.put(arena.getId(), arena);
|
this.arenas.put(arena.getId(), arena);
|
||||||
return new ArenaResponse(arena);
|
return new ArenaResponse(arena);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArenaResponse getArena(String arenaId) {
|
public ArenaResponse getArena(String arenaId) {
|
||||||
try {
|
return new ArenaResponse(getOrThrow(arenaId));
|
||||||
Arena arena = arenas.get(UUID.fromString(arenaId));
|
|
||||||
if (arena == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
|
||||||
return new ArenaResponse(arena);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid arena id.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startArena(String arenaId) {
|
public void startArena(String arenaId) {
|
||||||
Arena arena = arenas.get(UUID.fromString(arenaId));
|
Arena arena = getOrThrow(arenaId);
|
||||||
if (arena == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
|
||||||
if (arena.getCurrentStage() != ArenaStage.PRE_STAGING) {
|
if (arena.getCurrentStage() != ArenaStage.PRE_STAGING) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Arena is already started.");
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Arena is already started.");
|
||||||
}
|
}
|
||||||
|
@ -67,10 +77,12 @@ public class ArenaStore {
|
||||||
public void cleanArenas() {
|
public void cleanArenas() {
|
||||||
Set<UUID> removalSet = new HashSet<>();
|
Set<UUID> removalSet = new HashSet<>();
|
||||||
final Instant cutoff = Instant.now().minus(5, ChronoUnit.MINUTES);
|
final Instant cutoff = Instant.now().minus(5, ChronoUnit.MINUTES);
|
||||||
|
final Instant hardCutoff = Instant.now().minus(1, ChronoUnit.HOURS);
|
||||||
for (var arena : arenas.values()) {
|
for (var arena : arenas.values()) {
|
||||||
if (
|
if (
|
||||||
(arena.getCurrentStage() == ArenaStage.CLOSED) ||
|
(arena.getCurrentStage() == ArenaStage.CLOSED) ||
|
||||||
(arena.getCurrentStage() == ArenaStage.PRE_STAGING && arena.getCreatedAt().isBefore(cutoff))
|
(arena.getCurrentStage() == ArenaStage.PRE_STAGING && arena.getCreatedAt().isBefore(cutoff)) ||
|
||||||
|
(arena.getCreatedAt().isBefore(hardCutoff))
|
||||||
) {
|
) {
|
||||||
removalSet.add(arena.getId());
|
removalSet.add(arena.getId());
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,10 @@ public class Arena {
|
||||||
this.closesAt = battleEndsAt.plus(1, ChronoUnit.MINUTES);
|
this.closesAt = battleEndsAt.plus(1, ChronoUnit.MINUTES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return currentStage != ArenaStage.PRE_STAGING && currentStage != ArenaStage.CLOSED;
|
||||||
|
}
|
||||||
|
|
||||||
private void close() {
|
private void close() {
|
||||||
for (var client : clients.values()) {
|
for (var client : clients.values()) {
|
||||||
client.getNetManager().shutdown();
|
client.getNetManager().shutdown();
|
||||||
|
|
|
@ -8,3 +8,5 @@ server.tomcat.processor-cache=50
|
||||||
starship-arena.gateway.host=127.0.0.1
|
starship-arena.gateway.host=127.0.0.1
|
||||||
starship-arena.gateway.tcp-port=8081
|
starship-arena.gateway.tcp-port=8081
|
||||||
starship-arena.gateway.udp-port=8082
|
starship-arena.gateway.udp-port=8082
|
||||||
|
starship-arena.gateway.init-timeout-ms=1000
|
||||||
|
starship-arena.max-arenas=100
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
{
|
||||||
|
"name": "Corvette",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"type": "panel",
|
||||||
|
"name": "Main Fuselage",
|
||||||
|
"mass": 5000,
|
||||||
|
"points": [
|
||||||
|
{"x": 0.3, "y": 0.6},
|
||||||
|
{"x": 0.2, "y": 0.1},
|
||||||
|
{"x": 0.1, "y": 0.5},
|
||||||
|
{"x": 0.2, "y": 0.8},
|
||||||
|
{"x": 0.8, "y": 0.8},
|
||||||
|
{"x": 0.9, "y": 0.5},
|
||||||
|
{"x": 0.8, "y": 0.1},
|
||||||
|
{"x": 0.7, "y": 0.6}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "panel",
|
||||||
|
"name": "Front Cargo Bay",
|
||||||
|
"mass": 1000,
|
||||||
|
"points": [
|
||||||
|
{"x": 0.4, "y": 0.2},
|
||||||
|
{"x": 0.35, "y": 0.6},
|
||||||
|
{"x": 0.65, "y": 0.6},
|
||||||
|
{"x": 0.6, "y": 0.2}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "cockpit",
|
||||||
|
"mass": 800,
|
||||||
|
"points": [
|
||||||
|
{"x": 0.5, "y": 0.0},
|
||||||
|
{"x": 0.4, "y": 0.2},
|
||||||
|
{"x": 0.6, "y": 0.2}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "gun",
|
||||||
|
"name": "Port-Side Machine Gun",
|
||||||
|
"mass": 500,
|
||||||
|
"location": {"x": 0.15, "y": 0.35},
|
||||||
|
"rotation": 0,
|
||||||
|
"minRotation": -160,
|
||||||
|
"maxRotation": 5,
|
||||||
|
"barrelWidth": 0.02,
|
||||||
|
"barrelLength": 0.2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "gun",
|
||||||
|
"name": "Starboard-Side Machine Gun",
|
||||||
|
"mass": 500,
|
||||||
|
"location": {"x": 0.85, "y": 0.35},
|
||||||
|
"rotation": 0,
|
||||||
|
"minRotation": -5,
|
||||||
|
"maxRotation": 160,
|
||||||
|
"barrelWidth": 0.02,
|
||||||
|
"barrelLength": 0.2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue