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