-
+
diff --git a/html/webapp/index.html b/html/webapp/index.html
index 0f5fc84b..aaa02109 100644
--- a/html/webapp/index.html
+++ b/html/webapp/index.html
@@ -8,7 +8,7 @@
-
+
diff --git a/lwjgl3/src/main/java/io/streamlines/lwjgl3/StartupHelper.java b/lwjgl3/src/main/java/io/streamlines/lwjgl3/StartupHelper.java
index e691c118..1897e8a2 100644
--- a/lwjgl3/src/main/java/io/streamlines/lwjgl3/StartupHelper.java
+++ b/lwjgl3/src/main/java/io/streamlines/lwjgl3/StartupHelper.java
@@ -18,21 +18,17 @@
import org.lwjgl.system.macosx.LibC;
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.InputStreamReader;
+import java.io.*;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
-/**
- * Adds some utilities to ensure that the JVM was started with the
- * {@code -XstartOnFirstThread} argument, which is required on macOS for LWJGL 3
- * to function. Also helps on Windows when users have names with characters from
- * outside the Latin alphabet, a common cause of startup crashes.
- *
- * Based on this java-gaming.org post by kappa
- * @author damios
- */
+/// Adds some utilities to ensure that the JVM was started with the
+/// `-XstartOnFirstThread` argument, which is required on macOS for LWJGL 3
+/// to function. Also helps on Windows when users have names with characters from
+/// outside the Latin alphabet, a common cause of startup crashes.
+///
+/// Based on this java-gaming.org post by kappa
+/// @author damios
public class StartupHelper {
private static final String JVM_RESTARTED_ARG = "jvmIsRestarted";
@@ -41,30 +37,27 @@ private StartupHelper() {
throw new UnsupportedOperationException();
}
- /**
- * Starts a new JVM if the application was started on macOS without the
- * {@code -XstartOnFirstThread} argument. This also includes some code for
- * Windows, for the case where the user's home directory includes certain
- * non-Latin-alphabet characters (without this code, most LWJGL3 apps fail
- * immediately for those users). Returns whether a new JVM was started and
- * thus no code should be executed.
- *
- * Usage:
- *
- *
- * public static void main(String... args) {
- * if (StartupHelper.startNewJvmIfRequired(true)) return; // This handles macOS support and helps on Windows.
- * // after this is the actual main method code
- * }
- *
- *
- * @param redirectOutput
- * whether the output of the new JVM should be rerouted to the
- * old JVM, so it can be accessed in the same place; keeps the
- * old JVM running if enabled
- * @return whether a new JVM was started and thus no code should be executed
- * in this one
- */
+ /// Starts a new JVM if the application was started on macOS without the
+ /// `-XstartOnFirstThread` argument. This also includes some code for
+ /// Windows, for the case where the user's home directory includes certain
+ /// non-Latin-alphabet characters (without this code, most LWJGL3 apps fail
+ /// immediately for those users). Returns whether a new JVM was started and
+ /// thus no code should be executed.
+ ///
+ /// Usage:
+ ///
+ /// public static void main(String... args) {
+ /// if (StartupHelper.startNewJvmIfRequired(true)) return; // This handles macOS support and helps on Windows.
+ /// // after this is the actual main method code
+ /// }
+ ///
+ ///
+ /// @param redirectOutput
+ /// whether the output of the new JVM should be rerouted to the
+ /// old JVM, so it can be accessed in the same place; keeps the
+ /// old JVM running if enabled
+ /// @return whether a new JVM was started and thus no code should be executed
+ /// in this one
public static boolean startNewJvmIfRequired(boolean redirectOutput) {
String osName = System.getProperty("os.name").toLowerCase();
if (!osName.contains("mac")) {
@@ -151,24 +144,21 @@ public static boolean startNewJvmIfRequired(boolean redirectOutput) {
return true;
}
- /**
- * Starts a new JVM if the application was started on macOS without the
- * {@code -XstartOnFirstThread} argument. Returns whether a new JVM was
- * started and thus no code should be executed. Redirects the output of the
- * new JVM to the old one.
- *
- * Usage:
- *
- *
- * public static void main(String... args) {
- * if (StartupHelper.startNewJvmIfRequired()) return; // This handles macOS support and helps on Windows.
- * // the actual main method code
- * }
- *
- *
- * @return whether a new JVM was started and thus no code should be executed
- * in this one
- */
+ /// Starts a new JVM if the application was started on macOS without the
+ /// `-XstartOnFirstThread` argument. Returns whether a new JVM was
+ /// started and thus no code should be executed. Redirects the output of the
+ /// new JVM to the old one.
+ ///
+ /// Usage:
+ ///
+ /// public static void main(String... args) {
+ /// if (StartupHelper.startNewJvmIfRequired()) return; // This handles macOS support and helps on Windows.
+ /// // the actual main method code
+ /// }
+ ///
+ ///
+ /// @return whether a new JVM was started and thus no code should be executed
+ /// in this one
public static boolean startNewJvmIfRequired() {
return startNewJvmIfRequired(true);
}
diff --git a/server/build.gradle b/server/build.gradle
index 798a3f35..5de9bb17 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -1,8 +1,8 @@
apply plugin: 'application'
apply plugin: 'java'
mainClassName = 'io.streamlines.network.ServerLauncher'
-java.sourceCompatibility = 23
-java.targetCompatibility = 23
+java.sourceCompatibility = serverJdkVersion
+java.targetCompatibility = serverJdkVersion
sourceSets.test.java.srcDirs = [ "src/test/java/" ]
configurations {
@@ -10,7 +10,7 @@ configurations {
}
java {
toolchain {
- languageVersion = JavaLanguageVersion.of(23)
+ languageVersion = JavaLanguageVersion.of(serverJdkVersion)
}
}
diff --git a/server/gradle/wrapper/gradle-wrapper.properties b/server/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..4ff41b6b
--- /dev/null
+++ b/server/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Aug 30 16:57:32 EDT 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/server/src/main/java/io/streamlines/map/AirportGraph.java b/server/src/main/java/io/streamlines/map/AirportGraph.java
index 6f54954b..7af2ec2b 100644
--- a/server/src/main/java/io/streamlines/map/AirportGraph.java
+++ b/server/src/main/java/io/streamlines/map/AirportGraph.java
@@ -6,15 +6,13 @@
import java.util.*;
import java.util.stream.Collectors;
-/**
- * This class represents an undirected graph of airports, set up specifically for Streamlines.
- * The graph is viewed as a collection of vertices, which are sometimes connected by weighted, undirected edges.
- * This graph will never store duplicate vertices. The weights will always be non-negative integers.
- * The WeightedGraph will be capable of performing Floyd's algorithm. This collection is unshrinkable, meaning
- * calling remove will do nothing (to ensure the right behavior for something in {@link GameMap}).
- * @author Varun Singh
- */
-public class AirportGraph extends AbstractCollection {
+/// This class represents an undirected graph of airports, set up specifically for Streamlines.
+/// The graph is viewed as a collection of vertices, which are sometimes connected by weighted, undirected edges.
+/// This graph will never store duplicate vertices. The weights will always be non-negative integers.
+/// The WeightedGraph will be capable of performing Floyd's algorithm. This collection is **unshrinkable**, meaning
+/// calling remove will do nothing (to ensure the right behavior for [GameMap]).
+/// @author Varun Singh
+public class AirportGraph extends AbstractCollection implements AirportGraphable {
/**
* An adjacency list for keeping track of vertices and edges.
@@ -105,12 +103,8 @@ public void addEdge(Airport from, Airport to) {
currentFloyd.runMiniFloyd(airportBiMap.getId(to));
}
- /**
- * Decreases the 'popularity' of an edge
- * @param from The from vertex
- * @param to The to vertex
- */
- public void conditionallyRemoveEdge(Airport from, Airport to) {
+ @Override
+ public void removeEdgeInstance(Airport from, Airport to) {
int popularity = adjacencyMatrix.get(airportBiMap.getId(from)).get(airportBiMap.getId(to));
adjacencyMatrix.get(airportBiMap.getId(from)).set(airportBiMap.getId(to), popularity - 1);
adjacencyMatrix.get(airportBiMap.getId(from)).set(airportBiMap.getId(to), popularity - 1);
diff --git a/server/src/main/java/io/streamlines/map/Spawner.java b/server/src/main/java/io/streamlines/map/Spawner.java
index 3d223b39..47d4a8b9 100644
--- a/server/src/main/java/io/streamlines/map/Spawner.java
+++ b/server/src/main/java/io/streamlines/map/Spawner.java
@@ -1,18 +1,16 @@
package io.streamlines.map;
import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.*;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Disposable;
import io.streamlines.StreamlinesLogger;
import io.streamlines.StringUtilities;
import io.streamlines.flight.*;
import io.streamlines.inventory.*;
import io.streamlines.network.*;
-import java.time.Instant;
-import java.util.Timer;
import java.util.*;
import java.util.function.Consumer;
-import java.util.stream.Collectors;
/**
* Spawns passengers onto the map. Keeps track of airports so passengers can be assigned to connections.
@@ -22,7 +20,6 @@ public class Spawner extends GameMap implements Disposable {
private final Broadcastable broadcaster;
private final Timer timer;
private int passengerSpawnRate = 50;
- private final PeerListener peerListener;
private final Consumer pathFinder;
@@ -31,16 +28,15 @@ public Spawner(Broadcastable broadcaster) {
airports = new AirportGraph(INITIAL_AIRPORTS);
this.broadcaster = broadcaster;
timer = new Timer("spawner");
- peerListener = new PeerListener(this);
timeAtDayStart = System.currentTimeMillis() / 1000;
pathFinder = passenger -> {
- passenger.setConnections(getAirports().getCurrentFloyd().getLowestCostPath(passenger.getStart(),
+ passenger.setConnections(getGraph().getCurrentFloyd().getLowestCostPath(passenger.getStart(),
passenger.getFinalDestination()));
};
}
@Override
- public AirportGraph getAirports() {
+ public AirportGraph getGraph() {
return (AirportGraph) super.getAirports();
}
@@ -54,13 +50,13 @@ public int timeSinceDayStart() {
return (int) ((System.currentTimeMillis() / 1000) - timeAtDayStart);
}
- public void start() {
- createAirports(false);
+ public void start(Random generator) {
+ createAirports(false, generator);
assignPlayersInitialTerminals();
TimerTask spawnAirports = new TimerTask() {
@Override
public void run() {
- createAirports(true);
+ createAirports(true, generator);
passengerSpawnRate = 5 * airports.size();
}
};
@@ -71,17 +67,12 @@ public void run() {
public void run() {
int newDay = nextDay();
StreamlinesLogger.logger.info("DAY-CYCLE", "New Day broadcasting: {}", newDay);
- JsonValue dayData = new JsonValue(JsonValue.ValueType.object);
- dayData.addChild("day", new JsonValue(newDay));
- dayData.addChild("time", new JsonValue(0));
- peerListener.writePacket(dayData);
- broadcaster.broadcast(Packet.PacketType.DAY_DATA, dayData.toJson(JsonWriter.OutputType.json));
- peerListener.handleNewDay();
-
- JsonValue data = new JsonValue(JsonValue.ValueType.object);
- data.addChild("action", new JsonValue("identify"));
- data.addChild("id", new JsonValue(getTopDog()));
- broadcaster.broadcast(Packet.PacketType.TOP_DOG, data.toJson(JsonWriter.OutputType.json));
+ var dayData = new NewDayDto(newDay, 0);
+ broadcaster.broadcast(Packet.PacketType.DAY_DATA, dayData);
+ dayData.apply(Spawner.this, false);
+
+ var topDogData = new SetTopDogDto(getTopDog());
+ broadcaster.broadcast(Packet.PacketType.TOP_DOG_IDENTIFY, topDogData);
}
}, 0, GameMap.DAY_RATE * 1000);
@@ -92,14 +83,14 @@ public void run() {
// Double for hotspots
int spawnRate = Math.round(start.getPopularity() * passengerSpawnRate);
for (int i = 0; i < spawnRate; i++) {
- Airport end = getAirports().chooseRandomAirport();
+ Airport end = getGraph().chooseRandomAirport();
// If we accidentally generated the same airport twice, find another airport
while (start.equals(end)) {
- end = getAirports().chooseRandomAirport();
+ end = getGraph().chooseRandomAirport();
}
Passenger passenger = new Passenger(start, end);
- Deque journey = getAirports().getCurrentFloyd().getLowestCostPath(start, end);
+ Deque journey = getGraph().getCurrentFloyd().getLowestCostPath(start, end);
if (journey != null) {
journey.removeFirst();
passenger.setConnections(journey);
@@ -126,21 +117,21 @@ public void run() {
TimerTask broadcastAirportTraffic = new TimerTask() {
@Override
public void run() {
- JsonValue packet = new JsonValue(JsonValue.ValueType.object);
+ var list = new Array();
for (Airport airport : getAirports()) {
- packet.addChild(airport.getTrafficDetails());
+ list.add(airport.getTrafficDetails());
}
- broadcaster.broadcast(Packet.PacketType.UPDATE_TRAFFIC, packet.toJson(JsonWriter.OutputType.json));
+
+ broadcaster.broadcast(Packet.PacketType.UPDATE_TRAFFIC, new UpdateTrafficListDto(list));
}
};
- timer.scheduleAtFixedRate(broadcastAirportTraffic, 2000, 5000);
+ timer.scheduleAtFixedRate(broadcastAirportTraffic, 2 * 1000, 20 * 1000);
Weather weather = new Weather(this, broadcaster, timer);
weather.start();
}
- private void createAirports(boolean includeHotspots) {
- Random airportGenerator = new Random();
+ private void createAirports(boolean includeHotspots, Random airportGenerator) {
Airport[] newAirports = new Airport[INITIAL_AIRPORTS];
for (int i = 0; i < INITIAL_AIRPORTS; i++) {
final Vector2 position = randomAirportLocation(airportGenerator);
@@ -163,60 +154,55 @@ private void createAirports(boolean includeHotspots) {
AirplaneExchangeObserver observer = new AirplaneExchangeObserver() {
@Override
public void notifyTakeoff(Airplane airplane) {
- JsonValue id = new JsonValue(airplane.getId());
- JsonValue obj = new JsonValue(JsonValue.ValueType.object);
- obj.addChild("id", id);
- obj.addChild("action", new JsonValue("takeoff"));
- broadcaster.broadcast(new Packet(Packet.PacketType.AIRPLANE_DATA, obj.toJson(JsonWriter.OutputType.json)).getContents());
+ var data = new AirplaneSystemActionDto(airplane.getId(), AirplaneSystemAction.TAKEOFF);
+ broadcaster.broadcast(Packet.PacketType.AIRPLANE_SYSTEM_ACTION, data);
}
@Override
public void notifyLanding(Airplane airplane, boolean surpassed) {
String playerId = airplane.getOwner();
- JsonValue obj = new JsonValue(JsonValue.ValueType.object);
- obj.addChild("throughput", new JsonValue(airplane.getOccupancy()));
- obj.addChild("player", new JsonValue(playerId));
- obj.addChild("airport", new JsonValue(newAirport.getId()));
- obj.addChild("airplane", new JsonValue(airplane.getId()));
- peerListener.writePacket(obj);
- boolean success = peerListener.handleLanding(false);
+ LandingDto dto = new LandingDto(airplane.getOccupancy(), newAirport.getId(), airplane.getId(), playerId);
+ boolean success = dto.apply(Spawner.this, false);
if (!success) return;
- broadcaster.broadcast(Packet.PacketType.UNLOAD, obj.toJson(JsonWriter.OutputType.json));
+ broadcaster.broadcast(Packet.PacketType.UNLOAD, dto);
if (!surpassed) return;
- JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object);
- jsonValue.addChild("action", new JsonValue("prompt"));
- jsonValue.addChild("player", new JsonValue(playerId));
// Populate double quota powerups
ArrayList doubleQuotaPowerups = new ArrayList<>(Powerup.getDQPowerupCount());
- Random rand = new Random(Instant.now().toEpochMilli());
for (int i = 0; i < Powerup.getDQPowerupCount(); i++) {
- doubleQuotaPowerups.add(ServerLauncher.randomItemRequest(rand, Powerup.class, doubleQuotaPowerups));
+ doubleQuotaPowerups.add(
+ randomItemRequest(airportGenerator, Powerup.class, doubleQuotaPowerups)
+ );
}
- jsonValue.addChild("powerups", new JsonValue(doubleQuotaPowerups.stream()
- .map(InventoryItemType::name)
- .collect(Collectors.joining(","))));
// Populate double quota infrastructure
ArrayList doubleQuotaInfrastructure = new ArrayList<>(Powerup.getDQInfrastructureCount());
doubleQuotaInfrastructure.add(InventoryItemType.Empty_Infrastructure);
for (int i = 0; i < Powerup.getDQInfrastructureCount(); i++) {
- doubleQuotaInfrastructure.add(ServerLauncher.randomItemRequest(rand, InfrastructureItem.class, doubleQuotaInfrastructure));
+ doubleQuotaInfrastructure.add(
+ randomItemRequest(airportGenerator, InfrastructureItem.class, doubleQuotaInfrastructure)
+ );
}
doubleQuotaInfrastructure.remove(InventoryItemType.Empty_Infrastructure);
- jsonValue.addChild("infrastructure", new JsonValue(doubleQuotaInfrastructure.stream()
- .map(InventoryItemType::name)
- .collect(Collectors.joining(","))));
+ var surpassedPromptDto = new SurpassedPromptDto(doubleQuotaPowerups, doubleQuotaInfrastructure);
- broadcaster.broadcast(Packet.PacketType.SURPASSED, jsonValue.toJson(JsonWriter.OutputType.json));
+ broadcaster.broadcast(Packet.PacketType.SURPASSED_PROMPT, surpassedPromptDto);
}
};
newAirport.registerObserver(observer);
addAirport(newAirport);
newAirports[i] = newAirport;
}
- broadcaster.broadcast(Packet.PacketType.AIRPORT_DATA, Airport.serializeCollection(List.of(newAirports)));
+ broadcaster.broadcast(Packet.PacketType.AIRPORT_DATA, new AirportListDto(newAirports));
+ }
+
+ static InventoryItemType randomItemRequest(Random generator, Class extends InventoryItem> itemBase, ArrayList blacklist) {
+ List allItems = Inventory.getAllItems();
+ allItems.remove(InventoryItemType.Place_Route); // Ignored
+ final List chooseableItems = allItems.stream()
+ .filter(item -> !blacklist.contains(item) && itemBase.isAssignableFrom(item.getInventoryClass())).toList();
+ return chooseableItems.get(generator.nextInt(chooseableItems.size()));
}
private Vector2 randomAirportLocation(Random rand) {
@@ -270,25 +256,21 @@ public void assignPlayerInitialTerminals(String id) {
}
private void assignRandomTerminal(String playerId) {
- Airport first = getAirports().chooseRandomAirport(Airport.AirportType.LOCAL);
+ Airport first = getGraph().chooseRandomAirport(Airport.AirportType.LOCAL);
while (!first.requestTerminal(playerId)) { // Request terminals until we get a "yes"
- first = getAirports().chooseRandomAirport(Airport.AirportType.LOCAL);
+ first = getGraph().chooseRandomAirport(Airport.AirportType.LOCAL);
}
Terminal assigned = first.findTerminalByOwner(playerId).orElseThrow();
assigned.validate();
}
- public void removeOneFlight(Airport a, Airport b) {
- getAirports().conditionallyRemoveEdge(a, b);
- }
-
/**
* Marks a two-way path from airport a to airport b.
* @param a The start airport
* @param b The end airport
*/
public void addRoute(Airport a, Airport b) {
- getAirports().addEdge(a, b);
+ getGraph().addEdge(a, b);
}
@Override
diff --git a/server/src/main/java/io/streamlines/map/Weather.java b/server/src/main/java/io/streamlines/map/Weather.java
index 807ad884..8fc0bd5b 100644
--- a/server/src/main/java/io/streamlines/map/Weather.java
+++ b/server/src/main/java/io/streamlines/map/Weather.java
@@ -1,7 +1,5 @@
package io.streamlines.map;
-import com.badlogic.gdx.utils.JsonValue;
-import com.badlogic.gdx.utils.JsonWriter;
import io.streamlines.StreamlinesLogger;
import io.streamlines.flight.Airport;
import io.streamlines.network.*;
@@ -22,7 +20,6 @@ public class Weather extends TimerTask {
public final static long WEATHER_EVENT_LENGTH = 30000;
private final Timer timer;
private final Broadcastable broadcaster;
- private final PeerListener peerListener;
/**
* Constructs a weather event. Does not start it.
@@ -33,7 +30,6 @@ public Weather(Spawner map, Broadcastable broadcastable, Timer tim) {
rand = new Random();
timer = tim;
broadcaster = broadcastable;
- peerListener = new PeerListener(map);
}
/**
@@ -53,27 +49,20 @@ public void run() {
Airport.AirportType.LOCAL :
Airport.AirportType.HOTSPOT;
- // Find a random airport of required type. Includes airports already in a weather event.
- Airport a = gameMap.getAirports().chooseRandomAirport(airportType);
+ // Includes airports already in a weather event.
+ Airport a = gameMap.getGraph().chooseRandomAirport(airportType);
- // How serious is the event?
var severity = rand.nextBoolean() ? WeatherEventSeverity.INCONVENIENCE : WeatherEventSeverity.DISASTER;
- JsonValue data = new JsonValue(JsonValue.ValueType.object);
- data.addChild("status", new JsonValue(severity.ordinal()));
- data.addChild("airport", new JsonValue(a.getId()));
- peerListener.writePacket(data);
- peerListener.handleWeatherEvent();
- broadcaster.broadcast(Packet.PacketType.WEATHER_EVENT, data.toJson(JsonWriter.OutputType.javascript));
+ WeatherDto data = new WeatherDto(severity, a.getId());
+ data.apply(gameMap, false);
+ broadcaster.broadcast(Packet.PacketType.WEATHER_EVENT, data);
TimerTask t = new TimerTask() {
@Override
public void run() {
- JsonValue data = new JsonValue(JsonValue.ValueType.object);
- data.addChild("status", new JsonValue(a.getWeatherStatus().ordinal()));
- data.addChild("airport", new JsonValue(a.getId()));
- peerListener.writePacket(data);
- peerListener.handleWeatherEvent();
- broadcaster.broadcast(Packet.PacketType.WEATHER_EVENT, data.toJson(JsonWriter.OutputType.javascript));
+ var data = new WeatherDto(a.getWeatherStatus(), a.getId());
+ data.apply(gameMap, false);
+ broadcaster.broadcast(Packet.PacketType.WEATHER_EVENT, data);
}
};
diff --git a/server/src/main/java/io/streamlines/network/ServerLauncher.java b/server/src/main/java/io/streamlines/network/ServerLauncher.java
index 3e418f2a..e539243e 100644
--- a/server/src/main/java/io/streamlines/network/ServerLauncher.java
+++ b/server/src/main/java/io/streamlines/network/ServerLauncher.java
@@ -1,11 +1,9 @@
package io.streamlines.network;
-import com.badlogic.gdx.utils.JsonValue;
+import com.badlogic.gdx.utils.Json;
import com.badlogic.gdx.utils.JsonWriter;
import io.streamlines.*;
-import io.streamlines.flight.Airplane;
import io.streamlines.flight.Airport;
-import io.streamlines.inventory.*;
import io.streamlines.map.*;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
@@ -19,8 +17,7 @@
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer;
-import java.util.List;
-import java.util.*;
+import java.util.Random;
import java.util.stream.Collectors;
import static io.streamlines.StreamlinesLogger.logger;
@@ -34,7 +31,7 @@ public class ServerLauncher extends WebSocketServer implements Broadcastable {
private boolean someoneJoined = false;
private final PIDTracker pidTracker;
- private final PeerListener peerListener;
+ private final Json json;
public ServerLauncher(int port) throws UnknownHostException {
super(new InetSocketAddress(port));
@@ -42,7 +39,7 @@ public ServerLauncher(int port) throws UnknownHostException {
getPort());
gameMap = new Spawner(this);
pidTracker = new PIDTracker(4);
- peerListener = new PeerListener(gameMap);
+ json = new Json(JsonWriter.OutputType.javascript);
}
@Override
@@ -53,32 +50,29 @@ public void onOpen(WebSocket conn, ClientHandshake handshake) {
sendPacketTo(Packet.PacketType.INITIALIZED, "Server is ready!", conn);
}
- logger.info("SERVER-LAUNCHER",
- conn.getRemoteSocketAddress().getAddress().getHostName() + " entered the game!");
- JsonValue dayData = new JsonValue(JsonValue.ValueType.object);
- dayData.addChild("day", new JsonValue(gameMap.getDay()));
- dayData.addChild("time", new JsonValue(gameMap.timeSinceDayStart()));
- sendPacketTo(Packet.PacketType.DAY_DATA, dayData.toJson(JsonWriter.OutputType.json), conn);
+ var host = conn.getRemoteSocketAddress().getAddress().getHostName();
+ logger.info("SERVER-LAUNCHER", host + " entered the game!");
+ var newDayDto = new NewDayDto(gameMap.getDay(), gameMap.timeSinceDayStart());
+ sendPacketTo(Packet.PacketType.DAY_DATA, newDayDto, conn);
}
- boolean setUpNewPlayer(WebSocket conn, String name) {
+ private boolean setUpNewPlayer(WebSocket conn, PlayerNameDto userInfo) {
+ var name = userInfo.name();
logger.info("SERVER", "Received communication from: " + name);
// Assign PID - make client aware of its own identity
String PID = pidTracker.getNewPID();
conn.setAttachment(PID);
- JsonValue playerData = new JsonValue(JsonValue.ValueType.object);
- playerData.addChild("id", new JsonValue(PID));
- playerData.addChild("name", new JsonValue(name));
- Player newPlayer = gameMap.newPlayer(PID, name);
- playerData.addChild("color", new JsonValue(newPlayer.getColor().toString()));
- sendPacketTo(Packet.PacketType.PID_ASSIGNMENT, playerData.toJson(JsonWriter.OutputType.json), conn);
+ var newPlayer = gameMap.newPlayer(PID, name);
+ var playerData = new PidAssignmentDto(PID, name, newPlayer.getColor());
+ sendPacketTo(Packet.PacketType.PID_ASSIGNMENT, playerData, conn);
if (isReady) {
// This will also be sent later if the player joins before everything (including airports) is initialized
gameMap.assignPlayerInitialTerminals(PID);
}
// Broadcast to everyone so other players can see this one's initial game state
- broadcast(Packet.PacketType.AIRPORT_DATA, Airport.serializeCollection(gameMap.getAirports()));
+ broadcast(Packet.PacketType.AIRPORT_DATA,
+ new AirportListDto(gameMap.getAirports().toArray(Airport[]::new)));
// Send this player to all other players (syncs game state)
getConnections().stream().filter(item -> !item.equals(conn)).forEach(item -> {
@@ -102,7 +96,12 @@ boolean setUpNewPlayer(WebSocket conn, String name) {
*/
@Override
public void broadcast(Packet.PacketType type, String data) {
- broadcast(new Packet(type, data).getContents());
+ broadcast(new Packet(type, data).contents());
+ }
+
+ @Override
+ public void broadcast(Packet.PacketType packetType, Serializable data) {
+ broadcast(new Packet(packetType, data).contents());
}
/**
@@ -112,17 +111,18 @@ public void broadcast(Packet.PacketType type, String data) {
* @param conn The socket connection
*/
private void sendPacketTo(Packet.PacketType type, String data, WebSocket conn) {
- conn.send(new Packet(type, data).getContents());
+ conn.send(new Packet(type, data).contents());
+ }
+
+ private void sendPacketTo(Packet.PacketType type, Serializable dto, WebSocket conn) {
+ conn.send(new Packet(type, dto).contents());
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
- JsonValue data = new JsonValue(JsonValue.ValueType.object);
- data.addChild("player", new JsonValue(conn.getAttachment()));
- broadcast(Packet.PacketType.UNASSIGNED, data.toJson(JsonWriter.OutputType.javascript));
- peerListener.writePacket(data);
- peerListener.handlePlayerLoss(new PeerListener.LosePlayer(data.getString("player")));
-
+ var dto = new LosePlayerDto(conn.getAttachment());
+ broadcast(Packet.PacketType.UNASSIGNED, dto);
+ dto.apply(gameMap, false);
pidTracker.deallocatePID(conn.getAttachment());
logger.info("SERVER", "{} has left the game -- {} ({})", conn.getAttachment(), reason, code);
}
@@ -139,46 +139,30 @@ public void onMessage(WebSocket conn, String message) {
}
System.exit(0);
}
- logger.info("SERVER", "{}: {}", conn.getRemoteSocketAddress().getHostName(), message);
Packet packet = new Packet(message);
- JsonValue params = peerListener.readPacket(packet);
+ String data = packet.getData();
+ logger.info("SERVER-MESSAGE", "{}: {} (PACKET-TYPE: {})",
+ conn.getRemoteSocketAddress().getHostName(),
+ packet.getData(),
+ packet.getPacketType()
+ );
if (packet.getPacketType() == null) return;
// Only certain packets should be bounced back to the other clients
boolean bounce = switch (packet.getPacketType()) {
- case AIRPLANE_DATA -> {
- if (params.has("action")) {
- peerListener.handleAirplaneData();
- }
- yield true;
- }
- case TERMINAL_DATA -> peerListener.handleTerminalData();
- case PATH_EDIT -> peerListener.editPath();
- case NEW_PATH -> peerListener.newPath();
- case PATH_RMV -> peerListener.removeAirportFromPath();
- case NEW_BID -> peerListener.handleNewBid();
- case TOP_DOG -> peerListener.handleTopDog();
- case REQUEST_PASSENGER_COUNT -> {
- String planeId = params.getString("plane_id");
- Airplane plane = gameMap.getAirplaneById(planeId);
- if (plane == null) {
- StreamlinesLogger.logger.warn("Request passenger count for plane that does not exist");
- yield false;
- } else if (!plane.getOwner().equals(params.getString("player"))) {
- StreamlinesLogger.logger.warn("Passenger count request unauthorized for player other than the one" +
- " who owns the airplane");
- yield false;
- }
- int passengerCount = plane.getOccupancy();
-
- JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object);
- jsonValue.addChild("p_count", new JsonValue(passengerCount));
- jsonValue.addChild("plane_id", new JsonValue(planeId));
- sendPacketTo(Packet.PacketType.RETURN_PASSENGER_COUNT, jsonValue.toJson(JsonWriter.OutputType.json), conn);
- yield false;
- }
- case SURPASSED -> peerListener.addNewInventoryItems();
- case PLAYER_NAME -> setUpNewPlayer(conn, packet.parse().getString("pname"));
+ case AIRPLANE_USER_INVENTORY_ACTION -> parseAndApplyDto(AirplaneUserInventoryActionDto.class, data);
+ case TERMINAL_USER_INVENTORY_ACTION -> parseAndApplyDto(TerminalUserInventoryActionDto.class, data);
+ case GAMBLE_TERMINAL -> parseAndApplyDto(GambleTerminalDto.class, data);
+ case PATH_EDIT -> parseAndApplyDto(EditPathDto.class, data);
+ case NEW_PATH -> parseAndApplyDto(NewPathDto.class, data);
+ case PATH_RMV -> parseAndApplyDto(RemovePathDto.class, data);
+ case NEW_BID -> parseAndApplyDto(NewBidDto.class, data);
+ case TOP_DOG_PLACE_AIRPLANE -> parseAndApplyDto(TopDogPlaceAirplaneDto.class, data);
+ case TOP_DOG_PLACE_ROUTE -> parseAndApplyDto(TopDogPlaceRouteDto.class, data);
+ case TOP_DOG_IDENTIFY -> parseAndApplyDto(SetTopDogDto.class, data);
+ case REQUEST_PASSENGER_COUNT -> returnPassengerCount(data, conn);
+ case SURPASSED_CHOSE -> parseAndApplyDto(SurpassedChoseDto.class, data);
+ case PLAYER_NAME -> setUpNewPlayer(conn, json.fromJson(PlayerNameDto.class, data));
default -> false;
};
if (bounce) {
@@ -186,12 +170,26 @@ public void onMessage(WebSocket conn, String message) {
}
}
- public static InventoryItemType randomItemRequest(Random random, Class extends InventoryItem> itemBase, ArrayList blacklist) {
- List allItems = Inventory.getAllItems();
- allItems.remove(InventoryItemType.Place_Route); // Ignored
- final List chooseableItems = allItems.stream()
- .filter(item -> !blacklist.contains(item) && itemBase.isAssignableFrom(item.getInventoryClass())).toList();
- return chooseableItems.get(random.nextInt(chooseableItems.size()));
+ private boolean parseAndApplyDto(Class type, String data) {
+ return json.fromJson(type, data).apply(gameMap, false);
+ }
+
+ private boolean returnPassengerCount(String message, WebSocket conn) {
+ var dto = json.fromJson(RequestPassengerCountDto.class, message);
+ var plane = gameMap.getPlayer(dto.player()).getAirplaneById(dto.planeId());
+ if (plane.isEmpty()) {
+ StreamlinesLogger.logger.warn("Request passenger count for plane that does not exist");
+ return false;
+ } else if (!plane.get().getOwner().equals(dto.player())) {
+ StreamlinesLogger.logger.warn("Passenger count request unauthorized for player other than the one" +
+ " who owns the airplane");
+ }
+
+ // Fall back to 0
+ int passengerCount = plane.get().getOccupancy();
+ var returnDto = new ReturnPassengerCountDto(dto.planeId(), passengerCount);
+ sendPacketTo(Packet.PacketType.RETURN_PASSENGER_COUNT, returnDto, conn);
+ return false;
}
@Override
@@ -205,9 +203,9 @@ public void onMessage(WebSocket conn, ByteBuffer message) {
* listen for messages from stdin to send to clients
*/
void startGame() {
+ Random random = new Random();
// Create random terrain
- int[] v = GameMap.getMapDimensions();
- Terrain randomTerrain = new Terrain(v[0],v[1]);
+ Terrain randomTerrain = new Terrain(GameMapAccessible.MAP_WIDTH,GameMapAccessible.MAP_HEIGHT);
try {
exportTerrainImage(randomTerrain.getTerrainMap(), GameMap.TERRAIN_TEXTURE_PATH, () -> {
@@ -221,7 +219,7 @@ void startGame() {
throw new RuntimeException(e);
}
- gameMap.start();
+ gameMap.start(random);
logger.info("SERVER", "Server is ready!");
isReady = true;
broadcast(Packet.PacketType.INITIALIZED, "Server is ready!");
@@ -259,12 +257,12 @@ public static void main(String[] args) {
private final Logger logger = LoggerFactory.getLogger(ServerLauncher.class);
@Override
public void debug(String tag, String message) {
- logger.info(String.format("[%s]: %s", tag, message));
+ logger.info("[{}]: {}", tag, message);
}
@Override
public void info(String tag, String message) {
- logger.info(String.format("[%s]: %s", tag, message));
+ logger.info("[{}]: {}", tag, message);
}
@Override
diff --git a/server/src/test/java/io/streamlines/map/PlayerTest.java b/server/src/test/java/io/streamlines/map/PlayerParseTest.java
similarity index 95%
rename from server/src/test/java/io/streamlines/map/PlayerTest.java
rename to server/src/test/java/io/streamlines/map/PlayerParseTest.java
index 6a0ece42..f1f7844d 100644
--- a/server/src/test/java/io/streamlines/map/PlayerTest.java
+++ b/server/src/test/java/io/streamlines/map/PlayerParseTest.java
@@ -7,10 +7,12 @@
import io.streamlines.network.Packet;
import org.junit.jupiter.api.*;
+import java.io.Serializable;
+
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
-public class PlayerTest {
+public class PlayerParseTest {
private GameMap gameMap;
private static final String PLAYER_ID = "ABC";
@@ -40,6 +42,10 @@ public void broadcast(String data) {}
@Override
public void broadcast(Packet.PacketType packetType, String data) {}
+
+ @Override
+ public void broadcast(Packet.PacketType packetType, Serializable data) {
+ }
});
Player player = gameMap.newPlayer(PLAYER_ID);
assertEquals(player, player.parse(player.serialize()));
@@ -119,6 +125,6 @@ public void parseCyclicalPath_isCorrect() {
@Test
public void testChooseNonBlendingColor() { // makes sure it doesn't crash
Player player = new Player(new GameMap(), "ABCD");
- player.getColor();
+ var _ = player.getColor();
}
}
diff --git a/server/src/test/java/io/streamlines/map/SpawnerTest.java b/server/src/test/java/io/streamlines/map/SpawnerTest.java
index 7f45f05e..48ee42dd 100644
--- a/server/src/test/java/io/streamlines/map/SpawnerTest.java
+++ b/server/src/test/java/io/streamlines/map/SpawnerTest.java
@@ -1,14 +1,25 @@
package io.streamlines.map;
-import io.streamlines.network.Broadcastable;
-import org.junit.jupiter.api.Test;
+import io.streamlines.inventory.InventoryItemType;
+import io.streamlines.inventory.Powerup;
+import org.junit.jupiter.api.RepeatedTest;
-import static org.mockito.Mockito.mock;
+import java.util.ArrayList;
+import java.util.Random;
+
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
public class SpawnerTest {
- @Test
- public void test() {
- Broadcastable broadcaster = mock();
- Spawner spawner = new Spawner(broadcaster);
+ @RepeatedTest(value = 3)
+ public void testChooseRandomItem() {
+ Random random = new Random();
+ InventoryItemType item1 = Spawner.randomItemRequest(random, Powerup.class, new ArrayList<>(0));
+ assertNotNull(item1);
+ ArrayList blacklist = new ArrayList<>(1);
+ blacklist.add(item1);
+ InventoryItemType item2 = Spawner.randomItemRequest(random, Powerup.class, blacklist);
+ assertNotNull(item2);
+ assertNotEquals(item1, item2);
}
}
diff --git a/server/src/test/java/io/streamlines/network/ServerLauncherTest.java b/server/src/test/java/io/streamlines/network/ServerLauncherTest.java
deleted file mode 100644
index 436885bd..00000000
--- a/server/src/test/java/io/streamlines/network/ServerLauncherTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package io.streamlines.network;
-
-import io.streamlines.inventory.*;
-import org.junit.jupiter.api.RepeatedTest;
-
-import java.util.ArrayList;
-import java.util.Random;
-
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-
-public class ServerLauncherTest {
- @RepeatedTest(value = 3)
- public void testChooseRandomItem() {
- Random random = new Random();
- InventoryItemType item1 = ServerLauncher.randomItemRequest(random, Powerup.class, new ArrayList<>(0));
- assertNotNull(item1);
- ArrayList blacklist = new ArrayList<>(1);
- blacklist.add(item1);
- InventoryItemType item2 = ServerLauncher.randomItemRequest(random, Powerup.class, blacklist);
- assertNotNull(item2);
- assertNotEquals(item1, item2);
- }
-}
diff --git a/shared/build.gradle b/shared/build.gradle
index 56832307..a1093f31 100644
--- a/shared/build.gradle
+++ b/shared/build.gradle
@@ -2,12 +2,21 @@ repositories {
mavenCentral()
}
+configurations {
+ mockitoAgent
+}
+
dependencies {
api "com.badlogicgames.gdx:gdx:$gdxVersion"
testImplementation platform('org.junit:junit-bom:5.9.1') // JUnit 5
- testImplementation 'org.mockito:mockito-core:5+'
+ // Test mocking library
+ testImplementation libs.mockito
+ mockitoAgent(libs.mockito) {
+ transitive = false
+ }
+ // Unit testing library
testImplementation 'org.junit.jupiter:junit-jupiter'
- // aggregate jqwik dependency
+ // Property-based testing library
testImplementation "net.jqwik:jqwik:${jqwikVersion}"
}
@@ -16,9 +25,27 @@ tasks.withType(JavaCompile).configureEach {
}
test {
+ jvmArgs += "-javaagent:${configurations.mockitoAgent.asPath}"
useJUnitPlatform()
}
+tasks.register("unitTests", Test) {
+ useJUnitPlatform {
+ excludeTags "integ", "ui" // Anything else is assumed to be unit test
+ }
+}
+
+tasks.register("integrationTests", Test) {
+ useJUnitPlatform {
+ includeTags "integ"
+ }
+}
+
compileTestJava {
options.compilerArgs += '-parameters'
}
+
+sourceSets.main.java.srcDirs = [ "src/main/java/" ]
+
+java.sourceCompatibility = JavaVersion.VERSION_17
+java.targetCompatibility = JavaVersion.VERSION_17
diff --git a/shared/src/main/java/io/streamlines/flight/Airplane.java b/shared/src/main/java/io/streamlines/flight/Airplane.java
index 20749f41..f7b64539 100644
--- a/shared/src/main/java/io/streamlines/flight/Airplane.java
+++ b/shared/src/main/java/io/streamlines/flight/Airplane.java
@@ -32,7 +32,8 @@ public class Airplane implements Identifiable, Iterable, Locatable {
*/
private transient List path;
- private int capacity = 15;
+ public static final int INITIAL_CAPACITY = 15;
+ private int capacity = INITIAL_CAPACITY;
private transient final Set passengers;
/**
* Passenger count for this vessel as specified by the server, without any understanding of the passengers actually
@@ -237,7 +238,8 @@ Optional getTerminal() {
@Override
public boolean equals(Object obj) {
- if (!(obj instanceof Airplane other)) return false;
+ if (!(obj instanceof Airplane)) return false;
+ Airplane other = (Airplane) obj;
return other.id.equals(id) && other.path.equals(path) && other.resilient == resilient
&& other.location.equals(location);
}
@@ -261,19 +263,22 @@ private void updatePosition() {
location.set(newX, newY);
}
- /**
- * Goes to the next plane animation frame. If the plane is IN FLIGHT, checks if it is within 5 units of its
- * destination. If so, requests landing and switch to landing mode.
- * If the plane is LANDING, check if the plane has aproned (landed). If so, update the destination and
- * direction, switch to unloading mode. If the plane is landing but hasn't aproned, the terminal will
- * automatically apron the plane once another plane leaves the apron.
- * If the plane is UNLOADING, remove all passengers and notify them that they have completed their
- * flight. Assign passengers at destination to new flights. Switch to loading mode.
- * If the plane is in loading mode, check if the plane is at capacity. If so, leave the terminal and switch to
- * IN_FLIGHT mode.
- *
- * Switches the current destination upon landing in the terminal.
- */
+ /// Goes to the next plane animation frame.
+ ///
+ /// If the plane is IN FLIGHT, checks if it is within 5 units of its
+ /// destination. If so, requests landing and switch to landing mode.
+ ///
+ /// If the plane is LANDING, check if the plane has aproned (landed). If so, update the destination and
+ /// direction, switch to unloading mode. If the plane is landing but hasn't aproned, the terminal will
+ /// automatically apron the plane once another plane leaves the apron.
+ ///
+ /// If the plane is UNLOADING, remove all passengers and notify them that they have completed their
+ /// flight. Assign passengers at destination to new flights. Switch to loading mode.
+ ///
+ /// If the plane is in LOADING mode, check if the plane is at capacity. If so, leave the terminal and switch to
+ /// IN_FLIGHT mode.
+ ///
+ /// Switches the current destination upon landing in the terminal.
public void update() {
if (status == Status.IN_FLIGHT) {
// If airplane is within a distance equivalent to the planeSpeed of the target airport, change state and
diff --git a/shared/src/main/java/io/streamlines/flight/Airport.java b/shared/src/main/java/io/streamlines/flight/Airport.java
index 1394d4c3..fe37f57a 100644
--- a/shared/src/main/java/io/streamlines/flight/Airport.java
+++ b/shared/src/main/java/io/streamlines/flight/Airport.java
@@ -1,12 +1,13 @@
package io.streamlines.flight;
import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.*;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.Json;
import io.streamlines.*;
import io.streamlines.map.WeatherEventObserver;
import io.streamlines.map.WeatherEventSeverity;
+import io.streamlines.network.UpdateTraffic;
-import java.lang.StringBuilder;
import java.util.*;
import java.util.function.Consumer;
@@ -15,7 +16,12 @@
*
* @author Declan Scott, Varun Singh
*/
-public class Airport implements WeatherEventObserver, Identifiable, Locatable {
+public class Airport implements WeatherEventObserver, Identifiable, Locatable, Comparable {
+ @Override
+ public int compareTo(Airport o) {
+ return getId().compareTo(o.getId());
+ }
+
public enum AirportType {
HOTSPOT,
LOCAL
@@ -55,47 +61,47 @@ public void mutatePopularity() {
private transient final List finishPendingObservers;
private int currentTrafficCount;
- public static class DestinationInfo {
- private final String airportCode;
- private final int popularity;
+ public static final class DestinationInfo {
+ private String airportCode;
+ private int popularity;
+ public DestinationInfo() {}
public DestinationInfo(String airportCode, int popularity) {
this.airportCode = airportCode;
this.popularity = popularity;
}
- public DestinationInfo(String encoded) {
- /*
- * Encoded as such:
- * "airportCode"~"popularity"
- */
- String[] split = encoded.split("~");
-// this(split[0],Integer.parseInt(split[1])); (i should really be allowed to do this...)
- this.airportCode = split[0];
- this.popularity = Integer.parseInt(split[1]);
- }
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof DestinationInfo && airportCode.equals(((DestinationInfo) obj).airportCode);
+ }
- public String encode() {
- return airportCode + "~" + popularity;
- }
+ @Override
+ public String toString() {
+ return "DestinationInfo[" +
+ "airportCode=" + airportCode + ", " +
+ "popularity=" + popularity + ']';
+ }
- public String getAirportCode() {
+ public String airportCode() {
return airportCode;
}
- public int getPopularity() {
+ public int popularity() {
return popularity;
}
@Override
- public boolean equals(Object obj) {
- return airportCode.equals(((DestinationInfo) obj).airportCode);
+ public int hashCode() {
+ return Objects.hash(airportCode, popularity);
}
+
+
}
- private transient final List rankedDestinations = new ArrayList<>();
- public DestinationInfo[] getRankedDestinations() {
- return rankedDestinations.toArray(new DestinationInfo[0]);
+ private transient final Array rankedDestinations = new Array<>();
+ public Array getRankedDestinations() {
+ return rankedDestinations;
}
/**
@@ -321,7 +327,7 @@ public void clearWeatherEvent() {
weatherStatus = WeatherEventSeverity.FINE;
}
- public JsonValue getTrafficDetails() {
+ public UpdateTraffic getTrafficDetails() {
ArrayList allPassengers = new ArrayList<>(unassignedPassengers);
allPassengers.addAll(isolatedPassengers);
@@ -340,17 +346,9 @@ public JsonValue getTrafficDetails() {
sortedDests.sort(Comparator.comparing(dests::get));
sortedDests.forEach(x -> rankedDestinations.add(new DestinationInfo(x, dests.get(x))));
- // Send new info to client
- java.lang.StringBuilder destinationEncoding = new StringBuilder();
- for (DestinationInfo dest : rankedDestinations)
- destinationEncoding.append(dest.encode()).append("`");
-
- JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object);
- jsonValue.addChild("airport", new JsonValue(getId()));
- jsonValue.addChild("count", new JsonValue(allPassengers.size() + 1));
- jsonValue.addChild("destinations", new JsonValue(destinationEncoding.toString()));
- jsonValue.setName("traffic-details-" + getId());
- return jsonValue;
+ String airportId = getId();
+ int count = allPassengers.size() + 1;
+ return new UpdateTraffic(airportId, count, rankedDestinations);
}
/**
diff --git a/shared/src/main/java/io/streamlines/flight/PathService.java b/shared/src/main/java/io/streamlines/flight/PathService.java
index 6fe69bd3..1421c48b 100644
--- a/shared/src/main/java/io/streamlines/flight/PathService.java
+++ b/shared/src/main/java/io/streamlines/flight/PathService.java
@@ -10,18 +10,18 @@
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
-/**
- * Represents a drawable path - a set of routes that each share exactly one airport with exactly one other route
- * on this path. Stores and controls all airplanes that are moving on this path.
- * Represented as a linked list
- */
-public class PathService implements Iterable, Disposable, Identifiable, Json.Serializable {
+/// Represents a drawable path - a set of routes that each share exactly one airport with exactly one other route
+/// on this path. Stores and controls all airplanes that are moving on this path.
+/// Represented as a linked list.
+///
+/// When disposed, forces clearing of airplanes and airports.
+public class PathService implements Identifiable, Json.Serializable, Disposable {
private final Set airplanes;
private final List airports;
private boolean cyclical;
private static final AtomicInteger idTracker = new AtomicInteger(0);
- private final String globalId;
+ private String globalId;
private String owner;
@@ -61,15 +61,13 @@ public String getId() {
* Sets the new path for all airplanes.
* @param airport The airport getting removed from the path
*/
- public void removeAirport(Airport airport) {
- airports.remove(airport);
- if (airports.size() <= 1) {
- return;
- }
+ public boolean removeAirport(Airport airport) {
+ boolean success = airports.remove(airport);
+ if (!success) return false;
+ if (airports.size() <= 1) return true;
List newPath = new LinkedList<>(airports);
- airplanes.forEach(airplane -> {
- airplane.setPath(newPath);
- });
+ airplanes.forEach(airplane -> airplane.setPath(newPath));
+ return true;
}
public Collection getPlanes() { return airplanes; }
@@ -93,19 +91,21 @@ public void setOwner(String player) {
owner = owner == null ? player : owner;
}
- public String getOwner() { return owner; }
+ public String getOwner() { return getFirstPlane().getOwner(); }
public List getPath() { return airports; }
+ public int getRouteCount() { return Math.max(0, getPath().size() - (isCyclical() ? 0 : 1)); }
+
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
-
+ if (getRouteCount() == 0) builder.append("[INVALID]");
if (cyclical) builder.append("(c)");
- for (Airport a : airports) {
- builder.append(a.getId()).append(" -> ");
- }
- if (cyclical) builder.append(airports.get(0).getId());
+
+ for (Airport a : airports) builder.append(a.getId()).append(" -> ");
+
+ if (cyclical && !getPath().isEmpty()) builder.append(airports.get(0).getId());
builder.append("#");
for (Airplane p : airplanes) {
@@ -128,26 +128,24 @@ public void read(Json json, JsonValue jsonData) {
json.readFields(this, jsonData);
}
- /**
- * Edits the route such that b will follow a.
- * Unless route is being created, a should be
- * a pre-existing Airport in the route. If it is not, adds a behind
- * b (assuming b is pre-existing). If neither are in the path, adds a, b to end. If both are
- * in the path already, does nothing.
- * Case I - neither a nor b is present - add a, add b
- * Case II: a is present but b is not - add b in front of a
- * Case III: b is present but a is not - add a behind b
- * Case IV: a and b are both present - IF a and b are together the head and tail, make the path cyclical
- * @param a Airport start
- * @param b Airport destination
- * @return The original airport after a or null, if any of the following are true:
- * a is at the tail
- * a and b were both already in the path
- * a was not in the path
- * In other words, only returns a non-null Airport object if and only if a is in the path while b is not AND a is
- * not at the tail. This is the case where b replaced another airport to be the destination after a in the path,
- * so additional processing should be done.
- */
+ /// Edits the route such that **b** will follow **a**.
+ /// Unless route is being created, **a** should be
+ /// a pre-existing **Airport** in the route. If it is not, adds **a** behind
+ /// **b** (assuming b is pre-existing). If neither are in the path, adds a, b to end. If both are
+ /// in the path already, does nothing.
+ /// - Case I - neither a nor b is present - add _a_, add _b_
+ /// - Case II: a is present but b is not - add b in front of a
+ /// - Case III: b is present but a is not - add a behind b
+ /// - Case IV: a and b are both present - IF a and b are together the head and tail, make the path cyclical
+ /// @param a Airport start
+ /// @param b Airport destination
+ /// @return The original airport after a or null, if any of the following are true:
+ /// - a is at the tail
+ /// - a and b were both already in the path
+ /// - _a_ was not in the path
+ /// In other words, only returns a non-null Airport object if and only if a is in the path while b is not AND a is
+ /// not at the tail. This is the case where b replaced another airport to be the destination after _a_ in the path,
+ /// so additional processing MUST BE done.
public Airport editRoute(Airport a, Airport b) {
Airport originalDestination = airports.contains(a) && airports.indexOf(a) < airports.size() - 1 ?
airports.get(airports.indexOf(a) + 1) :
@@ -174,19 +172,22 @@ public Airport editRoute(Airport a, Airport b) {
return originalDestination;
}
+ /// @return True if path is cyclical, which requires that the last airport connects to the first and there
+ /// are more than 2 airports in this path
public boolean isCyclical() { return cyclical && airports.size() > 2; }
+ public boolean endConnectsToStart() { return cyclical; }
/**
* Returns the edge of this path service that is being hovered over by 'hoverPos'.
* @param hoverPos The mouse position of hover
- * @param dst what distance from the edge is valid to indicate hovering?
+ * @param dst how much distance from the edge is valid to indicate hovering?
* @return Array of two airports to indicate edge, or null
*/
public Airport[] getHoverEdge(Vector2 hoverPos, float dst) {
if (airports.isEmpty())
return null;
- Iterator i = iterator();
+ Iterator i = airports.iterator();
Airport start = i.next();
while (i.hasNext()) {
Airport end = i.next();
@@ -236,9 +237,7 @@ public boolean equals(Object obj) {
((PathService) obj).airports.equals(airports) && ((PathService) obj).airplanes.equals(airplanes);
}
- /**
- * THIS HASH CODE USES MUTABLE VALUES
- */
+ /// THIS HASH CODE USES MUTABLE VALUES
@Override
public int hashCode() {
return airports.hashCode() + airplanes.hashCode();
@@ -246,11 +245,11 @@ public int hashCode() {
/**
* Pre-condition: (x,y) is within a 50 pixel radius of an airport on this path
- * @param x The x position
- * @param y The y position
* @return The airport in front of this route. Null if pre-condition not met.
*/
- public Airport calculateNextOnPath(float x, float y) {
+ public Airport calculateNextOnPath(Vector2 position) {
+ float x = position.x;
+ float y = position.y;
Iterator iterator = airports.iterator();
Airport prev = iterator.next();
double closestDist = Double.MAX_VALUE;
@@ -290,7 +289,7 @@ public boolean pointIsOnPath(float x, float y, int pixRadius) {
StreamlinesLogger.logger.warn("There's a path with 0 airports in the game map!");
return false;
}
- Iterator iterator = iterator();
+ Iterator iterator = airports.iterator();
Airport prev = iterator.next();
while (iterator.hasNext()) {
@@ -317,28 +316,21 @@ public boolean pointIsOnPath(float x, float y, int pixRadius) {
}
@Override
- public Iterator iterator() {
- return new Iterator<>() { // iterate through airports list, and then head again
- private int index = 0;
- @Override
- public boolean hasNext() { // account for cyclical by ending at head (pointed to by tail)
- return index < airports.size() || (isCyclical() && index == airports.size() && !airports.isEmpty());
- }
+ public void dispose() {
+ if (getPlanes().isEmpty() && getPath().isEmpty()) {
+ owner = null;
+ globalId = null;
+ cyclical = false;
+ return;
+ }
- @Override
- public Airport next() {
- if (!hasNext()) throw new NoSuchElementException();
- return airports.get(index++ % airports.size());
- }
- };
+ throw new IllegalStateException("All planes and paths must be returned to inventory before resetting path.");
}
- @Override
- public void dispose() {
- getPath().forEach(item -> item.removeTerminal(getOwner()));
- getPlanes().clear();
- getPath().clear();
+ public boolean removePlane(Airplane airplane) {
+ return airplanes.remove(airplane);
}
+
}
/**
@@ -347,56 +339,62 @@ public void dispose() {
class CircularLinkedList extends LinkedList {
CircularLinkedList(List airports) { super(airports); }
- @Override
- public ListIterator listIterator(int index) {
- return new ListIterator<>() {
- private int curr = index % size();
+ class CircularLinkedListListIterator implements ListIterator {
+ private int curr;
- @Override
- public boolean hasNext() {
- return true;
- }
+ CircularLinkedListListIterator(int index) {
+ curr = index % size();
+ }
- @Override
- public Airport next() {
- return get(curr = nextIndex());
- }
+ @Override
+ public boolean hasNext() {
+ return true;
+ }
- @Override
- public boolean hasPrevious() {
- return true;
- }
+ @Override
+ public Airport next() {
+ return get(curr = nextIndex());
+ }
- @Override
- public Airport previous() {
- return get(curr = previousIndex());
- }
+ @Override
+ public boolean hasPrevious() {
+ return true;
+ }
- @Override
- public int nextIndex() {
- return (curr + 1) >= size() ? 0 : curr + 1;
- }
+ @Override
+ public Airport previous() {
+ return get(curr = previousIndex());
+ }
- @Override
- public int previousIndex() {
- return (curr - 1) < 0 ? (size() - 1) : curr - 1;
- }
+ @Override
+ public int nextIndex() {
+ return (curr + 1) >= size() ? 0 : curr + 1;
+ }
- @Override
- public void remove() {
- throw new UnsupportedOperationException();
- }
+ @Override
+ public int previousIndex() {
+ return (curr - 1) < 0 ? (size() - 1) : curr - 1;
+ }
- @Override
- public void set(Airport airport) {
- throw new UnsupportedOperationException();
- }
+ @Override
+ public void remove() {
+ CircularLinkedList.this.remove(get(curr));
+ }
- @Override
- public void add(Airport airport) {
- throw new UnsupportedOperationException();
- }
- };
+ @Override
+ public void set(Airport airport) {
+ CircularLinkedList.this.set(curr, airport);
+ }
+
+ @Override
+ public void add(Airport airport) {
+ CircularLinkedList.this.add(curr, airport);
+ }
+ }
+
+ @Override
+ public ListIterator listIterator(int index) {
+ return new CircularLinkedListListIterator(index);
}
}
diff --git a/shared/src/main/java/io/streamlines/inventory/EmptyCommand.java b/shared/src/main/java/io/streamlines/inventory/EmptyCommand.java
index fdc9bf8d..f7feb8f1 100644
--- a/shared/src/main/java/io/streamlines/inventory/EmptyCommand.java
+++ b/shared/src/main/java/io/streamlines/inventory/EmptyCommand.java
@@ -1,5 +1,6 @@
package io.streamlines.inventory;
+import io.streamlines.map.AirportGraphable;
import io.streamlines.map.GameMap;
import io.streamlines.map.Player;
@@ -26,7 +27,7 @@ boolean use(Player player, GameMap game) {
}
@Override
- boolean lose(Player victim, Player thief, GameMap game) {
+ boolean lose(Player victim, Player thief, AirportGraphable game) {
return true;
}
}
diff --git a/shared/src/main/java/io/streamlines/inventory/ExpandTerminalCommand.java b/shared/src/main/java/io/streamlines/inventory/ExpandTerminalCommand.java
index 2cc69454..0bd488a7 100644
--- a/shared/src/main/java/io/streamlines/inventory/ExpandTerminalCommand.java
+++ b/shared/src/main/java/io/streamlines/inventory/ExpandTerminalCommand.java
@@ -5,6 +5,10 @@
import io.streamlines.map.GameMap;
import io.streamlines.map.Player;
+/**
+ * Expands the capacity of the terminal.
+ * @see Terminal#expand()
+ */
public class ExpandTerminalCommand extends Powerup implements PermissionedAction {
private final Terminal terminal;
diff --git a/shared/src/main/java/io/streamlines/inventory/InfrastructureItem.java b/shared/src/main/java/io/streamlines/inventory/InfrastructureItem.java
index 6c8cae40..1f94ae94 100644
--- a/shared/src/main/java/io/streamlines/inventory/InfrastructureItem.java
+++ b/shared/src/main/java/io/streamlines/inventory/InfrastructureItem.java
@@ -1,5 +1,6 @@
package io.streamlines.inventory;
+import io.streamlines.map.AirportGraphable;
import io.streamlines.map.GameMap;
import io.streamlines.map.Player;
@@ -10,7 +11,7 @@ public abstract class InfrastructureItem extends InventoryItem {
* This would serve the purpose of "deleting" the item from the map by the victim.
* @param victim The victim who is losing the item
* @param thief The victim who is gaining the item
- * @param game The map of the game, for modification
+ * @param graph The graph of the game, for modification
*/
- abstract boolean lose(Player victim, Player thief, GameMap game);
+ abstract boolean lose(Player victim, Player thief, AirportGraphable graph);
}
diff --git a/shared/src/main/java/io/streamlines/inventory/Inventory.java b/shared/src/main/java/io/streamlines/inventory/Inventory.java
index 6f13fa21..e3a029d8 100644
--- a/shared/src/main/java/io/streamlines/inventory/Inventory.java
+++ b/shared/src/main/java/io/streamlines/inventory/Inventory.java
@@ -2,6 +2,8 @@
import com.badlogic.gdx.utils.JsonValue;
import io.streamlines.StreamlinesLogger;
+import io.streamlines.flight.Airplane;
+import io.streamlines.flight.PathService;
import io.streamlines.map.GameMap;
import io.streamlines.map.Player;
@@ -15,7 +17,7 @@
* @author Varun Singh, (C) 2024 Streamlines
*/
public class Inventory {
- private final String playerId;
+ private String playerId;
private transient GameMap gameMap;
/**
@@ -29,14 +31,14 @@ public class Inventory {
/**
* Instantiates an inventory manager for the player
* @param playerId Id of the player who owns the inventory
- * @param gameMap The map of the game
+ * @param map The map of the game
*/
- public Inventory(String playerId, GameMap gameMap) {
+ public Inventory(String playerId, GameMap map) {
itemBag = new EnumMap<>(InventoryItemType.class);
addItem(InventoryItemType.Place_Airplane, INITIAL_AIRPLANE_COUNT);
addItem(InventoryItemType.Place_Route, INITIAL_ROUTE_COUNT);
addItem(InventoryItemType.Expand_Airplane, 1);
- this.gameMap = gameMap;
+ this.gameMap = map;
this.playerId = playerId;
}
@@ -47,8 +49,8 @@ public Inventory() {
this("", new GameMap());
}
- public static Inventory parse(JsonValue representation) {
- Inventory inventory = new Inventory(representation.getString("playerId"), new GameMap());
+ public static Inventory parse(JsonValue representation, GameMap map) {
+ Inventory inventory = new Inventory(representation.getString("playerId"), map);
for (JsonValue itemType : representation.get("itemBag")) {
if (itemType.name.equals("class")) continue;
inventory.itemBag.put(InventoryItemType.valueOf(itemType.name), itemType.asInt());
@@ -56,15 +58,6 @@ public static Inventory parse(JsonValue representation) {
return inventory;
}
- /**
- * Set transient properties player id and map after the fact only for JSON deserialization
- * @param map The mutable game state (map)
- */
- public Inventory with(GameMap map) {
- gameMap = map;
- return this;
- }
-
private Player getPlayer() { return gameMap.getPlayer(playerId); }
/**
@@ -89,8 +82,8 @@ public boolean useItem(Powerup item) {
/**
* Must exist because Powerup and InventoryItem overload each other polymorphically
- * @param item
- * @return
+ * @param item The inventory command
+ * @return whether the operation was successful
*/
private boolean useInventoryItem(InventoryItem item) {
int itemCount = getItemCount(InventoryItemType.of(item));
@@ -105,38 +98,71 @@ private boolean useInventoryItem(InventoryItem item) {
}
/**
- * Removes an item from the player's infrastructure and the game map, and adds that item back to inventory.
- * @param item The command that represents an item getting returned to inventory
+ * Removes infrastructure from the player's map, and adds that item back to inventory.
+ * @param item The command that represents the infrastructure
+ * @return whether the operation was successful
*/
public boolean returnItem(InfrastructureItem item) {
- boolean success = item.lose(getPlayer(), getPlayer(), gameMap);
+ boolean success = item.lose(getPlayer(), getPlayer(), gameMap.getGraph());
if (success) addItem(InventoryItemType.of(item));
return success;
}
- /**
- * Adds item to inventory. Use the .class property of a concrete implementor {@link InventoryItem}.
- *
- * Post-condition: {@link #useItem(InventoryItem)} may be called on this item one additional time
- * @param ability The item getting added to inventory
- */
+ /// Returns a route to inventory. IF that is the last route on the path, cleanups and disposes the entire path,
+ /// returning all infrastructure (planes) to inventory.
+ ///
+ /// Pre-condition: The player owns the path
+ ///
+ /// Post-condition: The path is no longer in the player's map but its infrastructure is in inventory, if
+ /// operation is successful
+ /// @param item The place route command instance
+ /// @return whether the operation was successful
+ public boolean returnItem(PlaceRouteCommand item) {
+ boolean success = item.lose(getPlayer(), getPlayer(), gameMap.getGraph());
+ if (!success) {
+ StreamlinesLogger.logger.warn("Route not returned successfully");
+ return false;
+ }
+ addItem(InventoryItemType.Place_Route);
+ PathService path = item.output();
+ if (path.getPath().size() < 2) {
+ PlaceRouteCommand removeCommand = new PlaceRouteCommand(0, 0, path);
+ success = removeCommand.lose(getPlayer(), getPlayer(), gameMap.getGraph());
+ if (!success) {
+ StreamlinesLogger.logger.warn("Last route not returned successfully on path cleanup");
+ return false;
+ }
+ if (path.endConnectsToStart()) {
+ addItem(InventoryItemType.Place_Route);
+ }
+ for (Airplane airplane : new ArrayList<>(item.output().getPlanes())) {
+ returnItem(new PlaceAirplaneCommand(airplane, item.output()));
+ }
+ path.dispose();
+ getPlayer().getPaths().remove(path);
+ }
+ return success;
+ }
+
+ /// Adds item to inventory. property of a concrete implementor [InventoryItem].
+ ///
+ /// Post-condition: [#useItem(InventoryItem)] may be called on this item one additional time
+ /// @param ability The item getting added to inventory
public final void addItem(InventoryItemType ability) {
addItem(ability, 1);
}
- /**
- * Adds item to inventory. Use the .class property of a concrete implementor of {@link InventoryItem}
- * Post-condition: {@link #useItem(InventoryItem)} may be called on this item count additional times
- * @param ability The item getting added to inventory
- * @param count The number of additional times {@link #useItem(InventoryItem)} may be called
- */
+ /// Adds item to inventory.
+ /// Post-condition: [#useItem(InventoryItem)] may be called on this item **count** additional times
+ /// @param ability The item getting added to inventory
+ /// @param count The number of additional times [#useItem(InventoryItem)] may be called
void addItem(InventoryItemType ability, int count) {
itemBag.put(ability, getItemCount(ability) + count);
}
/**
* Gets the count of the number of a specific item type in inventory that have not been used.
- * @param item The TYPE of item, given by the class of the command
+ * @param item The TYPE of item, given by the type of the command
* @return The count
*/
public int getItemCount(InventoryItemType item) {
@@ -166,7 +192,8 @@ public String toString() {
@Override
public boolean equals(Object obj) {
- if (!(obj instanceof Inventory other)) return false;
+ if (!(obj instanceof Inventory)) return false;
+ Inventory other = (Inventory) obj;
for (InventoryItemType itemType : InventoryItemType.values()) {
if (getItemCount(itemType) != other.getItemCount(itemType)) return false;
}
diff --git a/shared/src/main/java/io/streamlines/inventory/PlaceAirplaneCommand.java b/shared/src/main/java/io/streamlines/inventory/PlaceAirplaneCommand.java
index e7201d42..5fa9be50 100644
--- a/shared/src/main/java/io/streamlines/inventory/PlaceAirplaneCommand.java
+++ b/shared/src/main/java/io/streamlines/inventory/PlaceAirplaneCommand.java
@@ -3,6 +3,7 @@
import io.streamlines.PermissionedAction;
import io.streamlines.flight.Airplane;
import io.streamlines.flight.PathService;
+import io.streamlines.map.AirportGraphable;
import io.streamlines.map.GameMap;
import io.streamlines.map.Player;
@@ -27,8 +28,8 @@ boolean use(Player player, GameMap game) {
}
@Override
- boolean lose(Player victim, Player thief, GameMap gameMap) {
- return currentPath.getPlanes().remove(airplane);
+ boolean lose(Player victim, Player thief, AirportGraphable gameMap) {
+ return currentPath.removePlane(airplane);
}
@Override
diff --git a/shared/src/main/java/io/streamlines/inventory/PlaceRouteCommand.java b/shared/src/main/java/io/streamlines/inventory/PlaceRouteCommand.java
index 86d88ee9..72eda7a5 100644
--- a/shared/src/main/java/io/streamlines/inventory/PlaceRouteCommand.java
+++ b/shared/src/main/java/io/streamlines/inventory/PlaceRouteCommand.java
@@ -2,6 +2,7 @@
import io.streamlines.StreamlinesLogger;
import io.streamlines.flight.*;
+import io.streamlines.map.AirportGraphable;
import io.streamlines.map.GameMap;
import io.streamlines.map.Player;
@@ -38,16 +39,19 @@ public PlaceRouteCommand(Airport start, Airport end, PathService path) {
encapsulatingPath = path;
}
- /**
- * Constructs {@link PlaceRouteCommand} for existing path.
- * @apiNote Should be used when invoking {@link #lose(Player, Player, GameMap)} to enforce that the airports are
- * on the path
- * @param startIndex The index of the start airport
- * @param endIndex The index of the end airport
- * @param path The path
- */
+ /// Constructs [PlaceRouteCommand] for existing path.
+ /// @apiNote Should be used when invoking [#lose(Player,Player,AirportGraphable)] to enforce that the airports are
+ /// on the path
+ /// @param startIndex The index of the start airport
+ /// @param endIndex The index of the end airport
+ /// @param path The path
public PlaceRouteCommand(int startIndex, int endIndex, PathService path) {
- this(path.getPath().get(startIndex), path.getPath().get(endIndex), path);
+ this(
+ path.getPath().get(startIndex),
+ endIndex > path.getPath().size() - 1 && path.isCyclical() ? path.getPath().get(0) : path.getPath().get(endIndex),
+ path
+ );
+
}
@Override
@@ -71,24 +75,32 @@ boolean use(Player player, GameMap map) {
map.addRoute(startAirport, endAirport);
if (originalDestination != null) {
- map.removeOneFlight(startAirport, originalDestination);
+ map.getGraph().removeEdgeInstance(startAirport, originalDestination);
}
- encapsulatingPath.forEach(Airport::retryIsolatedPassengers);
+ encapsulatingPath.getPath().forEach(Airport::retryIsolatedPassengers);
return true;
}
+ /// Removes airport and the routes (two edges) going to and from this airport on the path.
+ ///
+ /// Post-condition: The airport is no longer on the path. If the path becomes size 1 or 0, it is removed entirely.
+ /// but the other airport's terminals stay.
+ /// @throws IllegalArgumentException If the player does not have the path
@Override
- boolean lose(Player victim, Player thief, GameMap game) {
- removeAirportFromPath(game, endAirport);
- return true;
+ boolean lose(Player victim, Player thief, AirportGraphable game) {
+ if (!victim.getPaths().contains(encapsulatingPath)) {
+ throw new IllegalArgumentException("Cannot remove path from a different player without top dog");
+ }
+ return removeAirportFromPath(game, endAirport);
}
- private void removeAirportFromPath(GameMap game, Airport airport) {
- encapsulatingPath.removeAirport(airport);
+ private boolean removeAirportFromPath(AirportGraphable game, Airport airport) {
+ boolean success = encapsulatingPath.removeAirport(airport);
+ if (!success) return false;
Airport beforeA = null;
Airport afterA = null;
Airport prev = null;
- for (Airport inPath : encapsulatingPath) {
+ for (Airport inPath : encapsulatingPath.getPath()) {
if (inPath == airport) {
beforeA = prev;
}
@@ -98,13 +110,9 @@ else if (beforeA != null && prev == airport) {
}
prev = inPath;
}
- if (beforeA != null) game.removeOneFlight(beforeA, airport);
- if (afterA != null) game.removeOneFlight(airport, afterA);
-
- // Check if the entire path is now gone
- if (encapsulatingPath.getPath().size() == 1) {
- removeAirportFromPath(game, encapsulatingPath.getPath().get(0));
- }
+ if (beforeA != null) game.removeEdgeInstance(beforeA, airport);
+ if (afterA != null) game.removeEdgeInstance(airport, afterA);
+ return true;
}
public PathService output() {
diff --git a/shared/src/main/java/io/streamlines/inventory/TopDogCommand.java b/shared/src/main/java/io/streamlines/inventory/TopDogCommand.java
index 653a4607..793e582e 100644
--- a/shared/src/main/java/io/streamlines/inventory/TopDogCommand.java
+++ b/shared/src/main/java/io/streamlines/inventory/TopDogCommand.java
@@ -24,7 +24,7 @@ boolean use(Player topDog, GameMap game) {
return false;
}
// If it was rejected, let the player try again
- if (!infrastructureItem.lose(game.getPlayer(victimId), topDog, game)) return false;
+ if (!infrastructureItem.lose(game.getPlayer(victimId), topDog, game.getGraph())) return false;
topDog.getInventory().addItem(InventoryItemType.of(infrastructureItem));
// Top dog ability maxes out at one at a time. (You can get top dog more than once, but once you use it,
// it always resets back to empty)
diff --git a/shared/src/main/java/io/streamlines/map/AirportGraphable.java b/shared/src/main/java/io/streamlines/map/AirportGraphable.java
new file mode 100644
index 00000000..52b1c10e
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/map/AirportGraphable.java
@@ -0,0 +1,14 @@
+package io.streamlines.map;
+
+import io.streamlines.flight.Airport;
+
+import java.util.Collection;
+
+public interface AirportGraphable {
+ /**
+ * Decreases the 'popularity' of an edge
+ * @param from The from vertex
+ * @param to The to vertex
+ */
+ default void removeEdgeInstance(Airport from, Airport to) {}
+}
diff --git a/shared/src/main/java/io/streamlines/map/BiMap.java b/shared/src/main/java/io/streamlines/map/BiMap.java
index 7269fa5c..1ad67c85 100644
--- a/shared/src/main/java/io/streamlines/map/BiMap.java
+++ b/shared/src/main/java/io/streamlines/map/BiMap.java
@@ -29,7 +29,7 @@ public synchronized int put(T obj) {
return id;
}
- public boolean remove(T obj) {
+ public synchronized boolean remove(T obj) {
return (idMapper.remove(objIntegerMap.get(obj).intValue()) != null) && (objIntegerMap.remove(obj) >= 0);
}
diff --git a/shared/src/main/java/io/streamlines/map/GameMap.java b/shared/src/main/java/io/streamlines/map/GameMap.java
index 97ae28cb..76d59c55 100644
--- a/shared/src/main/java/io/streamlines/map/GameMap.java
+++ b/shared/src/main/java/io/streamlines/map/GameMap.java
@@ -51,12 +51,6 @@ public GameMap() {
updatePlayersIterable();
}
- /**
- * @return the dimensions of the bg map
- */
- public static int[] getMapDimensions() { return new int[]{GameMapAccessible.MAP_WIDTH, GameMapAccessible.MAP_HEIGHT}; }
-
-
@Override
public Collection getAirports() {
return airports;
@@ -67,28 +61,17 @@ public Airport getAirportById(String code) {
return getAirports().stream().filter(item -> item.getId().equals(code)).findFirst().orElseThrow();
}
- @Override
- public PathService getPathServiceById(String id, String owner) {
- return getPlayer(owner).getPathById(id).get();
- }
-
/**
* Expensive algorithm for finding airplane within path within player for all paths in all players.
* Orders of players, paths, and planes are non-deterministic.
* @param id The uniquely generated identifier for an airplane
* @return The airplane with that id or null if none we found
*/
- public Airplane getAirplaneById(String id) {
- for (Player player : getActivePlayers()) {
- for (PathService path : player.getPaths()) {
- for (Airplane airplane : path.getPlanes()) {
- if (airplane.getId().equals(id)) {
- return airplane;
- }
- }
- }
- }
- return null;
+ public Optional getAirplaneById(String id) {
+ return getActivePlayers().stream()
+ .map(player -> player.getAirplaneById(id))
+ .filter(Optional::isPresent).map(Optional::get)
+ .findFirst();
}
private final BoardConfidenceObserver finishPendingObserver = (String owner, boolean keep) -> {
@@ -117,32 +100,25 @@ public void removeOneFlight(Airport a, Airport b) {}
*/
public void addRoute(Airport a, Airport b) {}
- /**
- * Removes airport and the routes (two edges) going to and from this airport on the path.
- * Post-condition: The airport is no longer on the path. If the path becomes size 1 or 0, it is removed entirely
- * but the other airport's terminals stay.
- * @param path The path (Precondition: belongs to player)
- * @param airport The airport to remove from the path.
- * @param player The player (Precondition: Must be part of the game)
- * @return True if the path should be removed
- */
- public boolean removeAirportFromPath(PathService path, Airport airport, Player player) {
- player.getInventory().returnItem(new PlaceRouteCommand(null, airport, path));
- return path.getPath().size() < 2;
- }
-
@Override
public int getDay() {
return currentDay;
}
+ private final AirportGraphable graph = new AirportGraphable() {};
+
+ public AirportGraphable getGraph() {
+ return graph;
+ }
+
public int nextDay() {
return ++currentDay;
}
/**
* Setter for day number. Locks in terminals and checks if path services are valid.
- * @param newDayNumber The new or initial day number
+ * @param newDayNumber The new or initial day number. Keeping this as an explicit parameter is important for the client
+ * both if he disconnects and when someone joins late.
*/
public void startNewDay(int newDayNumber) {
currentDay = newDayNumber;
@@ -156,6 +132,10 @@ public void startNewDay(int newDayNumber) {
playerIterable.forEach(Player::validatePaths);
}
+ public void startNewDay() {
+ startNewDay(nextDay());
+ }
+
/**
* Creates or updates a player. Resets iterator if new player.
*
@@ -193,7 +173,7 @@ public Player newPlayer(String id, String name) {
}
public Player recreatePlayer(String id, String name, Color color) {
- Player player = new Player(id, this, new ArrayList<>(), color);
+ Player player = new Player(this, id, new ArrayList<>(), color);
player.setName(name);
setPlayer(player);
return player;
@@ -244,19 +224,19 @@ public void losePlayer(String id) {
if (!players.containsKey(id)) return;
players.get(id).dispose();
updatePlayersIterable();
-
}
/**
* Remove player from the game and the final leaderboard (not spectating).
- * Post-condition: player is disposed
- * @param id Id of the player
+ * Post-condition: player is disposed. If that is the second-to-last player, the game ends.
*/
- public void removePlayer(String id) {
- if (!players.containsKey(id)) return;
- players.get(id).dispose();
- players.remove(id);
+ public void removePlayerAndPossiblyEndGame(Player player) {
+ player.dispose();
+ players.remove(player.getId());
updatePlayersIterable();
+ if (shouldEnd()) {
+ end();
+ }
}
@Override
diff --git a/shared/src/main/java/io/streamlines/map/GameMapAccessible.java b/shared/src/main/java/io/streamlines/map/GameMapAccessible.java
index 44a8330f..299fe469 100644
--- a/shared/src/main/java/io/streamlines/map/GameMapAccessible.java
+++ b/shared/src/main/java/io/streamlines/map/GameMapAccessible.java
@@ -28,7 +28,6 @@ public interface GameMapAccessible {
* @return Airport with code
*/
Airport getAirportById(String code);
- PathService getPathServiceById(String id, String owner);
List getActivePlayers();
Collection getAllPlayers();
default boolean containsActivePlayer(String id) {
@@ -41,5 +40,4 @@ default boolean containsActivePlayer(String id) {
* @return The current day
*/
int getDay();
-
}
diff --git a/shared/src/main/java/io/streamlines/map/GraphAlgorithmObserver.java b/shared/src/main/java/io/streamlines/map/GraphAlgorithmObserver.java
index 569b4ff4..983491a3 100644
--- a/shared/src/main/java/io/streamlines/map/GraphAlgorithmObserver.java
+++ b/shared/src/main/java/io/streamlines/map/GraphAlgorithmObserver.java
@@ -43,27 +43,25 @@ public interface GraphAlgorithmObserver {
*/
void notifyDijkstraHasBegun();
- /** Called by the graph to notify this observer that
- * a vertex has been added to the "Finished Set"
- * during Dijkstra's algorithm. The second parameter
- * is the "cost" (total weight) of the best path
- * leading from the starting vertex to the one referenced
- * by the first parameter.
- *
- * @param vertexAddedToFinishedSet Vertex added to finished set
- * @param costOfPath Cost of the path
- */
+ /// Called by the graph to notify this observer that
+ /// a vertex has been added to the "Finished Set"
+ /// during Dijkstra's algorithm. The second parameter
+ /// is the "cost" (total weight) of the best path
+ /// leading from the starting vertex to the one referenced
+ /// by the first parameter.
+ ///
+ /// @param vertexAddedToFinishedSet Vertex added to finished set
+ /// @param costOfPath Cost of the path
void notifyDijkstraVertexFinished(V vertexAddedToFinishedSet, Float costOfPath);
- /**
- * Called by the graph to notify this observer that
- * Dijkstra's algorithm is over.
- *
- * @param path A list of Vertices that are connected along edges,
- * beginning with the "starting vertex" and ending with the
- * "finishing vertex". This will be the optimal (lowest cost)
- * path from start to finish.
- */
+ ///
+ /// Called by the graph to notify this observer that
+ /// Dijkstra's algorithm is over.
+ ///
+ /// @param path A list of Vertices that are connected along edges,
+ /// beginning with the "starting vertex" and ending with the
+ /// "finishing vertex". This will be the optimal (lowest cost)
+ /// path from start to finish.
void notifyDijkstraIsOver(Deque path);
}
diff --git a/shared/src/main/java/io/streamlines/map/Player.java b/shared/src/main/java/io/streamlines/map/Player.java
index 6f2348e9..6baa87d0 100644
--- a/shared/src/main/java/io/streamlines/map/Player.java
+++ b/shared/src/main/java/io/streamlines/map/Player.java
@@ -61,12 +61,12 @@ public Player(GameMap game, String id) {
/**
* Recreates a player object from JSON deserialization
- * @param id Id of the player
* @param game game state
+ * @param id Id of the player
* @param paths The player's current list of paths
* @param userColor The player's current color (final)
*/
- Player(String id, GameMap game, List paths, Color userColor) {
+ Player(GameMap game, String id, List paths, Color userColor) {
this.userColor = userColor;
this.id = id;
this.game = game;
@@ -97,47 +97,51 @@ public String getName() {
return name;
}
+ /// @param path The path
public void removePath(PathService path) {
- new ArrayList<>(path.getPlanes()).forEach(item -> inventory.returnItem(new PlaceAirplaneCommand(item, path)));
- paths.remove(path);
+ if (!getPaths().contains(path)) {
+ throw new IllegalArgumentException("Path " + path + " does not exist for player " + name);
+ }
+
+ // Inventory automatically cleans up the path once we're down to a single airport
+ while (path.getRouteCount() >= 1) {
+ inventory.returnItem(new PlaceRouteCommand(0, 1, path));
+ }
}
- /**
- * Gets the paths as a collection. Should be used only for iterating and checking for existence of a path. For
- * random access, use {@link #getPathById(int)}
- * @return The paths
- */
+ /// Removes airport and the routes (two edges) going to and from this airport on the path.
+ /// Post-condition: The airport is no longer on the path. If the path becomes size 1 or 0, it is removed entirely.
+ /// but the other airport's terminals stay.
+ ///
+ /// @param path The path (Precondition: belongs to player)
+ /// @param airport The airport to remove from the path
+ public void removeAirportFromPathAndPossiblyRemovePath(PathService path, Airport airport) {
+ getInventory().returnItem(new PlaceRouteCommand(null, airport, path));
+ }
+
+ /// Gets the paths as a collection. Should be used only for iterating and checking for existence of a path. For
+ /// random access, use [#getPathById(String)]
+ /// @return The paths
public Collection getPaths() {
return paths;
}
public Inventory getInventory() { return inventory; }
+ /// Removes invalid airports from the path
void validatePaths() {
- Iterator it = getPaths().iterator();
- while (it.hasNext()) {
- PathService path = it.next();
- if (path.getPath().get(0).findTerminalByOwner(getId()).isEmpty()) {
- PlaceRouteCommand removeCommand = new PlaceRouteCommand(1, 0, path);
- if (!getInventory().returnItem(removeCommand)) {
- StreamlinesLogger.logger.warn("Something's gone wrong, a route could not be removed from " + path);
- }
- }
+ for (int j = paths.size() - 1; j >= 0; j--) {
+ PathService path = paths.get(j);
// Remove invalid airports
- for (int i = path.getPath().size() - 1; i > 0; i--) {
+ for (int i = path.getPath().size() - 1; i >= 0 && !path.getPath().isEmpty(); i--) {
if (path.getPath().get(i).findTerminalByOwner(getId()).isEmpty()) {
- PlaceRouteCommand removeCommand = new PlaceRouteCommand(i - 1, i, path);
+ // We don't need a separate condition for i = 0 because |0-1|=1
+ PlaceRouteCommand removeCommand = new PlaceRouteCommand(Math.abs(i - 1), i, path);
if (!getInventory().returnItem(removeCommand)) {
StreamlinesLogger.logger.warn("Something's gone wrong, a route could not be removed from " + path);
}
}
}
-
- // Remove planes on paths of 1 or fewer airports (keep these terminals)
- if (path.getPath().size() < 2) {
- it.remove();
- removePath(path);
- }
}
}
@@ -160,12 +164,12 @@ public boolean editPath(PathService r, Airport a, Airport b) {
* @param b The second airport
*/
public void recreatePath(Airport a, Airport b, String airplaneId, String pathId) {
- var newPath = new PlaceRouteCommand(a, b, new PathService(pathId));
+ PlaceRouteCommand newPath = new PlaceRouteCommand(a, b, new PathService(pathId));
boolean used = inventory.useItem(newPath);
// Don't attempt to place an airplane on a path that was not created nor when no airplane was on the original
// path
if (!used || airplaneId.isEmpty()) return;
- var newPlane = new PlaceAirplaneCommand(new Airplane(a, b, id, airplaneId), newPath.output());
+ InfrastructureItem newPlane = new PlaceAirplaneCommand(new Airplane(a, b, id, airplaneId), newPath.output());
if (!inventory.useItem(newPlane)) {
StreamlinesLogger.logger.warn("Route creation successful but airplane creation not");
}
@@ -180,7 +184,7 @@ public void recreatePath(Airport a, Airport b, String airplaneId, String pathId)
* @return The newly created path
*/
public PathService createPath(Airport a, Airport b) {
- var newPath = new PlaceRouteCommand(a, b);
+ PlaceRouteCommand newPath = new PlaceRouteCommand(a, b);
boolean used = inventory.useItem(newPath);
// Don't attempt to place an airplane on a path that was not created
@@ -191,7 +195,7 @@ public PathService createPath(Airport a, Airport b) {
// when the passenger is finding a path
if (inventory.getItemCount(InventoryItemType.Place_Airplane) < 1) return newPath.output();
- var newPlane = new PlaceAirplaneCommand(new Airplane(a, b, id), newPath.output());
+ PlaceAirplaneCommand newPlane = new PlaceAirplaneCommand(new Airplane(a, b, id), newPath.output());
if (!inventory.useItem(newPlane)) {
StreamlinesLogger.logger.warn("Route creation successful but airplane creation not");
}
@@ -199,6 +203,17 @@ public PathService createPath(Airport a, Airport b) {
return newPath.output();
}
+ public Optional getAirplaneById(String id) {
+ for (PathService path : getPaths()) {
+ for (Airplane airplane : path.getPlanes()) {
+ if (airplane.getId().equals(id)) {
+ return Optional.of(airplane);
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
/**
* Null-safe, efficient accessor of the path by the path's player-transcendent id in the player's list of paths
* @param id The index of the path in the player's list. Represents the id of the path.
@@ -209,10 +224,6 @@ public Optional getPathById(String id) {
return paths.stream().filter(item -> item.getId().equals(id)).findFirst();
}
- public Optional getPathById(int id) {
- return getPathById("" + id);
- }
-
public Set getPendingTerminals() {
return game.getAirports().stream()
.map(item -> item.findTerminalByOwner(getId()))
@@ -268,8 +279,8 @@ public boolean isActive() {
@Override
public boolean equals(Object obj) {
- if (!(obj instanceof Player other)) return false;
-
+ if (!(obj instanceof Player)) return false;
+ Player other = (Player) obj;
return Objects.equals(other.getId(), getId()) && userColor.equals(other.userColor) && paths.equals(other.paths);
}
@@ -306,7 +317,7 @@ public Player parse(String representation) {
Json json = new Json();
JsonValue obj = new JsonReader().parse(representation);
List paths = new ArrayList<>();
- Player player = new Player(obj.getString("id"), game, paths, json.readValue(Color.class,
+ Player player = new Player(game, obj.getString("id"), paths, json.readValue(Color.class,
obj.get("userColor")));
player.name = obj.getString("name", obj.getString("id"));
player.complaints = obj.getInt("complaints", 0);
@@ -351,8 +362,7 @@ public Player parse(String representation) {
}
}
- Inventory inventoryRep = Inventory.parse(obj.get("inventory"));
- player.inventory = inventoryRep.with(game);
+ player.inventory = Inventory.parse(obj.get("inventory"), game);
return player;
}
@@ -382,8 +392,7 @@ public void addScore(int occupancy) {
public void dispose() {
StreamlinesLogger.logger.info("DISPOSAL", "Disposing of player " + getId());
validatePaths();
- paths.forEach(PathService::dispose);
- paths.clear();
+ new ArrayList<>(paths).forEach(this::removePath);
// Remove bids (terminals not on paths)
game.getAirports().forEach(item -> item.removeTerminal(id));
diff --git a/shared/src/main/java/io/streamlines/network/AirplaneSystemAction.java b/shared/src/main/java/io/streamlines/network/AirplaneSystemAction.java
new file mode 100644
index 00000000..ae856772
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/AirplaneSystemAction.java
@@ -0,0 +1,5 @@
+package io.streamlines.network;
+
+public enum AirplaneSystemAction {
+ TAKEOFF
+}
diff --git a/shared/src/main/java/io/streamlines/network/AirplaneSystemActionDto.java b/shared/src/main/java/io/streamlines/network/AirplaneSystemActionDto.java
new file mode 100644
index 00000000..bc292824
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/AirplaneSystemActionDto.java
@@ -0,0 +1,65 @@
+package io.streamlines.network;
+
+import io.streamlines.flight.Airplane;
+import io.streamlines.map.GameMap;
+
+import java.io.Serial;
+import java.util.Objects;
+
+public final class AirplaneSystemActionDto
+ implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String airplaneId;
+ private AirplaneSystemAction action;
+
+ public AirplaneSystemActionDto() {}
+
+ public AirplaneSystemActionDto(String airplaneId, AirplaneSystemAction action) {
+ this.airplaneId = airplaneId;
+ this.action = action;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ if (action == AirplaneSystemAction.TAKEOFF) {
+ gameMap.getAirplaneById(airplaneId).ifPresent(Airplane::takeoff);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "AirplaneSystemActionDto[" +
+ "airplaneId=" + airplaneId + ", " +
+ "action=" + action + ']';
+ }
+
+ public String airplaneId() {
+ return airplaneId;
+ }
+
+ public AirplaneSystemAction action() {
+ return action;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ AirplaneSystemActionDto that = (AirplaneSystemActionDto) obj;
+ return Objects.equals(this.airplaneId, that.airplaneId) &&
+ Objects.equals(this.action, that.action);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(airplaneId, action);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/AirplaneUserInventoryActionDto.java b/shared/src/main/java/io/streamlines/network/AirplaneUserInventoryActionDto.java
new file mode 100644
index 00000000..b2baf53b
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/AirplaneUserInventoryActionDto.java
@@ -0,0 +1,127 @@
+package io.streamlines.network;
+
+import com.badlogic.gdx.math.Vector2;
+import io.streamlines.StreamlinesLogger;
+import io.streamlines.flight.*;
+import io.streamlines.inventory.*;
+import io.streamlines.map.GameMap;
+import io.streamlines.map.Player;
+
+import java.io.Serial;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Uses airplane-related (infrastructure and powerup) items from inventory
+ */
+public final class AirplaneUserInventoryActionDto
+ implements DtoActionable {
+ private static String AIRPLANE_NOT_FOUND_MESSAGE = "Airplane not found";
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String playerId;
+ private String airplaneId;
+ private Vector2 position;
+ private String pathId;
+ private InventoryItemType action;
+
+ public AirplaneUserInventoryActionDto() {}
+
+ public AirplaneUserInventoryActionDto(String playerId, String airplaneId, Vector2 position, String pathId, InventoryItemType action) {
+ this.playerId = playerId;
+ this.airplaneId = airplaneId;
+ this.position = position;
+ this.pathId = pathId;
+ this.action = action;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Player player = gameMap.getPlayer(playerId);
+
+ if (action == InventoryItemType.Expand_Airplane) {
+ Optional airplane = gameMap.getAirplaneById(airplaneId);
+ if (airplane.isEmpty()) {
+ StreamlinesLogger.logger.error("DTO", AIRPLANE_NOT_FOUND_MESSAGE);
+ return false;
+ }
+ player.getInventory().useItem(new ExpandAirplaneCommand(airplane.get()));
+ } else if (action == InventoryItemType.Resilience) {
+ Optional airplane = gameMap.getAirplaneById(airplaneId);
+ if (airplane.isEmpty()) {
+ StreamlinesLogger.logger.error("DTO", AIRPLANE_NOT_FOUND_MESSAGE);
+ return false;
+ }
+ player.getInventory().useItem(new ResilienceCommand(airplane.get()));
+ } else if (action == InventoryItemType.Place_Airplane) {
+ Optional onPathOpt = player.getPathById(pathId);
+ if (onPathOpt.isEmpty()) {
+ StreamlinesLogger.logger.warn("Attempted to place airplane on path that does not exist for that " +
+ "player");
+ return false;
+ }
+ PathService onPath = onPathOpt.get();
+ Airport nextAirport = onPath.calculateNextOnPath(position);
+
+ Airplane airplane = new Airplane(
+ position, nextAirport, onPath.getPath(), player.getId(), airplaneId
+ );
+
+ player.getInventory().useItem(new PlaceAirplaneCommand(airplane, onPath));
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "AirplaneUserInventoryActionDto[" +
+ "playerId=" + playerId + ", " +
+ "airplaneId=" + airplaneId + ", " +
+ "position=" + position + ", " +
+ "pathId=" + pathId + ", " +
+ "action=" + action + ']';
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ public String airplaneId() {
+ return airplaneId;
+ }
+
+ public Vector2 position() {
+ return position;
+ }
+
+ public String pathId() {
+ return pathId;
+ }
+
+ public InventoryItemType action() {
+ return action;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ AirplaneUserInventoryActionDto that = (AirplaneUserInventoryActionDto) obj;
+ return Objects.equals(this.playerId, that.playerId) &&
+ Objects.equals(this.airplaneId, that.airplaneId) &&
+ Objects.equals(this.position, that.position) &&
+ Objects.equals(this.pathId, that.pathId) &&
+ Objects.equals(this.action, that.action);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(playerId, airplaneId, position, pathId, action);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/AirportListDto.java b/shared/src/main/java/io/streamlines/network/AirportListDto.java
new file mode 100644
index 00000000..192b7512
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/AirportListDto.java
@@ -0,0 +1,58 @@
+package io.streamlines.network;
+
+import io.streamlines.StreamlinesLogger;
+import io.streamlines.flight.Airport;
+import io.streamlines.map.GameMap;
+
+import java.io.Serial;
+import java.util.Arrays;
+
+/**
+ * New data found to ADD TO old airport data (less error-prone than clearing & replacing)
+ */
+public final class AirportListDto implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private Airport[] airports;
+
+
+ public AirportListDto() {}
+
+ /**
+ * @param airports
+ */
+ public AirportListDto(Airport[] airports) {
+ this.airports = airports;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ StreamlinesLogger.logger.info("WS", "New Airport Data. Handling...");
+ for (Airport airport : airports) {
+ gameMap.addAirport(airport);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ AirportListDto that = (AirportListDto) obj;
+ return Arrays.equals(this.airports, that.airports);
+ }
+
+ @Override
+ public String toString() {
+ return "AirportListDto[" +
+ "airports=" + airports + ']';
+ }
+
+ public Airport[] airports() {
+ return airports;
+ }
+}
diff --git a/shared/src/main/java/io/streamlines/network/Broadcastable.java b/shared/src/main/java/io/streamlines/network/Broadcastable.java
index a690b895..374026c3 100644
--- a/shared/src/main/java/io/streamlines/network/Broadcastable.java
+++ b/shared/src/main/java/io/streamlines/network/Broadcastable.java
@@ -1,5 +1,7 @@
package io.streamlines.network;
+import java.io.Serializable;
+
/**
* Broadcastable across a network. Broadcasting can be standardized or raw.
*/
@@ -21,4 +23,6 @@ public interface Broadcastable {
* @param data The JSON data
*/
void broadcast(Packet.PacketType packetType, String data);
+
+ void broadcast(Packet.PacketType packetType, Serializable data);
}
diff --git a/shared/src/main/java/io/streamlines/network/DtoActionable.java b/shared/src/main/java/io/streamlines/network/DtoActionable.java
new file mode 100644
index 00000000..8e42cb27
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/DtoActionable.java
@@ -0,0 +1,14 @@
+package io.streamlines.network;
+
+import io.streamlines.map.GameMap;
+
+import java.io.Serializable;
+import java.util.function.BiFunction;
+
+/**
+ * For all data transfer objects. Takes in a game map and outputs whether to bounce
+ */
+public interface DtoActionable extends Serializable, BiFunction {
+ @Override
+ Boolean apply(GameMap gameMap, Boolean isClient);
+}
diff --git a/shared/src/main/java/io/streamlines/network/EditPathDto.java b/shared/src/main/java/io/streamlines/network/EditPathDto.java
new file mode 100644
index 00000000..794a3576
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/EditPathDto.java
@@ -0,0 +1,96 @@
+package io.streamlines.network;
+
+import io.streamlines.StreamlinesLogger;
+import io.streamlines.flight.Airport;
+import io.streamlines.flight.PathService;
+import io.streamlines.map.GameMap;
+
+import java.io.Serial;
+import java.util.Objects;
+import java.util.Optional;
+
+public final class EditPathDto
+ implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String playerId;
+ private String pathId;
+ private String airportId1;
+ private String airportId2;
+
+ public EditPathDto() {}
+
+ public EditPathDto(String playerId, String pathId, String airportId1, String airportId2) {
+ this.playerId = playerId;
+ this.pathId = pathId;
+ this.airportId1 = airportId1;
+ this.airportId2 = airportId2;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Optional pathOpt = gameMap.getPlayer(playerId).getPathById(pathId);
+ if (pathOpt.isEmpty()) {
+ StreamlinesLogger.logger.warn("Attempted to edit a path that does not exist for that player");
+ return false;
+ }
+ PathService path = pathOpt.get();
+
+ Airport airportStart = gameMap.getAirportById(airportId1);
+ Airport airportEnd = gameMap.getAirportById(airportId2);
+ if (airportStart != null && airportEnd != null) {
+ gameMap.getPlayer(playerId).editPath(path, airportStart, airportEnd);
+ } else {
+ StreamlinesLogger.logger.error("PEER-LISTENER", "Someone's cheating or we have a bug");
+ throw new SecurityException("Someone's cheating or we have a bug");
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "EditPathDto[" +
+ "playerId=" + playerId + ", " +
+ "pathId=" + pathId + ", " +
+ "airportId1=" + airportId1 + ", " +
+ "airportId2=" + airportId2 + ']';
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ public String pathId() {
+ return pathId;
+ }
+
+ public String airportId1() {
+ return airportId1;
+ }
+
+ public String airportId2() {
+ return airportId2;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ EditPathDto that = (EditPathDto) obj;
+ return Objects.equals(this.playerId, that.playerId) &&
+ Objects.equals(this.pathId, that.pathId) &&
+ Objects.equals(this.airportId1, that.airportId1) &&
+ Objects.equals(this.airportId2, that.airportId2);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(playerId, pathId, airportId1, airportId2);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/GambleTerminalDto.java b/shared/src/main/java/io/streamlines/network/GambleTerminalDto.java
new file mode 100644
index 00000000..4871506f
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/GambleTerminalDto.java
@@ -0,0 +1,77 @@
+package io.streamlines.network;
+
+import io.streamlines.flight.Airport;
+import io.streamlines.flight.Terminal;
+import io.streamlines.map.GameMap;
+import io.streamlines.map.Player;
+
+import java.io.Serial;
+import java.util.Objects;
+import java.util.Optional;
+
+public final class GambleTerminalDto implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String playerId;
+ private String airportId;
+ private boolean keep;
+
+ public GambleTerminalDto() {}
+ public GambleTerminalDto(String playerId, String airportId, boolean keep) {
+ this.playerId = playerId;
+ this.airportId = airportId;
+ this.keep = keep;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Airport airportStart = gameMap.getAirportById(airportId);
+ Optional terminal = airportStart.findTerminalByOwner(playerId);
+ if (terminal.isEmpty()) {
+ return false;
+ }
+ terminal.get().finishPending(keep);
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "GambleTerminalDto[" +
+ "playerId=" + playerId + ", " +
+ "airportId=" + airportId + ", " +
+ "keep=" + keep + ']';
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ public String airportId() {
+ return airportId;
+ }
+
+ public boolean keep() {
+ return keep;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ GambleTerminalDto that = (GambleTerminalDto) obj;
+ return Objects.equals(this.playerId, that.playerId) &&
+ Objects.equals(this.airportId, that.airportId) &&
+ this.keep == that.keep;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(playerId, airportId, keep);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/LandingDto.java b/shared/src/main/java/io/streamlines/network/LandingDto.java
new file mode 100644
index 00000000..eef3667b
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/LandingDto.java
@@ -0,0 +1,108 @@
+package io.streamlines.network;
+
+import io.streamlines.StreamlinesLogger;
+import io.streamlines.flight.Airport;
+import io.streamlines.flight.Terminal;
+import io.streamlines.map.GameMap;
+
+import java.io.Serial;
+import java.util.Objects;
+
+/**
+ * Handle plane landing. Side effect: adds to player's score and terminal throughput
+ */
+public final class LandingDto
+ implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private int throughput;
+ private String airportId;
+ private String airplaneId;
+ private String playerId;
+
+ public LandingDto() {}
+
+ /**
+ *
+ */
+ public LandingDto(int throughput, String airportId, String airplaneId, String playerId) {
+ this.throughput = throughput;
+ this.airportId = airportId;
+ this.airplaneId = airplaneId;
+ this.playerId = playerId;
+ }
+
+ /**
+ * @param gameMap The current state of the game map
+ * @param isClient true for setting the throughput through parameters, false if not (already set internally)
+ * @return whether the operation was successful
+ */
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Airport airport = gameMap.getAirportById(airportId);
+
+ if (airport.findTerminalByOwner(playerId).isEmpty()) {
+ StreamlinesLogger.logger.warn("Could not land airplane at airport where the player does not own a " +
+ "terminal");
+ return false;
+ }
+
+ if (isClient) {
+ airport.findTerminalByOwner(playerId).get().updateThroughput(throughput);
+ }
+ gameMap.getPlayer(playerId).addScore(throughput);
+
+ Terminal terminal = airport.findTerminalByOwner(playerId).get();
+ StreamlinesLogger.logger.info("AIRPLANE", "Landing complete for flight " +
+ airplaneId + " " + "(Terminal Throughput: " + terminal.getThroughput() + ")");
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "LandingDto[" +
+ "throughput=" + throughput + ", " +
+ "airportId=" + airportId + ", " +
+ "airplaneId=" + airplaneId + ", " +
+ "playerId=" + playerId + ']';
+ }
+
+ public int throughput() {
+ return throughput;
+ }
+
+ public String airportId() {
+ return airportId;
+ }
+
+ public String airplaneId() {
+ return airplaneId;
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ LandingDto that = (LandingDto) obj;
+ return this.throughput == that.throughput &&
+ Objects.equals(this.airportId, that.airportId) &&
+ Objects.equals(this.airplaneId, that.airplaneId) &&
+ Objects.equals(this.playerId, that.playerId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(throughput, airportId, airplaneId, playerId);
+ }
+
+
+}
+
diff --git a/shared/src/main/java/io/streamlines/network/LosePlayerDto.java b/shared/src/main/java/io/streamlines/network/LosePlayerDto.java
new file mode 100644
index 00000000..eb731dc5
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/LosePlayerDto.java
@@ -0,0 +1,61 @@
+package io.streamlines.network;
+
+import io.streamlines.StreamlinesLogger;
+import io.streamlines.map.GameMap;
+import io.streamlines.map.Player;
+
+import javax.management.InstanceNotFoundException;
+import javax.security.auth.login.AccountNotFoundException;
+import javax.security.auth.login.CredentialNotFoundException;
+import java.io.Serial;
+import java.util.Objects;
+
+public final class LosePlayerDto implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String playerId;
+
+ public LosePlayerDto() {}
+
+ public LosePlayerDto(String playerId) {
+ this.playerId = playerId;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Player player = gameMap.getPlayer(playerId);
+ if (player == null) {
+ StreamlinesLogger.logger.warn("Player " + playerId + " not found");
+ return false;
+ }
+ gameMap.removePlayerAndPossiblyEndGame(player);
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ LosePlayerDto that = (LosePlayerDto) obj;
+ return Objects.equals(this.playerId, that.playerId);
+ }
+
+ @Override
+ public String toString() {
+ return "LosePlayerDto[" +
+ "playerId=" + playerId + ']';
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(playerId);
+ }
+}
diff --git a/shared/src/main/java/io/streamlines/network/NewBidDto.java b/shared/src/main/java/io/streamlines/network/NewBidDto.java
new file mode 100644
index 00000000..2e7ef6bb
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/NewBidDto.java
@@ -0,0 +1,73 @@
+package io.streamlines.network;
+
+import io.streamlines.flight.Airport;
+import io.streamlines.flight.Terminal;
+import io.streamlines.map.GameMap;
+
+import java.io.Serial;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Requests a terminal with the given terminal id for the given player
+ * true if successful (a terminal exists and is granted), false if unsuccessful (requesting terminal did
+ * not provide one)
+ */
+public final class NewBidDto implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String terminalId;
+ private String owner;
+
+ public NewBidDto() {}
+ /**
+ *
+ */
+ public NewBidDto(String terminalId, String owner) {
+ this.terminalId = terminalId;
+ this.owner = owner;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ String airportId = terminalId.substring(0, 3);
+ Airport a = gameMap.getAirportById(airportId);
+ Optional found = a.findTerminalById(terminalId);
+ return found.filter(terminal -> a.requestTerminal(owner, terminal)).isPresent();
+ }
+
+ @Override
+ public String toString() {
+ return "NewBidDto[" +
+ "terminalId=" + terminalId + ", " +
+ "owner=" + owner + ']';
+ }
+
+ public String terminalId() {
+ return terminalId;
+ }
+
+ public String owner() {
+ return owner;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ NewBidDto that = (NewBidDto) obj;
+ return Objects.equals(this.terminalId, that.terminalId) &&
+ Objects.equals(this.owner, that.owner);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(terminalId, owner);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/NewDayDto.java b/shared/src/main/java/io/streamlines/network/NewDayDto.java
new file mode 100644
index 00000000..0341470b
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/NewDayDto.java
@@ -0,0 +1,71 @@
+package io.streamlines.network;
+
+import com.badlogic.gdx.utils.TimeUtils;
+import io.streamlines.StreamlinesLogger;
+import io.streamlines.map.GameMap;
+
+import java.io.Serial;
+import java.util.Objects;
+
+/**
+ * @see GameMap#startNewDay(int)
+ */
+public final class NewDayDto implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private int dayNumber;
+ private int time;
+
+ public NewDayDto() {}
+ /**
+ * @param dayNumber The day
+ * @param time The offset time due to network delays to keep time synced across peers
+ */
+ public NewDayDto(int dayNumber, int time) {
+ this.dayNumber = dayNumber;
+ this.time = time;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ gameMap.startNewDay(dayNumber);
+ GameMap.timeAtDayStart = (TimeUtils.millis() / 1000) - time;
+ StreamlinesLogger.logger.info("Time", "Day " + dayNumber + " has begun");
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "NewDayDto[" +
+ "dayNumber=" + dayNumber + ", " +
+ "time=" + time + ']';
+ }
+
+ public int dayNumber() {
+ return dayNumber;
+ }
+
+ public int time() {
+ return time;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ NewDayDto that = (NewDayDto) obj;
+ return this.dayNumber == that.dayNumber &&
+ this.time == that.time;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(dayNumber, time);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/NewPathDto.java b/shared/src/main/java/io/streamlines/network/NewPathDto.java
new file mode 100644
index 00000000..520d0a19
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/NewPathDto.java
@@ -0,0 +1,101 @@
+package io.streamlines.network;
+
+import io.streamlines.StreamlinesLogger;
+import io.streamlines.flight.Airport;
+import io.streamlines.map.GameMap;
+import io.streamlines.map.Player;
+
+import java.io.Serial;
+import java.util.Objects;
+
+public final class NewPathDto implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String playerId;
+ private String pathId;
+ private String airportId1;
+ private String airportId2;
+ private String airplaneId;
+
+ public NewPathDto() {}
+
+ public NewPathDto(String playerId, String pathId, String airportId1, String airportId2,
+ String airplaneId) {
+ this.playerId = playerId;
+ this.pathId = pathId;
+ this.airportId1 = airportId1;
+ this.airportId2 = airportId2;
+ this.airplaneId = airplaneId;
+ }
+
+ public NewPathDto(String playerId, String pathId, String airportId1, String airportId2) {
+ this(playerId, pathId, airportId1, airportId2, "");
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Airport airportStart = gameMap.getAirportById(airportId1);
+ Airport airportEnd = gameMap.getAirportById(airportId2);
+ Player player = gameMap.getPlayer(playerId);
+ if (airportStart != null && airportEnd != null) {
+ player.recreatePath(airportStart, airportEnd, airplaneId, pathId);
+ } else {
+ StreamlinesLogger.logger.error("PEER-LISTENER", "Someone's cheating or we have a bug");
+ throw new SecurityException("Someone's cheating or we have a bug");
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "NewPathDto[" +
+ "playerId=" + playerId + ", " +
+ "pathId=" + pathId + ", " +
+ "airportId1=" + airportId1 + ", " +
+ "airportId2=" + airportId2 + ", " +
+ "airplaneId=" + airplaneId + ']';
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ public String pathId() {
+ return pathId;
+ }
+
+ public String airportId1() {
+ return airportId1;
+ }
+
+ public String airportId2() {
+ return airportId2;
+ }
+
+ public String airplaneId() {
+ return airplaneId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ NewPathDto that = (NewPathDto) obj;
+ return Objects.equals(this.playerId, that.playerId) &&
+ Objects.equals(this.pathId, that.pathId) &&
+ Objects.equals(this.airportId1, that.airportId1) &&
+ Objects.equals(this.airportId2, that.airportId2) &&
+ Objects.equals(this.airplaneId, that.airplaneId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(playerId, pathId, airportId1, airportId2, airplaneId);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/Packet.java b/shared/src/main/java/io/streamlines/network/Packet.java
index bb16099b..cae20e36 100644
--- a/shared/src/main/java/io/streamlines/network/Packet.java
+++ b/shared/src/main/java/io/streamlines/network/Packet.java
@@ -1,7 +1,9 @@
package io.streamlines.network;
-import com.badlogic.gdx.utils.JsonReader;
-import com.badlogic.gdx.utils.JsonValue;
+import com.badlogic.gdx.utils.*;
+
+import java.io.Serializable;
+import java.util.Objects;
/**
* Packets are sent with the following format:
@@ -12,85 +14,71 @@
*/
public final class Packet {
private static final JsonReader jsonReader;
+ private static final Json json;
+ private final String contents;
+
static {
jsonReader = new JsonReader();
+ json = new Json(JsonWriter.OutputType.javascript);
}
public enum PacketType {
- UNASSIGNED("UNASSIGNED"),
- INITIALIZED("INITIALIZED"),
- AIRPORT_DATA("airport"),
- TERMINAL_DATA("terminal"),
- DAY_DATA("day"),
+ UNASSIGNED, INITIALIZED,
+ AIRPORT_DATA,
+ TERMINAL_USER_INVENTORY_ACTION, GAMBLE_TERMINAL,
+ DAY_DATA,
/**
* A player has chosen a name. Signals that the server should set up the player object.
*/
- PLAYER_NAME("playerName"),
- PLAYER_DATA("player"),
- /**
- * Airplane is ready for takeoff
- */
- AIRPLANE_DATA("airplane"),
- PATH_DATA("path"),
- PID_ASSIGNMENT("PID"),
- /**
- * Call player.editPath(paths[index], startAirport, endAirport).
- */
- PATH_EDIT("PATH_EDIT"),
- NEW_PATH("NEW_PATH"),
- PATH_RMV("PATH_RMV"),
- NEW_BID("NEW_BID"),
- /**
- * quota: The new throughput that the terminal has (server correction)
- * score: The additional score to add to the player who owns the airplane.
- * player: ID of the player
- * airport: ID of the airport
- */
- UNLOAD("UNLOAD"),
- SURPASSED("surpassed"),
- WEATHER_EVENT("weather"),
- REQUEST_PASSENGER_COUNT("request_passenger"),
- RETURN_PASSENGER_COUNT("return_passenger"),
- UPDATE_TRAFFIC("update_traffic"),
- TOP_DOG("topDog");
-
- private final String callsign;
- PacketType(String callsign) {
- this.callsign = callsign;
- }
+ PLAYER_NAME, PLAYER_DATA,
+ AIRPLANE_SYSTEM_ACTION, AIRPLANE_USER_INVENTORY_ACTION,
+ PID_ASSIGNMENT,
+ PATH_EDIT, NEW_PATH, PATH_RMV,
+ NEW_BID, UNLOAD, SURPASSED_CHOSE, SURPASSED_PROMPT,
+ WEATHER_EVENT,
+ REQUEST_PASSENGER_COUNT, RETURN_PASSENGER_COUNT, UPDATE_TRAFFIC,
+ TOP_DOG_PLACE_AIRPLANE, TOP_DOG_IDENTIFY, TOP_DOG_PLACE_ROUTE;
+
+ private final String callsign = name();
}
public PacketType getPacketType() {
for (PacketType packetType : PacketType.values()) {
- if (contents.startsWith(packetType.callsign + "="))
+ if (contents.startsWith(packetType.name() + "=")) {
return packetType;
+ }
}
return null;
}
- private final String contents;
-
/**
* Creates packet for receiving serialized contents, so it can get parsed.
- * @param rawContents String representation of data
+ *
+ * @param contents String representation of data
*/
- public Packet(String rawContents) {
- this.contents = rawContents;
+ public Packet(String contents) {
+ this.contents = contents;
}
/**
* Creates packet for sending manually serialized contents
- * @param type The type of packet
+ *
+ * @param type The type of packet
* @param contents String representation of data.
*/
public Packet(PacketType type, String contents) {
- this.contents = type.callsign + "=" + contents;
+ this(type.callsign + "=" + contents);
+ }
+
+ public Packet(PacketType type, Serializable data) {
+ this(type.callsign + "=" + json.toJson(data));
}
public String getData() {
- if (getPacketType() == null)
+ if (getPacketType() == null) {
return "NULL_PACKET";
+ }
return contents.substring((getPacketType().callsign + "=").length());
}
@@ -98,7 +86,30 @@ public JsonValue parse() {
return jsonReader.parse(getData());
}
- public String getContents() {
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ Packet that = (Packet) obj;
+ return Objects.equals(this.contents, that.contents);
+ }
+
+ @Override
+ public String toString() {
+ return "Packet[" +
+ "contents=" + contents + ']';
+ }
+
+ public String contents() {
return contents;
}
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(contents);
+ }
}
diff --git a/shared/src/main/java/io/streamlines/network/PeerDto.java b/shared/src/main/java/io/streamlines/network/PeerDto.java
index d2890445..7f5ed991 100644
--- a/shared/src/main/java/io/streamlines/network/PeerDto.java
+++ b/shared/src/main/java/io/streamlines/network/PeerDto.java
@@ -1,4 +1,41 @@
package io.streamlines.network;
-public record PeerDto(String playerId) {
+import java.util.Objects;
+
+public final class PeerDto {
+ private String playerId;
+
+ public PeerDto() {}
+ public PeerDto(String playerId) {
+ this.playerId = playerId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ PeerDto that = (PeerDto) obj;
+ return Objects.equals(this.playerId, that.playerId);
+ }
+
+ @Override
+ public String toString() {
+ return "PeerDto[" +
+ "playerId=" + playerId + ']';
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(playerId);
+ }
+
+
}
diff --git a/shared/src/main/java/io/streamlines/network/PeerListener.java b/shared/src/main/java/io/streamlines/network/PeerListener.java
deleted file mode 100644
index acbb104a..00000000
--- a/shared/src/main/java/io/streamlines/network/PeerListener.java
+++ /dev/null
@@ -1,241 +0,0 @@
-package io.streamlines.network;
-
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.JsonValue;
-import com.badlogic.gdx.utils.TimeUtils;
-import io.streamlines.StreamlinesLogger;
-import io.streamlines.flight.*;
-import io.streamlines.inventory.*;
-import io.streamlines.map.*;
-
-import java.util.Optional;
-
-/**
- * Shared actions between game server and game clients when a message is received over web socket.
- * Should only contain actions that both the server and client do. Client-specific actions should stay on the client
- */
-public class PeerListener {
- private final GameMap gameMap;
- private JsonValue params;
-
- public PeerListener(GameMap map) {
- gameMap = map;
- }
-
- /**
- * Parses parameters from packet
- * @param packet The packet object containing JSON-serialized key-value pairs
- */
- JsonValue readPacket(Packet packet) {
- return params = packet.parse();
- }
-
- public void writePacket(JsonValue packet) {
- params = packet;
- }
-
- private Player getPlayer() {
- return gameMap.getPlayer(params.getString("player"));
- }
-
- public void handleNewDay() {
- int dayNumber = params.getInt("day");
- gameMap.startNewDay(dayNumber);
- GameMap.timeAtDayStart = (TimeUtils.millis() / 1000) - params.getInt("time");
- StreamlinesLogger.logger.info("Time", "Day " + dayNumber + " has begun");
- }
-
- /**
- * Uses airplane-related (infrastructure and powerup) items from inventory
- */
- public void handleAirplaneData() {
- Player player = gameMap.getPlayer(params.getString("player_id"));
- if (params.getString("action").equals("expand")) {
- player.getInventory().useItem(new ExpandAirplaneCommand(
- gameMap.getAirplaneById(params.getString("id"))
- ));
- } else if (params.getString("action").equals("resilience")) {
- gameMap.getPlayer(params.getString("player_id")).getInventory().useItem(new ResilienceCommand(
- gameMap.getAirplaneById(params.getString("id"))
- ));
- } else if (params.getString("action").equals("place")) {
- Optional onPathOpt = player.getPathById(params.getInt("path"));
- if (onPathOpt.isEmpty()) {
- StreamlinesLogger.logger.warn("Attempted to place airplane on path that does not exist for that " +
- "player");
- return;
- }
- PathService onPath = onPathOpt.get();
- Airport nextAirport = onPath.calculateNextOnPath(
- params.getFloat("positionX"), params.getFloat("positionY")
- );
-
- Airplane airplane = new Airplane(
- new Vector2(params.getFloat("positionX"), params.getFloat("positionY")),
- nextAirport,
- onPath.getPath(),
- player.getId(),
- params.getString("id")
- );
-
- player.getInventory().useItem(new PlaceAirplaneCommand(airplane, onPath));
- }
- }
-
- public boolean handleTerminalData() {
- Player player = gameMap.getPlayer(params.getString("player_id"));
- Airport airportStart = gameMap.getAirportById(params.getString("id"));
- Optional terminal = airportStart.findTerminalByOwner(player.getId());
- if (terminal.isEmpty()) {
- return false;
- }
- if (params.getString("action").equals("expand")) {
- player.getInventory().useItem(new ExpandTerminalCommand(terminal.get()));
- } else if (params.getString("action").equals("gamble")) {
- terminal.get().finishPending(params.getBoolean("keep"));
- }
- return true;
- }
-
- public boolean editPath() {
- Optional pathOpt = getPlayer().getPathById(params.getInt("path"));
- if (pathOpt.isEmpty()) {
- StreamlinesLogger.logger.warn("Attempted to edit a path that does not exist for that player");
- return false;
- }
- PathService path = pathOpt.get();
-
- Airport airportStart = gameMap.getAirportById(params.getString("1"));
- Airport airportEnd = gameMap.getAirportById(params.getString("2"));
- if (airportStart != null && airportEnd != null) {
- getPlayer().editPath(path, airportStart, airportEnd);
- } else {
- StreamlinesLogger.logger.error("PEER-LISTENER", "Someone's cheating or we have a bug");
- throw new SecurityException("Someone's cheating or we have a bug");
- }
- return true;
- }
-
- public boolean removeAirportFromPath() {
- PathService path = gameMap.getPathServiceById(params.getString("pathId"), getPlayer().getId());
- Airport toRemove = gameMap.getAirportById(params.getString("airportId"));
- Player player = getPlayer();
- if (gameMap.removeAirportFromPath(path, toRemove, player)) {
- player.removePath(path);
- }
- return true; // Assume success either way
- }
-
- boolean newPath() {
- Airport airportStart = gameMap.getAirportById(params.getString("1"));
- Airport airportEnd = gameMap.getAirportById(params.getString("2"));
- String airplaneId = params.getString("airplaneId", "");
- if (airportStart != null && airportEnd != null) {
- getPlayer().recreatePath(airportStart, airportEnd, airplaneId, params.getString("path", ""));
- } else {
- StreamlinesLogger.logger.error("PEER-LISTENER", "Someone's cheating or we have a bug");
- throw new SecurityException("Someone's cheating or we have a bug");
- }
- return true;
- }
-
- /**
- * Requests a terminal with the given terminal id for the given player
- * @return true if successful (a terminal exists and is granted), false if unsuccessful (requesting terminal did
- * not provide one)
- */
- public boolean handleNewBid() {
- String terminalId = params.getString("terminal");
- String airportId = terminalId.substring(0, 3);
- Airport a = gameMap.getAirportById(airportId);
- Optional found = a.findTerminalById(terminalId);
- return found.filter(terminal -> a.requestTerminal(params.getString("owner"), terminal)).isPresent();
- }
-
- public boolean handleTopDog() {
- if (params.getString("action").equals("identify")) {
- gameMap.setTopDog(params.getString("id"));
- } else if (params.getString("action").equals("use")) {
- Player victim = gameMap.getPlayer(params.getString("victim"));
- if (victim.getPathById(params.getInt("path")).isEmpty()) {
- StreamlinesLogger.logger.warn("Path does not exist for victim");
- return false;
- }
-
- if (params.getString("infrastructureType").equals("plane")) {
- var command = new PlaceAirplaneCommand(
- gameMap.getAirplaneById(params.getString("airplane")),
- victim.getPathById(params.getInt("path")).get()
- );
- gameMap.getPlayer(gameMap.getTopDog()).getInventory().useItem(new TopDogCommand(victim.getId(),
- command));
- } else if (params.getString("infrastructureType").equals("route")) {
- var command = new PlaceRouteCommand(
- gameMap.getAirportById(params.getString("start")),
- gameMap.getAirportById(params.getString("end")),
- victim.getPathById(params.getInt("path")).get()
- );
- gameMap.getPlayer(gameMap.getTopDog()).getInventory().useItem(new TopDogCommand(victim.getId(),
- command));
- } else {
- return false;
- }
- }
-
- return true;
- }
-
- public boolean addNewInventoryItems() {
- if (params.has("action") && params.getString("action").equals("chose")) {
- getPlayer().getInventory().addItem(InventoryItemType.valueOf(params.getString("item")));
- getPlayer().getInventory().addItem(InventoryItemType.Place_Route);
- }
- return true;
- }
-
- /**
- * Handle plane landing
- * @param servile true for setting the throughput through parameters, false if not (already set internally)
- * @return whether the operation was successful
- */
- public boolean handleLanding(boolean servile) {
- int throughput = params.getInt("throughput");
- Airport airport = gameMap.getAirportById(params.getString("airport"));
-
- if (airport.findTerminalByOwner(getPlayer().getId()).isEmpty()) {
- StreamlinesLogger.logger.warn("Could not land airplane at airport where the player does not own a " +
- "terminal");
- return false;
- }
-
- if (servile) {
- airport.findTerminalByOwner(getPlayer().getId()).get().updateThroughput(throughput);
- }
- getPlayer().addScore(throughput);
-
- Terminal terminal = airport.findTerminalByOwner(getPlayer().getId()).get();
- StreamlinesLogger.logger.info("AIRPLANE", "Landing complete for flight " +
- params.getString("airplane") + " " + "(Terminal Throughput: " + terminal.getThroughput() + ")");
- return true;
- }
-
- public void handleWeatherEvent() {
- Airport affectedAirport = gameMap.getAirportById(params.getString("airport"));
- WeatherEventSeverity status = WeatherEventSeverity.values()[params.getInt("status")];
- if (params.getInt("status") == WeatherEventSeverity.FINE.ordinal()) {
- affectedAirport.clearWeatherEvent();
- } else {
- affectedAirport.handleWeatherEvent(status);
- }
- }
-
- public record LosePlayer(String playerId) {}
-
- public void handlePlayerLoss(LosePlayer dto) {
- gameMap.removePlayer(dto.playerId());
- if(gameMap.shouldEnd()) {
- gameMap.end();
- }
- }
-
-}
diff --git a/shared/src/main/java/io/streamlines/network/PidAssignmentDto.java b/shared/src/main/java/io/streamlines/network/PidAssignmentDto.java
new file mode 100644
index 00000000..fbcc6d35
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/PidAssignmentDto.java
@@ -0,0 +1,63 @@
+package io.streamlines.network;
+
+import com.badlogic.gdx.graphics.Color;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Objects;
+
+public final class PidAssignmentDto implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String id;
+ private String name;
+ private Color color;
+
+ public PidAssignmentDto() {}
+ public PidAssignmentDto(String id, String name, Color color) {
+ this.id = id;
+ this.name = name;
+ this.color = color;
+ }
+
+ @Override
+ public String toString() {
+ return "PidAssignmentDto[" +
+ "id=" + id + ", " +
+ "name=" + name + ", " +
+ "color=" + color + ']';
+ }
+
+ public String id() {
+ return id;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public Color color() {
+ return color;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ PidAssignmentDto that = (PidAssignmentDto) obj;
+ return Objects.equals(this.id, that.id) &&
+ Objects.equals(this.name, that.name) &&
+ Objects.equals(this.color, that.color);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, color);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/PlayerNameDto.java b/shared/src/main/java/io/streamlines/network/PlayerNameDto.java
new file mode 100644
index 00000000..017dc5cf
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/PlayerNameDto.java
@@ -0,0 +1,45 @@
+package io.streamlines.network;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Objects;
+
+public final class PlayerNameDto implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String name;
+
+ public PlayerNameDto() { }
+ public PlayerNameDto(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ PlayerNameDto that = (PlayerNameDto) obj;
+ return Objects.equals(this.name, that.name);
+ }
+
+ @Override
+ public String toString() {
+ return "PlayerNameDto[" +
+ "name=" + name + ']';
+ }
+
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/RemovePathDto.java b/shared/src/main/java/io/streamlines/network/RemovePathDto.java
new file mode 100644
index 00000000..ea8e8433
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/RemovePathDto.java
@@ -0,0 +1,80 @@
+package io.streamlines.network;
+
+import io.streamlines.StreamlinesLogger;
+import io.streamlines.flight.Airport;
+import io.streamlines.flight.PathService;
+import io.streamlines.map.GameMap;
+import io.streamlines.map.Player;
+
+import java.io.Serial;
+import java.util.Objects;
+import java.util.Optional;
+
+public final class RemovePathDto implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String pathId;
+ private String airportId;
+ private String playerId;
+
+ public RemovePathDto() {}
+ public RemovePathDto(String pathId, String airportId, String playerId) {
+ this.pathId = pathId;
+ this.airportId = airportId;
+ this.playerId = playerId;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Player player = gameMap.getPlayer(playerId);
+ Optional path = player.getPathById(pathId);
+ if (path.isEmpty()) {
+ StreamlinesLogger.logger.warn("No such path with id " + pathId + " belongs to player " + playerId);
+ return false;
+ }
+ Airport toRemove = gameMap.getAirportById(airportId);
+ player.removeAirportFromPathAndPossiblyRemovePath(path.get(), toRemove); // Assume success either way
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "RemovePathDto[" +
+ "pathId=" + pathId + ", " +
+ "airportId=" + airportId + ", " +
+ "playerId=" + playerId + ']';
+ }
+
+ public String pathId() {
+ return pathId;
+ }
+
+ public String airportId() {
+ return airportId;
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ RemovePathDto that = (RemovePathDto) obj;
+ return Objects.equals(this.pathId, that.pathId) &&
+ Objects.equals(this.airportId, that.airportId) &&
+ Objects.equals(this.playerId, that.playerId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(pathId, airportId, playerId);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/RequestPassengerCountDto.java b/shared/src/main/java/io/streamlines/network/RequestPassengerCountDto.java
new file mode 100644
index 00000000..df2df2d7
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/RequestPassengerCountDto.java
@@ -0,0 +1,58 @@
+package io.streamlines.network;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Objects;
+
+/// The server also verifies that this player is authorized to see the passenger counts (owns the
+/// plane)
+public final class RequestPassengerCountDto implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String planeId;
+ private String player;
+
+ public RequestPassengerCountDto() {}
+ /**
+ *
+ */
+ public RequestPassengerCountDto(String planeId, String player) {
+ this.planeId = planeId;
+ this.player = player;
+ }
+
+ @Override
+ public String toString() {
+ return "RequestPassengerCountDto[" +
+ "planeId=" + planeId + ", " +
+ "player=" + player + ']';
+ }
+
+ public String planeId() {
+ return planeId;
+ }
+
+ public String player() {
+ return player;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ RequestPassengerCountDto that = (RequestPassengerCountDto) obj;
+ return Objects.equals(this.planeId, that.planeId) &&
+ Objects.equals(this.player, that.player);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(planeId, player);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/ReturnPassengerCountDto.java b/shared/src/main/java/io/streamlines/network/ReturnPassengerCountDto.java
new file mode 100644
index 00000000..041875f5
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/ReturnPassengerCountDto.java
@@ -0,0 +1,67 @@
+package io.streamlines.network;
+
+import io.streamlines.map.GameMap;
+
+import java.io.Serial;
+import java.util.Objects;
+
+/**
+ * Returns the passenger count to the specific player who requested it. Does not bounce.
+ */
+public final class ReturnPassengerCountDto implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String planeId;
+ private int passengerCount;
+
+ public ReturnPassengerCountDto() {}
+ /**
+ * @param planeId The id of the plane
+ * @param passengerCount The number of passengers for that plane
+ */
+ public ReturnPassengerCountDto(String planeId, int passengerCount) {
+ this.planeId = planeId;
+ this.passengerCount = passengerCount;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ gameMap.getAirplaneById(planeId).ifPresent(airplane -> airplane.setGenericPassengerCount(passengerCount));
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "ReturnPassengerCountDto[" +
+ "planeId=" + planeId + ", " +
+ "passengerCount=" + passengerCount + ']';
+ }
+
+ public String planeId() {
+ return planeId;
+ }
+
+ public int passengerCount() {
+ return passengerCount;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ ReturnPassengerCountDto that = (ReturnPassengerCountDto) obj;
+ return Objects.equals(this.planeId, that.planeId) &&
+ this.passengerCount == that.passengerCount;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(planeId, passengerCount);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/SetTopDogDto.java b/shared/src/main/java/io/streamlines/network/SetTopDogDto.java
new file mode 100644
index 00000000..ef1c600b
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/SetTopDogDto.java
@@ -0,0 +1,53 @@
+package io.streamlines.network;
+
+import io.streamlines.map.GameMap;
+
+import java.io.Serial;
+import java.util.Objects;
+
+public final class SetTopDogDto
+ implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String playerId;
+
+ public SetTopDogDto() {}
+ public SetTopDogDto(String playerId) {
+ this.playerId = playerId;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ gameMap.setTopDog(playerId);
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ SetTopDogDto that = (SetTopDogDto) obj;
+ return Objects.equals(this.playerId, that.playerId);
+ }
+
+ @Override
+ public String toString() {
+ return "SetTopDogDto[" +
+ "playerId=" + playerId + ']';
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(playerId);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/SurpassedChoseDto.java b/shared/src/main/java/io/streamlines/network/SurpassedChoseDto.java
new file mode 100644
index 00000000..704c6586
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/SurpassedChoseDto.java
@@ -0,0 +1,64 @@
+package io.streamlines.network;
+
+import io.streamlines.inventory.InventoryItemType;
+import io.streamlines.map.GameMap;
+import io.streamlines.map.Player;
+
+import java.io.Serial;
+import java.util.Objects;
+
+public final class SurpassedChoseDto implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String playerId;
+ private InventoryItemType item;
+
+ public SurpassedChoseDto() {}
+ public SurpassedChoseDto(String playerId, InventoryItemType item) {
+ this.playerId = playerId;
+ this.item = item;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Player player = gameMap.getPlayer(playerId);
+ player.getInventory().addItem(item);
+ player.getInventory().addItem(InventoryItemType.Place_Route);
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "SurpassedChoseDto[" +
+ "playerId=" + playerId + ", " +
+ "item=" + item + ']';
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ public InventoryItemType item() {
+ return item;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ SurpassedChoseDto that = (SurpassedChoseDto) obj;
+ return Objects.equals(this.playerId, that.playerId) &&
+ Objects.equals(this.item, that.item);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(playerId, item);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/SurpassedPromptDto.java b/shared/src/main/java/io/streamlines/network/SurpassedPromptDto.java
new file mode 100644
index 00000000..7a5e5db6
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/SurpassedPromptDto.java
@@ -0,0 +1,58 @@
+package io.streamlines.network;
+
+import io.streamlines.inventory.InventoryItemType;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Objects;
+
+/// DTO for surpassed prompt action
+public final class SurpassedPromptDto implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private List powerups;
+ private List infrastructure;
+
+ public SurpassedPromptDto() {}
+ public SurpassedPromptDto(List powerups,
+ List infrastructure) {
+ this.powerups = powerups;
+ this.infrastructure = infrastructure;
+ }
+
+ @Override
+ public String toString() {
+ return "SurpassedPromptDto[" +
+ "powerups=" + powerups + ", " +
+ "infrastructure=" + infrastructure + ']';
+ }
+
+ public List powerups() {
+ return powerups;
+ }
+
+ public List infrastructure() {
+ return infrastructure;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ SurpassedPromptDto that = (SurpassedPromptDto) obj;
+ return Objects.equals(this.powerups, that.powerups) &&
+ Objects.equals(this.infrastructure, that.infrastructure);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(powerups, infrastructure);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/TerminalUserInventoryActionDto.java b/shared/src/main/java/io/streamlines/network/TerminalUserInventoryActionDto.java
new file mode 100644
index 00000000..83476ec3
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/TerminalUserInventoryActionDto.java
@@ -0,0 +1,83 @@
+package io.streamlines.network;
+
+import io.streamlines.flight.Airport;
+import io.streamlines.flight.Terminal;
+import io.streamlines.inventory.ExpandTerminalCommand;
+import io.streamlines.inventory.InventoryItemType;
+import io.streamlines.map.GameMap;
+import io.streamlines.map.Player;
+
+import java.io.Serial;
+import java.util.Objects;
+import java.util.Optional;
+
+public final class TerminalUserInventoryActionDto
+ implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String playerId;
+ private String airportId;
+ private InventoryItemType action;
+
+ public TerminalUserInventoryActionDto() {}
+ public TerminalUserInventoryActionDto(String playerId, String airportId, InventoryItemType action) {
+ this.playerId = playerId;
+ this.airportId = airportId;
+ this.action = action;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Player player = gameMap.getPlayer(playerId);
+ Airport airportStart = gameMap.getAirportById(airportId);
+ Optional terminal = airportStart.findTerminalByOwner(playerId);
+ if (terminal.isEmpty()) {
+ return false;
+ }
+ if (action == InventoryItemType.Expand_Terminal) {
+ player.getInventory().useItem(new ExpandTerminalCommand(terminal.get()));
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "TerminalUserInventoryActionDto[" +
+ "playerId=" + playerId + ", " +
+ "airportId=" + airportId + ", " +
+ "action=" + action + ']';
+ }
+
+ public String playerId() {
+ return playerId;
+ }
+
+ public String airportId() {
+ return airportId;
+ }
+
+ public InventoryItemType action() {
+ return action;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ TerminalUserInventoryActionDto that = (TerminalUserInventoryActionDto) obj;
+ return Objects.equals(this.playerId, that.playerId) &&
+ Objects.equals(this.airportId, that.airportId) &&
+ Objects.equals(this.action, that.action);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(playerId, airportId, action);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/TopDogPlaceAirplaneDto.java b/shared/src/main/java/io/streamlines/network/TopDogPlaceAirplaneDto.java
new file mode 100644
index 00000000..046a81d4
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/TopDogPlaceAirplaneDto.java
@@ -0,0 +1,85 @@
+package io.streamlines.network;
+
+import io.streamlines.StreamlinesLogger;
+import io.streamlines.inventory.*;
+import io.streamlines.map.GameMap;
+import io.streamlines.map.Player;
+
+import java.io.Serial;
+import java.util.Objects;
+
+public final class TopDogPlaceAirplaneDto
+ implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String airplaneId;
+ private String pathId;
+ private String victimId;
+
+ public TopDogPlaceAirplaneDto() {}
+ public TopDogPlaceAirplaneDto(String airplaneId, String pathId, String victimId) {
+ this.airplaneId = airplaneId;
+ this.pathId = pathId;
+ this.victimId = victimId;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Player victim = gameMap.getPlayer(victimId);
+ if (victim.getPathById(pathId).isEmpty()) {
+ StreamlinesLogger.logger.warn("Path does not exist for victim");
+ return false;
+ }
+
+ if (victim.getPathById(pathId).isEmpty() || gameMap.getAirplaneById(airplaneId).isEmpty()) {
+ return false;
+ }
+ InfrastructureItem command = new PlaceAirplaneCommand(gameMap.getAirplaneById(airplaneId).get(),
+ victim.getPathById(pathId).get());
+ gameMap.getPlayer(gameMap.getTopDog()).getInventory().useItem(new TopDogCommand(victim.getId(),
+ command));
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "TopDogPlaceAirplaneDto[" +
+ "airplaneId=" + airplaneId + ", " +
+ "pathId=" + pathId + ", " +
+ "victimId=" + victimId + ']';
+ }
+
+ public String airplaneId() {
+ return airplaneId;
+ }
+
+ public String pathId() {
+ return pathId;
+ }
+
+ public String victimId() {
+ return victimId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ TopDogPlaceAirplaneDto that = (TopDogPlaceAirplaneDto) obj;
+ return Objects.equals(this.airplaneId, that.airplaneId) &&
+ Objects.equals(this.pathId, that.pathId) &&
+ Objects.equals(this.victimId, that.victimId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(airplaneId, pathId, victimId);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/TopDogPlaceRouteDto.java b/shared/src/main/java/io/streamlines/network/TopDogPlaceRouteDto.java
new file mode 100644
index 00000000..e861f9cd
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/TopDogPlaceRouteDto.java
@@ -0,0 +1,94 @@
+package io.streamlines.network;
+
+import io.streamlines.StreamlinesLogger;
+import io.streamlines.inventory.*;
+import io.streamlines.map.GameMap;
+import io.streamlines.map.Player;
+
+import java.io.Serial;
+import java.util.Objects;
+
+public final class TopDogPlaceRouteDto
+ implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private String victimId;
+ private String pathId;
+ private String airportStart;
+ private String airportEnd;
+
+ public TopDogPlaceRouteDto() {}
+ public TopDogPlaceRouteDto(String victimId, String pathId, String airportStart, String airportEnd) {
+ this.victimId = victimId;
+ this.pathId = pathId;
+ this.airportStart = airportStart;
+ this.airportEnd = airportEnd;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Player victim = gameMap.getPlayer(victimId);
+ if (victim.getPathById(pathId).isEmpty()) {
+ StreamlinesLogger.logger.warn("Path does not exist for victim");
+ return false;
+ }
+
+ InfrastructureItem command = new PlaceRouteCommand(
+ gameMap.getAirportById(airportStart),
+ gameMap.getAirportById(airportEnd),
+ victim.getPathById(pathId).get()
+ );
+ gameMap.getPlayer(gameMap.getTopDog()).getInventory().useItem(
+ new TopDogCommand(victim.getId(), command)
+ );
+
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "TopDogPlaceRouteDto[" +
+ "victimId=" + victimId + ", " +
+ "pathId=" + pathId + ", " +
+ "airportStart=" + airportStart + ", " +
+ "airportEnd=" + airportEnd + ']';
+ }
+
+ public String victimId() {
+ return victimId;
+ }
+
+ public String pathId() {
+ return pathId;
+ }
+
+ public String airportStart() {
+ return airportStart;
+ }
+
+ public String airportEnd() {
+ return airportEnd;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ TopDogPlaceRouteDto that = (TopDogPlaceRouteDto) obj;
+ return Objects.equals(this.victimId, that.victimId) &&
+ Objects.equals(this.pathId, that.pathId) &&
+ Objects.equals(this.airportStart, that.airportStart) &&
+ Objects.equals(this.airportEnd, that.airportEnd);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(victimId, pathId, airportStart, airportEnd);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/UpdateTraffic.java b/shared/src/main/java/io/streamlines/network/UpdateTraffic.java
new file mode 100644
index 00000000..bd221e01
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/UpdateTraffic.java
@@ -0,0 +1,69 @@
+package io.streamlines.network;
+
+import com.badlogic.gdx.utils.Array;
+import io.streamlines.flight.Airport;
+import io.streamlines.map.GameMap;
+
+import java.util.Objects;
+
+/// Data transfer object for update traffic command
+public final class UpdateTraffic {
+ private String airport;
+ private int trafficCount;
+ private Array destinations;
+
+ public UpdateTraffic() {}
+ public UpdateTraffic(String airport, int trafficCount, Array destinations) {
+ this.airport = airport;
+ this.trafficCount = trafficCount;
+ this.destinations = destinations;
+ }
+
+ void accept(GameMap gameMap) {
+ Airport toUpdate = gameMap.getAirportById(airport());
+ toUpdate.updateTrafficCount(trafficCount());
+ toUpdate.clearDestinations();
+ destinations().forEach(toUpdate::addDestination);
+ }
+
+ @Override
+ public String toString() {
+ return "UpdateTraffic[" +
+ "airport=" + airport + ", " +
+ "trafficCount=" + trafficCount + ", " +
+ "destinations=" + destinations + ']';
+ }
+
+ public String airport() {
+ return airport;
+ }
+
+ public int trafficCount() {
+ return trafficCount;
+ }
+
+ public Array destinations() {
+ return destinations;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ UpdateTraffic that = (UpdateTraffic) obj;
+ return Objects.equals(this.airport, that.airport) &&
+ this.trafficCount == that.trafficCount &&
+ Objects.equals(this.destinations, that.destinations);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(airport, trafficCount, destinations);
+ }
+
+
+}
diff --git a/shared/src/main/java/io/streamlines/network/UpdateTrafficListDto.java b/shared/src/main/java/io/streamlines/network/UpdateTrafficListDto.java
new file mode 100644
index 00000000..95df26a3
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/UpdateTrafficListDto.java
@@ -0,0 +1,20 @@
+package io.streamlines.network;
+
+import com.badlogic.gdx.utils.Array;
+import io.streamlines.map.GameMap;
+
+public class UpdateTrafficListDto implements DtoActionable {
+ private Array updateTrafficList;
+ public UpdateTrafficListDto() {}
+ public UpdateTrafficListDto(Array updateTrafficList) {
+ this.updateTrafficList = updateTrafficList;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ if (isClient) {
+ updateTrafficList.forEach(x -> x.accept(gameMap));
+ }
+ return false;
+ }
+}
diff --git a/shared/src/main/java/io/streamlines/network/WeatherDto.java b/shared/src/main/java/io/streamlines/network/WeatherDto.java
new file mode 100644
index 00000000..3a7cbfa0
--- /dev/null
+++ b/shared/src/main/java/io/streamlines/network/WeatherDto.java
@@ -0,0 +1,67 @@
+package io.streamlines.network;
+
+import io.streamlines.flight.Airport;
+import io.streamlines.map.GameMap;
+import io.streamlines.map.WeatherEventSeverity;
+
+import java.io.Serial;
+import java.util.Objects;
+
+public final class WeatherDto implements DtoActionable {
+ @Serial
+ private static final long serialVersionUID = 0L;
+ private WeatherEventSeverity status;
+ private String airportId;
+
+ public WeatherDto() {}
+ public WeatherDto(WeatherEventSeverity status, String airportId) {
+ this.status = status;
+ this.airportId = airportId;
+ }
+
+ @Override
+ public Boolean apply(GameMap gameMap, Boolean isClient) {
+ Airport affectedAirport = gameMap.getAirportById(airportId);
+ if (status == WeatherEventSeverity.FINE) {
+ affectedAirport.clearWeatherEvent();
+ } else {
+ affectedAirport.handleWeatherEvent(status);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "WeatherDto[" +
+ "status=" + status + ", " +
+ "airportId=" + airportId + ']';
+ }
+
+ public WeatherEventSeverity status() {
+ return status;
+ }
+
+ public String airportId() {
+ return airportId;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != this.getClass()) {
+ return false;
+ }
+ WeatherDto that = (WeatherDto) obj;
+ return Objects.equals(this.status, that.status) &&
+ Objects.equals(this.airportId, that.airportId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(status, airportId);
+ }
+
+
+}
diff --git a/shared/src/test/java/io/streamlines/QuotaTest.java b/shared/src/test/java/io/streamlines/QuotaTest.java
index 55faa849..57050701 100644
--- a/shared/src/test/java/io/streamlines/QuotaTest.java
+++ b/shared/src/test/java/io/streamlines/QuotaTest.java
@@ -16,8 +16,9 @@
public class QuotaTest {
private static GameMap gameMap;
private static Airport start, second, third;
+ private final static String PLAYER_ID = "TRENT";
private static Player getPlayer() {
- return gameMap.getPlayer("TRENT");
+ return gameMap.getPlayer(PLAYER_ID);
}
@BeforeAll
@@ -29,21 +30,23 @@ public static void initialize() {
gameMap.addAirport(start);
gameMap.addAirport(second);
gameMap.addAirport(third);
-
- gameMap.newPlayer("TRENT");
+ // Prevent the game from ending
+ gameMap.newPlayer(PLAYER_ID + "A");
+ gameMap.newPlayer(PLAYER_ID + "B");
}
@BeforeEach
public void setup() {
- start.requestTerminal(getPlayer().getId());
- second.requestTerminal(getPlayer().getId());
- third.requestTerminal(getPlayer().getId());
+ gameMap.newPlayer(PLAYER_ID);
+ start.requestTerminal(PLAYER_ID);
+ second.requestTerminal(PLAYER_ID);
+ third.requestTerminal(PLAYER_ID);
}
@ParameterizedTest
@ValueSource(booleans = { true, false })
public void testLosesPathOnLoseOneOfTwoTerminalsRegardlessOfFlight(boolean flight) {
- gameMap.startNewDay(2);
+ gameMap.startNewDay();
PathService path = getPlayer().createPath(start, second);
if (flight) {
for (int i = 0; i < 50; i++) {
@@ -54,13 +57,13 @@ public void testLosesPathOnLoseOneOfTwoTerminalsRegardlessOfFlight(boolean fligh
for (int i = 0; i < 5; i++) path.getFirstPlane().update();
}
- gameMap.startNewDay(3);
- assertTrue(start.findTerminalByOwner("TRENT").isPresent());
- Terminal startTerminal = start.findTerminalByOwner("TRENT").get();
+ gameMap.startNewDay();
+ assertTrue(start.findTerminalByOwner(PLAYER_ID).isPresent());
+ Terminal startTerminal = start.findTerminalByOwner(PLAYER_ID).get();
startTerminal.finishPending(false);
- gameMap.startNewDay(4);
+ gameMap.startNewDay();
assertFalse(getPlayer().getPaths().contains(path));
- assertTrue(start.findTerminalByOwner("TRENT").isEmpty());
+ assertTrue(start.findTerminalByOwner(PLAYER_ID).isEmpty());
assertEquals(Inventory.INITIAL_ROUTE_COUNT, getPlayer().getInventory().getItemCount(InventoryItemType.Place_Route));
assertEquals(Inventory.INITIAL_AIRPLANE_COUNT, getPlayer().getInventory().getItemCount(InventoryItemType.Place_Airplane));
}
@@ -68,15 +71,15 @@ public void testLosesPathOnLoseOneOfTwoTerminalsRegardlessOfFlight(boolean fligh
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3, 4, 5})
public void testTakesoffPlaneOnAbandon(int order) {
- gameMap.startNewDay(2); // Get the terminals
+ gameMap.startNewDay(); // Get the terminals
PathService path = getPlayer().createPath(start, second);
getPlayer().editPath(path, second, third); // Draw the route
- gameMap.startNewDay(3); // Changes to pending
+ gameMap.startNewDay(); // Changes to pending
- Terminal startTerminal = start.findTerminalByOwner("TRENT").orElseThrow();
- Terminal secondTerminal = second.findTerminalByOwner("TRENT").orElseThrow();
- Terminal thirdTerminal = third.findTerminalByOwner("TRENT").orElseThrow();
+ Terminal startTerminal = start.findTerminalByOwner(PLAYER_ID).orElseThrow();
+ Terminal secondTerminal = second.findTerminalByOwner(PLAYER_ID).orElseThrow();
+ Terminal thirdTerminal = third.findTerminalByOwner(PLAYER_ID).orElseThrow();
if (order == 1) {
startTerminal.finishPending(false);
secondTerminal.finishPending(true);
@@ -99,6 +102,8 @@ public void testTakesoffPlaneOnAbandon(int order) {
secondTerminal.finishPending(true);
}
+ assertEquals(1, path.getRouteCount());
+ assertEquals(2, path.getPath().size());
assertNotNull(path.getFirstPlane());
assertTrue(getPlayer().getInventory().getItemCount(InventoryItemType.Place_Airplane) < Inventory.INITIAL_AIRPLANE_COUNT);
assertTrue(getPlayer().getPaths().contains(path));
@@ -113,7 +118,7 @@ public void testLostAirplaneTakesoffUntilValidTerminal() {
Airport fourth = new Airport(new Vector2(90, 10), Airport.AirportType.LOCAL, "JKL");
gameMap.addAirport(fourth);
fourth.requestTerminal(getPlayer().getId());
- gameMap.startNewDay(2);
+ gameMap.startNewDay();
PathService path = getPlayer().createPath(start, second);
getPlayer().editPath(path, second, third);
@@ -130,7 +135,7 @@ public void testLostAirplaneTakesoffUntilValidTerminal() {
path.getFirstPlane().addPassenger(passenger);
}
for (int i = 0; i < 10; i++) path.getFirstPlane().update(); // Complete airport1, airport2 quotas
- gameMap.startNewDay(3);
+ gameMap.startNewDay();
third.findTerminalByOwner(getPlayer().getId()).orElseThrow().finishPending(false);
fourth.findTerminalByOwner(getPlayer().getId()).orElseThrow().finishPending(false);
for (int i = 0; i < 10; i++) {
@@ -143,18 +148,18 @@ public void testLostAirplaneTakesoffUntilValidTerminal() {
@Test
public void testAutomatesGracesTerminalOnNextDay() {
- gameMap.startNewDay(2); // Receive terminals
+ gameMap.startNewDay(); // Receive terminals
getPlayer().createPath(start, second);
- gameMap.startNewDay(3); // Missed quota; pending
- gameMap.startNewDay(4); // Grace
+ gameMap.startNewDay(); // Missed quota; pending
+ gameMap.startNewDay(); // Grace
assertTrue(getPlayer().getPendingTerminals().isEmpty());
assertTrue(getPlayer().getPaths().isEmpty());
- gameMap.startNewDay(5);
+ gameMap.startNewDay();
}
@Test
public void testLosesRouteWithTerminalOnValidate() {
- gameMap.startNewDay(2);
+ gameMap.startNewDay();
PathService path = getPlayer().createPath(start, second);
getPlayer().editPath(path, second, third);
@@ -168,7 +173,7 @@ public void testLosesRouteWithTerminalOnValidate() {
for (int i = 0; i < 5; i++) path.getFirstPlane().update();
}
- gameMap.startNewDay(3);
+ gameMap.startNewDay();
start.findTerminalByOwner(getPlayer().getId()).orElseThrow().finishPending(false);
assertEquals(Arrays.asList(second, third), path.getPath());
}
@@ -178,9 +183,9 @@ public void testLosesRouteWithTerminalOnValidate() {
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3, 4 })
public void testReturnsWhenPathReturns_pathOf2(int order) {
- gameMap.startNewDay(2);
+ gameMap.startNewDay();
PathService path = getPlayer().createPath(start, second);
- gameMap.startNewDay(3);
+ gameMap.startNewDay();
if (order <= 2) {
start.findTerminalByOwner(getPlayer().getId()).orElseThrow().finishPending(order % 2 == 1);
@@ -212,7 +217,7 @@ public void test_currentDestinationChangesWhenInterrupted() {
createAndRequest(airport1, airport4);
// Day 1
- gameMap.startNewDay(1);
+ gameMap.startNewDay();
createAndRequest(airport2, airport3, airport5);
PathService path = getPlayer().createPath(airport1, airport4);
forceFlight(path.getFirstPlane(), airport4);
@@ -225,7 +230,7 @@ public void test_currentDestinationChangesWhenInterrupted() {
forceFlight(secondAirplane, airport1);
// Day 2
- gameMap.startNewDay(2);
+ gameMap.startNewDay();
getPlayer().editPath(path, airport4, airport5);
forceFlight(secondAirplane, airport4);
getPlayer().editPath(path, airport1, airport3);
@@ -239,13 +244,13 @@ public void test_currentDestinationChangesWhenInterrupted() {
getPlayer().editPath(path, airport1, airport2);
// Day 3
- gameMap.startNewDay(3);
+ gameMap.startNewDay();
airport2.findTerminalByOwner(getPlayer().getId()).orElseThrow().finishPending(true);
airport3.findTerminalByOwner(getPlayer().getId()).orElseThrow().finishPending(true);
airport1.findTerminalByOwner(getPlayer().getId()).orElseThrow().finishPending(true);
// Day 4
- gameMap.startNewDay(4);
+ gameMap.startNewDay();
forceFlight(thirdAirplane, airport4);
forceFlight(thirdAirplane, airport5);
forceFlight(thirdAirplane, airport4);
@@ -278,8 +283,12 @@ private void forceFlight(Airplane airplane, Airport destination) {
@AfterEach
public void cleanup() {
- gameMap.removePlayer(getPlayer().getId());
- gameMap.newPlayer("TRENT");
+ gameMap.removePlayerAndPossiblyEndGame(getPlayer());
+ }
+
+ @AfterAll
+ public static void finalCleanup() {
+ gameMap.dispose();
}
}
diff --git a/shared/src/test/java/io/streamlines/map/PlayerTest.java b/shared/src/test/java/io/streamlines/map/PlayerTest.java
new file mode 100644
index 00000000..a7a0846c
--- /dev/null
+++ b/shared/src/test/java/io/streamlines/map/PlayerTest.java
@@ -0,0 +1,222 @@
+package io.streamlines.map;
+
+import com.badlogic.gdx.math.Vector2;
+import io.streamlines.flight.Airplane;
+import io.streamlines.flight.Airport;
+import io.streamlines.flight.Passenger;
+import io.streamlines.flight.PathService;
+import io.streamlines.inventory.Inventory;
+import io.streamlines.inventory.InventoryItemType;
+import io.streamlines.inventory.PlaceAirplaneCommand;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+public class PlayerTest {
+ private final static String PLAYER_ID = "ABC";
+ private final static String SECOND_PLAYER_ID = "DEF";
+
+ @Test
+ public void testPlayerWith2AirportPath_removePath_notHavePath() {
+ // Arrange
+ GameMap map = mock();
+ PathService pathService = new PathService(PathService.generateNewId());
+ pathService.editRoute(mock(), mock());
+ Player player = new Player(map, PLAYER_ID);
+ when(map.getPlayer(PLAYER_ID)).thenReturn(player);
+ player.getPaths().add(pathService);
+
+ // Act
+ player.removePath(pathService);
+
+ // Assert
+ assertFalse(player.getPaths().contains(pathService));
+ }
+
+ @Test
+ public void testPlayerWithInfrastructureOnCyclicalPath_removePath_notHavePath() {
+ // Arrange
+ GameMap map = mock();
+ when(map.getGraph()).thenReturn(mock());
+ PathService pathService = new PathService(PathService.generateNewId());
+ pathService.getPath().addAll(List.of(mock(), mock(), mock()));
+ pathService.editRoute(pathService.getPath().get(2), pathService.getPath().get(0));
+ pathService.getPlanes().addAll(Set.of(mock(), mock()));
+ Player player = new Player(map, PLAYER_ID);
+ when(map.getPlayer(PLAYER_ID)).thenReturn(player);
+ player.getPaths().add(pathService);
+
+ // Act
+ player.removePath(pathService);
+
+ // Assert
+ assertFalse(player.getPaths().contains(pathService));
+ }
+
+ @Test
+ public void testPlayerWithInfrastructureOnLinearPath_notHavePath() {
+ // Arrange
+ GameMap map = mock();
+ when(map.getGraph()).thenReturn(mock());
+ PathService pathService = new PathService(PathService.generateNewId());
+ pathService.getPath().addAll(List.of(mock(), mock(), mock()));
+ pathService.getPlanes().addAll(Set.of(mock(), mock()));
+ Player player = new Player(map, PLAYER_ID);
+ when(map.getPlayer(PLAYER_ID)).thenReturn(player);
+ player.getPaths().add(pathService);
+
+ // Act
+ player.removePath(pathService);
+
+ // Assert
+ assertFalse(player.getPaths().contains(pathService));
+ }
+
+ @Test
+ public void testOtherPlayer_removePath_fails_pathExistsAndInfrastructureStaysOnMap() {
+ // Arrange
+ GameMap map = mock();
+ Player player = new Player(map, PLAYER_ID);
+ when(map.getPlayer(PLAYER_ID)).thenReturn(player);
+ Airplane airplane = mock();
+ when(airplane.getOwner()).thenReturn(PLAYER_ID);
+ PathService pathService = new PathService(PathService.generateNewId());
+ pathService.addPlane(airplane);
+ List airportList = List.of(mock(), mock(), mock());
+ pathService.getPath().addAll(airportList);
+ player.getPaths().add(pathService);
+
+ Player maliciousPlayer = new Player(map, SECOND_PLAYER_ID);
+
+ // Act & Assert
+ assertThrows(IllegalArgumentException.class, () -> maliciousPlayer.removePath(pathService));
+ assertEquals(airportList, pathService.getPath());
+ assertArrayEquals(Set.of(airplane).toArray(), pathService.getPlanes().toArray());
+ assertTrue(player.getPaths().contains(pathService));
+ }
+
+ @Test
+ @Tag("integ")
+ public void testPlayer_removePath_returnsInfrastructureToInventory() {
+ GameMap map = new GameMap();
+ Player player = map.newPlayer(PLAYER_ID);
+ List airportList = List.of(
+ new Airport(Vector2.X, Airport.AirportType.LOCAL, "CYZ"),
+ new Airport(Vector2.Y, Airport.AirportType.HOTSPOT, "ABC"),
+ new Airport(Vector2.Zero, Airport.AirportType.LOCAL, "DEF")
+ );
+ for (Airport airport : airportList) {
+ map.addAirport(airport);
+ airport.requestTerminal(PLAYER_ID);
+ }
+ map.startNewDay(map.nextDay());
+
+ PathService path = player.createPath(airportList.get(0), airportList.get(1));
+ player.editPath(path, airportList.get(1), airportList.get(2));
+ player.editPath(path, airportList.get(2), airportList.get(0));
+
+ for (int i = 1; i < Inventory.INITIAL_AIRPLANE_COUNT; i++) {
+ player.getInventory().useItem(new PlaceAirplaneCommand(mock(), path));
+ }
+ player.removePath(path);
+
+ // Act & Assert
+ assertTrue(player.getInventory().getItemCount(InventoryItemType.Place_Route) >= 3);
+ assertTrue(player.getInventory().getItemCount(InventoryItemType.Place_Airplane) >= 3);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testPlayer_manyAirports_removeAirportFromPath_hasRestOfPath(boolean cyclical) {
+ // Arrange
+ GameMap map = mock();
+ when(map.getGraph()).thenReturn(mock());
+ PathService pathService = new PathService(PathService.generateNewId());
+ pathService.getPath().addAll(List.of(mock(), mock(), mock()));
+ if (cyclical) {
+ pathService.editRoute(pathService.getPath().get(2), pathService.getPath().get(0));
+ }
+ pathService.getPlanes().addAll(Set.of(mock(), mock()));
+ Player player = new Player(map, PLAYER_ID);
+ when(map.getPlayer(PLAYER_ID)).thenReturn(player);
+ player.getPaths().add(pathService);
+
+ // Act
+ player.removeAirportFromPathAndPossiblyRemovePath(pathService, pathService.getPath().get(0));
+
+ // Assert
+ assertTrue(player.getPaths().contains(pathService));
+ assertEquals(1, pathService.getRouteCount());
+ assertEquals(2, pathService.getPath().size());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { 0, 1 })
+ public void testPlayer_twoAirports_removeAirportFromPath_noPath(int index) {
+ // Arrange
+ GameMap map = mock();
+ when(map.getGraph()).thenReturn(mock());
+ PathService pathService = new PathService(PathService.generateNewId());
+ pathService.getPath().addAll(List.of(mock(), mock()));
+ pathService.getPlanes().addAll(Set.of(mock(), mock()));
+ Player player = new Player(map, PLAYER_ID);
+ when(map.getPlayer(PLAYER_ID)).thenReturn(player);
+ player.getPaths().add(pathService);
+
+ // Act
+ player.removeAirportFromPathAndPossiblyRemovePath(pathService, pathService.getPath().get(index));
+
+ // Assert
+ assertFalse(player.getPaths().contains(pathService));
+ }
+
+ @Test
+ @Tag("integ")
+ public void testPlayer_afterPlaneMovement_removeAirportFromPath_initialProvidedInventory() {
+ // Arrange
+ GameMap map = new GameMap();
+ Player player = map.newPlayer(PLAYER_ID);
+ Airport[] airports = new Airport[] {
+ new Airport(Vector2.X, Airport.AirportType.LOCAL, "CYZ"),
+ new Airport(Vector2.X.add(Vector2.X), Airport.AirportType.HOTSPOT, "ABC"),
+ new Airport(Vector2.X.add(Vector2.Y), Airport.AirportType.LOCAL, "DEF"),
+ new Airport(Vector2.Y.add(Vector2.Y), Airport.AirportType.HOTSPOT, "GHI")
+ };
+ for (Airport airport : airports) {
+ map.addAirport(airport);
+ airport.requestTerminal(PLAYER_ID);
+ }
+ map.startNewDay();
+ PathService pathService = player.createPath(airports[0], airports[1]);
+ player.editPath(pathService, airports[1], airports[2]);
+ player.editPath(pathService, airports[2], airports[3]);
+
+ for (int i = 0; i < Airplane.INITIAL_CAPACITY; i++) {
+ pathService.getFirstPlane().addPassenger(new Passenger(airports[1]));
+ }
+
+ for (int i = 0; i < 3; i++) {
+ pathService.getFirstPlane().update();
+ }
+
+ // Act
+ player.removeAirportFromPathAndPossiblyRemovePath(pathService, pathService.getPath().get(1));
+
+ int expectedUsedRoutes = 2;
+ int expectedUsedPlanes = 1;
+
+ // Assert
+ assertTrue(player.getPaths().contains(pathService));
+ assertEquals(Inventory.INITIAL_ROUTE_COUNT - expectedUsedRoutes,
+ player.getInventory().getItemCount(InventoryItemType.Place_Route));
+ assertEquals(Inventory.INITIAL_AIRPLANE_COUNT - expectedUsedPlanes,
+ player.getInventory().getItemCount(InventoryItemType.Place_Airplane));
+ }
+}
diff --git a/shared/src/test/java/io/streamlines/network/PeerListenerTest.java b/shared/src/test/java/io/streamlines/network/PeerListenerTest.java
deleted file mode 100644
index a645692c..00000000
--- a/shared/src/test/java/io/streamlines/network/PeerListenerTest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package io.streamlines.network;
-
-import io.streamlines.map.GameMap;
-import io.streamlines.map.Player;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class PeerListenerTest {
- private GameMap gameMap;
- private PeerListener peerListener;
- private Player player;
-
- private final static String PLAYER_ID = "abc";
-
- @BeforeEach
- public void initialPlayerGameSetup() {
- gameMap = new GameMap();
- peerListener = new PeerListener(gameMap);
- player = gameMap.newPlayer(PLAYER_ID);
- }
- @Test
- public void testHandlePlayerLoss_removesAllPlayerResources() {
- peerListener.handlePlayerLoss(new PeerListener.LosePlayer(PLAYER_ID));
-
- assertEquals(0, gameMap.getAllPlayers().size());
- }
-}
diff --git a/shared/src/test/java/io/streamlines/network/PeerMessageTest.java b/shared/src/test/java/io/streamlines/network/PeerMessageTest.java
new file mode 100644
index 00000000..2d31a83d
--- /dev/null
+++ b/shared/src/test/java/io/streamlines/network/PeerMessageTest.java
@@ -0,0 +1,99 @@
+package io.streamlines.network;
+
+import com.badlogic.gdx.math.Vector2;
+import io.streamlines.flight.Airport;
+import io.streamlines.map.GameMap;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Integration tests on data transfer objects when messages are sent over websockets.
+ * The concrete GameMap, Airport, and Terminal objects are used (they are NOT mocked).
+ */
+@Tag("integ")
+public class PeerMessageTest {
+ private GameMap gameMap;
+
+ private final static String PLAYER_ID = "abc";
+ private final static String SECOND_PLAYER_ID = "xyz";
+
+ @BeforeEach
+ public void initialPlayerGameSetup() {
+ gameMap = new GameMap();
+ gameMap.newPlayer(PLAYER_ID);
+ }
+
+ @Test
+ public void testLosePlayer_apply_removesAllPlayerResources() {
+ new LosePlayerDto(PLAYER_ID).apply(gameMap, false);
+
+ assertEquals(0, gameMap.getAllPlayers().size());
+ }
+
+ @Test
+ public void testExpandedAirportList_apply_addsAirports() {
+ var originalAirport = new Airport(Vector2.Y, Airport.AirportType.HOTSPOT, "UWR");
+ gameMap.addAirport(originalAirport);
+
+ var newAirport1 = new Airport(Vector2.Zero, Airport.AirportType.LOCAL, "CYZ");
+ var newAirport2 = new Airport(Vector2.X, Airport.AirportType.LOCAL, "ALB");
+ new AirportListDto(new Airport[] { newAirport1, newAirport2 }).apply(gameMap, true);
+
+ var expectedAirportList = new Airport[] { originalAirport, newAirport1, newAirport2 };
+ Arrays.sort(expectedAirportList);
+ assertArrayEquals(expectedAirportList, gameMap.getAirports().stream().sorted().toArray());
+ }
+
+ @Test
+ public void testAirportList_ofModifiedAirports_apply_modifiesAirports() {
+ var originalAirport = new Airport(Vector2.Y, Airport.AirportType.HOTSPOT, "UWR");
+ gameMap.addAirport(originalAirport);
+
+ originalAirport.requestTerminal(PLAYER_ID);
+ originalAirport.requestTerminal(SECOND_PLAYER_ID);
+ new AirportListDto(new Airport[]{ originalAirport }).apply(gameMap, true);
+
+ var airports = gameMap.getAirports().toArray(Airport[]::new);
+ assertArrayEquals(new Airport[] { originalAirport }, airports);
+ assertTrue(airports[0].findTerminalByOwner(PLAYER_ID).isPresent());
+ assertTrue(airports[0].findTerminalByOwner(SECOND_PLAYER_ID).isPresent());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ public void testNewDay_apply_startsNewDay_irrespectiveOfPeerType(boolean isClient) {
+ GameMap mockedGameMap = mock();
+ new NewDayDto(1, 0).apply(mockedGameMap, isClient);
+ verify(mockedGameMap).startNewDay(1);
+ }
+
+ @Test
+ public void testLanding_applyClient_addsThroughput() {
+ var airportId = "CYZ";
+ Airport originalAirport = new Airport(Vector2.Y, Airport.AirportType.HOTSPOT, airportId);
+ gameMap.addAirport(originalAirport);
+ originalAirport.requestTerminal(PLAYER_ID);
+ originalAirport.lockInTerminals();
+ int originalThroughput = originalAirport.findTerminalByOwner(PLAYER_ID).get().getThroughput();
+ new LandingDto(100, airportId, originalAirport.getId(), PLAYER_ID).apply(gameMap, true);
+ assertEquals(originalThroughput + 100, originalAirport.findTerminalByOwner(PLAYER_ID).get().getThroughput());
+ }
+
+ @Test
+ public void testLanding_applyServer_doesNotAddThroughput() {
+ var airportId = "CYZ";
+ Airport originalAirport = new Airport(Vector2.Y, Airport.AirportType.HOTSPOT, airportId);
+ gameMap.addAirport(originalAirport);
+ originalAirport.requestTerminal(PLAYER_ID);
+ originalAirport.lockInTerminals();
+ int originalThroughput = originalAirport.findTerminalByOwner(PLAYER_ID).get().getThroughput();
+ new LandingDto(100, airportId, originalAirport.getId(), PLAYER_ID).apply(gameMap, false);
+ assertEquals(originalThroughput, originalAirport.findTerminalByOwner(PLAYER_ID).get().getThroughput());
+ }
+}