From 33c027f847c7378373630f362c1f10af56d1aac7 Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Sat, 26 Jul 2025 23:19:40 -0400 Subject: [PATCH 01/46] Use dto for weather --- .../java/io/streamlines/network/Client.java | 10 ++++---- .../main/java/io/streamlines/map/Weather.java | 23 ++++++------------- shared/build.gradle | 2 +- .../java/io/streamlines/network/Packet.java | 1 + .../io/streamlines/network/PeerListener.java | 23 +++++++++++++------ .../io/streamlines/network/WeatherDto.java | 5 ++++ 6 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 shared/src/main/java/io/streamlines/network/WeatherDto.java diff --git a/core/src/main/java/io/streamlines/network/Client.java b/core/src/main/java/io/streamlines/network/Client.java index 14297fbe..1eaa9ca8 100644 --- a/core/src/main/java/io/streamlines/network/Client.java +++ b/core/src/main/java/io/streamlines/network/Client.java @@ -103,6 +103,7 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { Packet parseablePacket = new Packet(packet); String packetData = parseablePacket.getData(); peerListener.readPacket(parseablePacket); + Gdx.app.log("WS", "Got message: " + parseablePacket.getContents()); final JsonValue pathParameters = parseablePacket.parse(); @@ -206,11 +207,12 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { peerListener.removeAirportFromPath(); break; case WEATHER_EVENT: - if (pathParameters.getInt("status") > WeatherEventSeverity.FINE.ordinal()) { - LogMessage.setMessage("A level " + pathParameters.getInt("status") + " weather event is " + - "underway at " + pathParameters.getString("airport")); + WeatherDto parameters = peerListener.parseData(WeatherDto.class, packet); + if (parameters.status().ordinal() > WeatherEventSeverity.FINE.ordinal()) { + LogMessage.setMessage("A level " + parameters.status().ordinal() + " weather event is " + + "underway at " + parameters.airportId()); } - peerListener.handleWeatherEvent(); + peerListener.handleWeatherEvent(parameters); break; case RETURN_PASSENGER_COUNT: String planeId = pathParameters.getString("plane_id"); diff --git a/server/src/main/java/io/streamlines/map/Weather.java b/server/src/main/java/io/streamlines/map/Weather.java index 807ad884..ba4fce16 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.*; @@ -53,27 +51,20 @@ public void run() { Airport.AirportType.LOCAL : Airport.AirportType.HOTSPOT; - // Find a random airport of required type. Includes airports already in a weather event. + // Includes airports already in a weather event. Airport a = gameMap.getAirports().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()); + peerListener.handleWeatherEvent(data); + broadcaster.broadcast(Packet.PacketType.WEATHER_EVENT, peerListener.serializeData(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()); + peerListener.handleWeatherEvent(data); + broadcaster.broadcast(Packet.PacketType.WEATHER_EVENT, peerListener.serializeData(data)); } }; diff --git a/shared/build.gradle b/shared/build.gradle index 56832307..93a80dc0 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -5,7 +5,7 @@ repositories { dependencies { api "com.badlogicgames.gdx:gdx:$gdxVersion" testImplementation platform('org.junit:junit-bom:5.9.1') // JUnit 5 - testImplementation 'org.mockito:mockito-core:5+' + testImplementation libs.mockito testImplementation 'org.junit.jupiter:junit-jupiter' // aggregate jqwik dependency testImplementation "net.jqwik:jqwik:${jqwikVersion}" diff --git a/shared/src/main/java/io/streamlines/network/Packet.java b/shared/src/main/java/io/streamlines/network/Packet.java index bb16099b..5ad54b54 100644 --- a/shared/src/main/java/io/streamlines/network/Packet.java +++ b/shared/src/main/java/io/streamlines/network/Packet.java @@ -9,6 +9,7 @@ * Every broadcast can only have ONE packet type. Parameters use param=value format but SHOULD NOT be included in * PacketType enumeration. But there can be name overlap (for example a parameter called player and a packet type * called player). + * @apiNote Must be used with the corresponding DTO */ public final class Packet { private static final JsonReader jsonReader; diff --git a/shared/src/main/java/io/streamlines/network/PeerListener.java b/shared/src/main/java/io/streamlines/network/PeerListener.java index acbb104a..fb74e651 100644 --- a/shared/src/main/java/io/streamlines/network/PeerListener.java +++ b/shared/src/main/java/io/streamlines/network/PeerListener.java @@ -1,8 +1,7 @@ package io.streamlines.network; import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.utils.JsonValue; -import com.badlogic.gdx.utils.TimeUtils; +import com.badlogic.gdx.utils.*; import io.streamlines.StreamlinesLogger; import io.streamlines.flight.*; import io.streamlines.inventory.*; @@ -17,9 +16,11 @@ public class PeerListener { private final GameMap gameMap; private JsonValue params; + private final Json json; public PeerListener(GameMap map) { gameMap = map; + json = new Json(JsonWriter.OutputType.javascript); } /** @@ -30,6 +31,15 @@ JsonValue readPacket(Packet packet) { return params = packet.parse(); } + T parseData(Class type, String data) { + return json.fromJson(type, data); + } + + public String serializeData(Object obj) { + return json.toJson(obj); + } + + @Deprecated public void writePacket(JsonValue packet) { params = packet; } @@ -219,13 +229,12 @@ public boolean handleLanding(boolean servile) { 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()) { + public void handleWeatherEvent(WeatherDto dto) { + Airport affectedAirport = gameMap.getAirportById(dto.airportId()); + if (dto.status() == WeatherEventSeverity.FINE) { affectedAirport.clearWeatherEvent(); } else { - affectedAirport.handleWeatherEvent(status); + affectedAirport.handleWeatherEvent(dto.status()); } } 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..a0f91fe3 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/WeatherDto.java @@ -0,0 +1,5 @@ +package io.streamlines.network; + +import io.streamlines.map.WeatherEventSeverity; + +public record WeatherDto(WeatherEventSeverity status, String airportId) { } From 6ba767bf737095cc71abefef57b674bf26bb29b6 Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Sun, 3 Aug 2025 17:08:10 -0400 Subject: [PATCH 02/46] #218 Remove peerListener dependency by Spawner, add more DTOs --- .../java/io/streamlines/network/Client.java | 14 +++-- .../main/java/io/streamlines/map/Spawner.java | 22 ++----- .../main/java/io/streamlines/map/Weather.java | 10 ++-- .../streamlines/network/ServerLauncher.java | 7 ++- .../java/io/streamlines/map/PlayerTest.java | 7 ++- .../io/streamlines/network/Broadcastable.java | 2 + .../io/streamlines/network/DtoActionable.java | 15 +++++ .../io/streamlines/network/LandingDto.java | 39 ++++++++++++ .../io/streamlines/network/LosePlayerDto.java | 14 +++++ .../io/streamlines/network/NewDayDto.java | 15 +++++ .../java/io/streamlines/network/Packet.java | 17 +++++- .../io/streamlines/network/PeerListener.java | 59 +------------------ .../io/streamlines/network/WeatherDto.java | 15 ++++- .../streamlines/network/PeerListenerTest.java | 7 +-- 14 files changed, 147 insertions(+), 96 deletions(-) create mode 100644 shared/src/main/java/io/streamlines/network/DtoActionable.java create mode 100644 shared/src/main/java/io/streamlines/network/LandingDto.java create mode 100644 shared/src/main/java/io/streamlines/network/LosePlayerDto.java create mode 100644 shared/src/main/java/io/streamlines/network/NewDayDto.java diff --git a/core/src/main/java/io/streamlines/network/Client.java b/core/src/main/java/io/streamlines/network/Client.java index 1eaa9ca8..b61d3f23 100644 --- a/core/src/main/java/io/streamlines/network/Client.java +++ b/core/src/main/java/io/streamlines/network/Client.java @@ -125,7 +125,7 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { break; case DAY_DATA: gameLoop.disablePaths(); - peerListener.handleNewDay(); + peerListener.parseData(NewDayDto.class, packet).apply(gameMap, true); gameLoop.reenablePaths(); LogMessage.setMessage("A new day has begun!"); @@ -164,7 +164,8 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { gameLoop.nextStage(); break; case UNLOAD: - peerListener.handleLanding(true); + LandingDto landingParameters = peerListener.parseData(LandingDto.class, packet); + landingParameters.apply(gameMap, true); break; case SURPASSED: if (pathParameters.getString("action").equals("prompt") && @@ -212,7 +213,7 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { LogMessage.setMessage("A level " + parameters.status().ordinal() + " weather event is " + "underway at " + parameters.airportId()); } - peerListener.handleWeatherEvent(parameters); + parameters.apply(gameMap, true); break; case RETURN_PASSENGER_COUNT: String planeId = pathParameters.getString("plane_id"); @@ -239,7 +240,7 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { break; case UNASSIGNED: LogMessage.setMessage(pathParameters.getString("player") + " has left the game!"); - peerListener.handlePlayerLoss(new PeerListener.LosePlayer(pathParameters.getString("player"))); + new LosePlayerDto(pathParameters.getString("player")).apply(gameMap, true); if(gameMap.hasEnded()) { Gdx.app.postRunnable(() -> gameLoop.nextStage()); } @@ -265,6 +266,11 @@ public void broadcast(Packet.PacketType type, String data) { socket.send(new Packet(type, data).getContents()); } + @Override + public void broadcast(Packet.PacketType packetType, DtoActionable data) { + socket.send(new Packet(packetType, data)); + } + @Override public void broadcast(String data) { socket.send(data); diff --git a/server/src/main/java/io/streamlines/map/Spawner.java b/server/src/main/java/io/streamlines/map/Spawner.java index 3d223b39..097dd57c 100644 --- a/server/src/main/java/io/streamlines/map/Spawner.java +++ b/server/src/main/java/io/streamlines/map/Spawner.java @@ -22,7 +22,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,7 +30,6 @@ 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(), @@ -71,12 +69,9 @@ 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(); + var dayData = new NewDayDto(newDay, 0); + broadcaster.broadcast(Packet.PacketType.DAY_DATA, dayData); + dayData.apply(Spawner.this, false); JsonValue data = new JsonValue(JsonValue.ValueType.object); data.addChild("action", new JsonValue("identify")); @@ -173,15 +168,10 @@ public void notifyTakeoff(Airplane airplane) { @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); diff --git a/server/src/main/java/io/streamlines/map/Weather.java b/server/src/main/java/io/streamlines/map/Weather.java index ba4fce16..375503b0 100644 --- a/server/src/main/java/io/streamlines/map/Weather.java +++ b/server/src/main/java/io/streamlines/map/Weather.java @@ -20,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. @@ -31,7 +30,6 @@ public Weather(Spawner map, Broadcastable broadcastable, Timer tim) { rand = new Random(); timer = tim; broadcaster = broadcastable; - peerListener = new PeerListener(map); } /** @@ -56,15 +54,15 @@ public void run() { var severity = rand.nextBoolean() ? WeatherEventSeverity.INCONVENIENCE : WeatherEventSeverity.DISASTER; WeatherDto data = new WeatherDto(severity, a.getId()); - peerListener.handleWeatherEvent(data); - broadcaster.broadcast(Packet.PacketType.WEATHER_EVENT, peerListener.serializeData(data)); + data.apply(gameMap, false); + broadcaster.broadcast(Packet.PacketType.WEATHER_EVENT, data); TimerTask t = new TimerTask() { @Override public void run() { var data = new WeatherDto(a.getWeatherStatus(), a.getId()); - peerListener.handleWeatherEvent(data); - broadcaster.broadcast(Packet.PacketType.WEATHER_EVENT, peerListener.serializeData(data)); + 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..478d7c5f 100644 --- a/server/src/main/java/io/streamlines/network/ServerLauncher.java +++ b/server/src/main/java/io/streamlines/network/ServerLauncher.java @@ -105,6 +105,11 @@ public void broadcast(Packet.PacketType type, String data) { broadcast(new Packet(type, data).getContents()); } + @Override + public void broadcast(Packet.PacketType packetType, DtoActionable data) { + broadcast(new Packet(packetType, data).getContents()); + } + /** * Send a packet to a specific client * @param type The type of packet @@ -121,7 +126,7 @@ public void onClose(WebSocket conn, int code, String reason, boolean remote) { 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"))); + new LosePlayerDto(data.getString("player")).apply(gameMap, false); pidTracker.deallocatePID(conn.getAttachment()); logger.info("SERVER", "{} has left the game -- {} ({})", conn.getAttachment(), reason, code); diff --git a/server/src/test/java/io/streamlines/map/PlayerTest.java b/server/src/test/java/io/streamlines/map/PlayerTest.java index 6a0ece42..d1e8487c 100644 --- a/server/src/test/java/io/streamlines/map/PlayerTest.java +++ b/server/src/test/java/io/streamlines/map/PlayerTest.java @@ -3,8 +3,7 @@ import com.badlogic.gdx.math.Vector2; import io.streamlines.flight.Airport; import io.streamlines.flight.PathService; -import io.streamlines.network.Broadcastable; -import io.streamlines.network.Packet; +import io.streamlines.network.*; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -40,6 +39,10 @@ public void broadcast(String data) {} @Override public void broadcast(Packet.PacketType packetType, String data) {} + + @Override + public void broadcast(Packet.PacketType packetType, DtoActionable data) { + } }); Player player = gameMap.newPlayer(PLAYER_ID); assertEquals(player, player.parse(player.serialize())); diff --git a/shared/src/main/java/io/streamlines/network/Broadcastable.java b/shared/src/main/java/io/streamlines/network/Broadcastable.java index a690b895..08bc57bf 100644 --- a/shared/src/main/java/io/streamlines/network/Broadcastable.java +++ b/shared/src/main/java/io/streamlines/network/Broadcastable.java @@ -21,4 +21,6 @@ public interface Broadcastable { * @param data The JSON data */ void broadcast(Packet.PacketType packetType, String data); + + void broadcast(Packet.PacketType packetType, DtoActionable 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..0e110891 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/DtoActionable.java @@ -0,0 +1,15 @@ +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 servile); +} 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..7f661a97 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/LandingDto.java @@ -0,0 +1,39 @@ +package io.streamlines.network; + +import io.streamlines.StreamlinesLogger; +import io.streamlines.flight.Airport; +import io.streamlines.flight.Terminal; +import io.streamlines.map.GameMap; + +/** + * Handle plane landing + */ +public record LandingDto(int throughput, String airportId, String airplaneId, String playerId) + implements DtoActionable { + /** + * @param gameMap The current state of the game map + * @param servile 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 servile) { + 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 (servile) { + 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; + } +} + 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..4caf8f0c --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/LosePlayerDto.java @@ -0,0 +1,14 @@ +package io.streamlines.network; + +import io.streamlines.map.GameMap; + +public record LosePlayerDto(String playerId) implements DtoActionable { + @Override + public Boolean apply(GameMap gameMap, Boolean servile) { + gameMap.removePlayer(playerId); + if(gameMap.shouldEnd()) { + gameMap.end(); + } + return true; + } +} 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..29236ac9 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/NewDayDto.java @@ -0,0 +1,15 @@ +package io.streamlines.network; + +import com.badlogic.gdx.utils.TimeUtils; +import io.streamlines.StreamlinesLogger; +import io.streamlines.map.GameMap; + +public record NewDayDto(int dayNumber, int time) implements DtoActionable { + @Override + public Boolean apply(GameMap gameMap, Boolean servile) { + gameMap.startNewDay(dayNumber); + GameMap.timeAtDayStart = (TimeUtils.millis() / 1000) - time; + StreamlinesLogger.logger.info("Time", "Day " + dayNumber + " has begun"); + return false; + } +} diff --git a/shared/src/main/java/io/streamlines/network/Packet.java b/shared/src/main/java/io/streamlines/network/Packet.java index 5ad54b54..ffe7483c 100644 --- a/shared/src/main/java/io/streamlines/network/Packet.java +++ b/shared/src/main/java/io/streamlines/network/Packet.java @@ -1,7 +1,6 @@ package io.streamlines.network; -import com.badlogic.gdx.utils.JsonReader; -import com.badlogic.gdx.utils.JsonValue; +import com.badlogic.gdx.utils.*; /** * Packets are sent with the following format: @@ -13,9 +12,11 @@ */ public final class Packet { private static final JsonReader jsonReader; + private static final Json json; static { jsonReader = new JsonReader(); + json = new Json(JsonWriter.OutputType.javascript); } public enum PacketType { @@ -70,7 +71,10 @@ public PacketType getPacketType() { return null; } - private final String contents; + private transient final String contents; + + private PacketType packetType; + private DtoActionable dto; /** * Creates packet for receiving serialized contents, so it can get parsed. @@ -86,9 +90,16 @@ public Packet(String rawContents) { * @param contents String representation of data. */ public Packet(PacketType type, String contents) { + packetType = type; this.contents = type.callsign + "=" + contents; } + public Packet(PacketType type, DtoActionable data) { + packetType = type; + dto = data; + this.contents = type.callsign + "=" + json.toJson(data); + } + public String getData() { if (getPacketType() == null) return "NULL_PACKET"; diff --git a/shared/src/main/java/io/streamlines/network/PeerListener.java b/shared/src/main/java/io/streamlines/network/PeerListener.java index fb74e651..ca0183ab 100644 --- a/shared/src/main/java/io/streamlines/network/PeerListener.java +++ b/shared/src/main/java/io/streamlines/network/PeerListener.java @@ -5,7 +5,8 @@ import io.streamlines.StreamlinesLogger; import io.streamlines.flight.*; import io.streamlines.inventory.*; -import io.streamlines.map.*; +import io.streamlines.map.GameMap; +import io.streamlines.map.Player; import java.util.Optional; @@ -35,10 +36,6 @@ T parseData(Class type, String data) { return json.fromJson(type, data); } - public String serializeData(Object obj) { - return json.toJson(obj); - } - @Deprecated public void writePacket(JsonValue packet) { params = packet; @@ -48,13 +45,6 @@ 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 */ @@ -202,49 +192,4 @@ public boolean addNewInventoryItems() { } 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(WeatherDto dto) { - Airport affectedAirport = gameMap.getAirportById(dto.airportId()); - if (dto.status() == WeatherEventSeverity.FINE) { - affectedAirport.clearWeatherEvent(); - } else { - affectedAirport.handleWeatherEvent(dto.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/WeatherDto.java b/shared/src/main/java/io/streamlines/network/WeatherDto.java index a0f91fe3..4b1f7b03 100644 --- a/shared/src/main/java/io/streamlines/network/WeatherDto.java +++ b/shared/src/main/java/io/streamlines/network/WeatherDto.java @@ -1,5 +1,18 @@ package io.streamlines.network; +import io.streamlines.flight.Airport; +import io.streamlines.map.GameMap; import io.streamlines.map.WeatherEventSeverity; -public record WeatherDto(WeatherEventSeverity status, String airportId) { } +public record WeatherDto(WeatherEventSeverity status, String airportId) implements DtoActionable { + @Override + public Boolean apply(GameMap gameMap, Boolean servile) { + Airport affectedAirport = gameMap.getAirportById(airportId); + if (status == WeatherEventSeverity.FINE) { + affectedAirport.clearWeatherEvent(); + } else { + affectedAirport.handleWeatherEvent(status); + } + return false; + } +} diff --git a/shared/src/test/java/io/streamlines/network/PeerListenerTest.java b/shared/src/test/java/io/streamlines/network/PeerListenerTest.java index a645692c..5866d76d 100644 --- a/shared/src/test/java/io/streamlines/network/PeerListenerTest.java +++ b/shared/src/test/java/io/streamlines/network/PeerListenerTest.java @@ -1,7 +1,6 @@ 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; @@ -9,20 +8,16 @@ 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)); + new LosePlayerDto(PLAYER_ID).apply(gameMap, false); assertEquals(0, gameMap.getAllPlayers().size()); } From b91275336850e5db0f8cae8a37546eaa4e9cc2db Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Mon, 4 Aug 2025 21:51:28 -0400 Subject: [PATCH 03/46] Add dtos for everything using write packet --- .../java/io/streamlines/RouteManager.java | 56 +++--- .../java/io/streamlines/UI/AirportUI.java | 13 +- .../java/io/streamlines/UI/DoubleQuotaUI.java | 18 +- .../java/io/streamlines/UI/MainGameUI.java | 80 +++----- .../java/io/streamlines/network/Client.java | 104 +++++----- .../main/java/io/streamlines/map/Spawner.java | 16 +- .../streamlines/network/ServerLauncher.java | 42 ++--- .../io/streamlines/flight/PathService.java | 6 +- .../network/AirplaneSystemAction.java | 5 + .../network/AirplaneSystemActionDto.java | 15 ++ .../AirplaneUserInventoryActionDto.java | 47 +++++ .../io/streamlines/network/DtoActionable.java | 2 +- .../io/streamlines/network/EditPathDto.java | 31 +++ .../network/GambleTerminalDto.java | 22 +++ .../io/streamlines/network/LandingDto.java | 6 +- .../io/streamlines/network/LosePlayerDto.java | 2 +- .../io/streamlines/network/NewBidDto.java | 22 +++ .../io/streamlines/network/NewDayDto.java | 2 +- .../io/streamlines/network/NewPathDto.java | 24 +++ .../java/io/streamlines/network/Packet.java | 58 ++---- .../io/streamlines/network/PeerListener.java | 178 +----------------- .../io/streamlines/network/RemovePathDto.java | 19 ++ .../io/streamlines/network/SetTopDogDto.java | 12 ++ .../network/SurpassedChoseDto.java | 14 ++ .../TerminalUserInventoryActionDto.java | 27 +++ .../network/TopDogPlaceAirplaneDto.java | 28 +++ .../network/TopDogPlaceRouteDto.java | 30 +++ .../io/streamlines/network/WeatherDto.java | 2 +- 28 files changed, 468 insertions(+), 413 deletions(-) create mode 100644 shared/src/main/java/io/streamlines/network/AirplaneSystemAction.java create mode 100644 shared/src/main/java/io/streamlines/network/AirplaneSystemActionDto.java create mode 100644 shared/src/main/java/io/streamlines/network/AirplaneUserInventoryActionDto.java create mode 100644 shared/src/main/java/io/streamlines/network/EditPathDto.java create mode 100644 shared/src/main/java/io/streamlines/network/GambleTerminalDto.java create mode 100644 shared/src/main/java/io/streamlines/network/NewBidDto.java create mode 100644 shared/src/main/java/io/streamlines/network/NewPathDto.java create mode 100644 shared/src/main/java/io/streamlines/network/RemovePathDto.java create mode 100644 shared/src/main/java/io/streamlines/network/SetTopDogDto.java create mode 100644 shared/src/main/java/io/streamlines/network/SurpassedChoseDto.java create mode 100644 shared/src/main/java/io/streamlines/network/TerminalUserInventoryActionDto.java create mode 100644 shared/src/main/java/io/streamlines/network/TopDogPlaceAirplaneDto.java create mode 100644 shared/src/main/java/io/streamlines/network/TopDogPlaceRouteDto.java diff --git a/core/src/main/java/io/streamlines/RouteManager.java b/core/src/main/java/io/streamlines/RouteManager.java index dd6750ed..c201d995 100644 --- a/core/src/main/java/io/streamlines/RouteManager.java +++ b/core/src/main/java/io/streamlines/RouteManager.java @@ -5,8 +5,6 @@ import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.utils.JsonValue; -import com.badlogic.gdx.utils.JsonWriter; import io.streamlines.UI.FullScreenWidget; import io.streamlines.UI.MainGameUI; import io.streamlines.flight.*; @@ -56,16 +54,17 @@ public void execute() { // Trusts client to know inventory TODO: Server correction PathService path = clientPlayer.createPath(routeBuilderStart, routeBuilderEnd); if (path != null) { - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("player", new JsonValue(clientPlayer.getId())); - jsonValue.addChild("path", new JsonValue(path.getId())); - jsonValue.addChild("1", new JsonValue(routeBuilderStart.getId())); - jsonValue.addChild("2", new JsonValue(routeBuilderEnd.getId())); if (path.getFirstPlane() != null) { - jsonValue.addChild("airplaneId", new JsonValue(path.getFirstPlane().getId())); + var dto = new NewPathDto(clientPlayer.getId(), path.getId(), routeBuilderStart.getId(), + routeBuilderEnd.getId(), path.getFirstPlane().getId()); + dto.apply(peerListener.getGameMap(), true); + broadcaster.broadcast(Packet.PacketType.NEW_PATH, dto); + } else { + var dto = new NewPathDto(clientPlayer.getId(), path.getId(), routeBuilderStart.getId(), + routeBuilderEnd.getId()); + dto.apply(peerListener.getGameMap(), true); + broadcaster.broadcast(Packet.PacketType.NEW_PATH, dto); } - - broadcaster.broadcast(Packet.PacketType.NEW_PATH, jsonValue.toJson(JsonWriter.OutputType.json)); } } }; @@ -94,14 +93,11 @@ public void prepare() { @Override public void execute() { if (selectedPath == null) return; - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("player", new JsonValue(clientPlayer.getId())); - jsonValue.addChild("path", new JsonValue(selectedPath.getId())); - jsonValue.addChild("1", new JsonValue(routeBuilderStart.getId())); - jsonValue.addChild("2", new JsonValue(routeBuilderEnd.getId())); - peerListener.writePacket(jsonValue); - peerListener.editPath(); - broadcaster.broadcast(Packet.PacketType.PATH_EDIT, jsonValue.toJson(JsonWriter.OutputType.json)); + var dto = new EditPathDto( + clientPlayer.getId(), selectedPath.getId(), routeBuilderStart.getId(), routeBuilderEnd.getId() + ); + dto.apply(peerListener.getGameMap(), true); + broadcaster.broadcast(Packet.PacketType.PATH_EDIT, dto); } }; private final RouteActionable DISABLED = new RouteActionable() { @@ -250,16 +246,10 @@ public void prepare() { } private void handleAirplanePlacement(PathService r) { - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("player_id", new JsonValue(clientPlayer.getId())); - jsonValue.addChild("id", new JsonValue(Airplane.generateNewId())); - jsonValue.addChild("positionX", new JsonValue(userCursor.x)); - jsonValue.addChild("positionY", new JsonValue(userCursor.y)); - jsonValue.addChild("path", new JsonValue(r.getId())); - jsonValue.addChild("action", new JsonValue("place")); - peerListener.writePacket(jsonValue); - peerListener.handleAirplaneData(); - broadcaster.broadcast(Packet.PacketType.AIRPLANE_DATA, jsonValue.toJson(JsonWriter.OutputType.json)); + var data = new AirplaneUserInventoryActionDto(clientPlayer.getId(), Airplane.generateNewId(), userCursor, r.getId(), + InventoryItemType.Place_Airplane); + broadcaster.broadcast(Packet.PacketType.AIRPLANE_USER_INVENTORY_ACTION, data); + data.apply(peerListener.getGameMap(), false); } public void run(GameMapAccessible map, MainGameUI mainGame, InventoryItemType selection) { @@ -268,14 +258,10 @@ public void run(GameMapAccessible map, MainGameUI mainGame, InventoryItemType se } if (Gdx.input.isKeyJustPressed(Input.Keys.D) && selectedPath != null) { map.getAirports().stream().filter(ScreenGraphicUtils::isHoveringOver).findFirst().ifPresent(a -> { - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("pathId", new JsonValue(selectedPath.getId())); - jsonValue.addChild("airportId", new JsonValue(a.getId())); - jsonValue.addChild("player", new JsonValue(clientPlayer.getId())); - peerListener.writePacket(jsonValue); - if (peerListener.removeAirportFromPath()) { + var dto = new RemovePathDto(selectedPath.getId(), a.getId(), clientPlayer.getId()); + if (dto.apply(peerListener.getGameMap(), true)) { mainGame.refresh(); - broadcaster.broadcast(Packet.PacketType.PATH_RMV, jsonValue.toJson(JsonWriter.OutputType.json)); + broadcaster.broadcast(Packet.PacketType.PATH_RMV, dto); selectedPath = null; } }); diff --git a/core/src/main/java/io/streamlines/UI/AirportUI.java b/core/src/main/java/io/streamlines/UI/AirportUI.java index e7bc1764..274e9a72 100644 --- a/core/src/main/java/io/streamlines/UI/AirportUI.java +++ b/core/src/main/java/io/streamlines/UI/AirportUI.java @@ -8,14 +8,12 @@ import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.utils.StringBuilder; -import com.badlogic.gdx.utils.*; import com.kotcrab.vis.ui.widget.*; import io.streamlines.Main; import io.streamlines.StreamlinesLogger; import io.streamlines.flight.Airport; import io.streamlines.flight.Terminal; -import io.streamlines.network.Client; -import io.streamlines.network.Packet; +import io.streamlines.network.*; public class AirportUI implements FullScreenWidget { private Airport airportToRepresent; @@ -33,15 +31,12 @@ public AirportUI() { private void bidOn(int term, Client gameClient) { Terminal terminal = airportToRepresent.getTerminals()[term]; StreamlinesLogger.logger.info("Bidding","Sending terminal bid for " + terminal.getId()); - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("terminal", new JsonValue(terminal.getId())); - jsonValue.addChild("owner", new JsonValue(gameClient.getPlayer().getId())); - gameClient.getPeerListener().writePacket(jsonValue); - if (!gameClient.getPeerListener().handleNewBid()) { + var dto = new NewBidDto(terminal.getId(), gameClient.getPlayer().getId()); + if (!dto.apply(gameClient.getPeerListener().getGameMap(), true)) { LogMessage.setMessage("Bid was unsuccessful"); return; } - gameClient.broadcast(Packet.PacketType.NEW_BID, jsonValue.toJson(JsonWriter.OutputType.json)); + gameClient.broadcast(Packet.PacketType.NEW_BID, dto); refresh(); } diff --git a/core/src/main/java/io/streamlines/UI/DoubleQuotaUI.java b/core/src/main/java/io/streamlines/UI/DoubleQuotaUI.java index 03178fcc..5f4057f3 100644 --- a/core/src/main/java/io/streamlines/UI/DoubleQuotaUI.java +++ b/core/src/main/java/io/streamlines/UI/DoubleQuotaUI.java @@ -5,21 +5,20 @@ import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.ui.Button; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; -import com.badlogic.gdx.utils.*; +import com.badlogic.gdx.utils.Align; import com.kotcrab.vis.ui.widget.VisLabel; import com.kotcrab.vis.ui.widget.VisTable; import io.streamlines.Main; import io.streamlines.StreamlinesLogger; import io.streamlines.inventory.*; -import io.streamlines.network.Client; -import io.streamlines.network.Packet; +import io.streamlines.network.*; import java.util.ArrayList; public class DoubleQuotaUI implements FullScreenWidget { private final UIStage stage; - private TextureRegion stageTexture; + private final TextureRegion stageTexture; /** * List of choices for items @@ -81,14 +80,9 @@ public void refresh() { public void clicked(InputEvent event, float x, float y) { StreamlinesLogger.logger.info("DOUBLE QUOTA UI", "Chosen Item: " + curItem + " (end of" + " double quota exchange)"); - playerInventory.addItem(curItem); - playerInventory.addItem(InventoryItemType.Place_Route); - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("action", new JsonValue("chose")); - jsonValue.addChild("player", new JsonValue(client.getPlayer().getId())); - jsonValue.addChild("item", new JsonValue(curItem.toString())); - client.broadcast(Packet.PacketType.SURPASSED, - jsonValue.toJson(JsonWriter.OutputType.javascript)); + var dto = new SurpassedChoseDto(client.getPlayer().getId(), curItem); + client.broadcast(Packet.PacketType.SURPASSED_CHOSE, dto); + dto.apply(client.getPeerListener().getGameMap(), true); close(); } }); diff --git a/core/src/main/java/io/streamlines/UI/MainGameUI.java b/core/src/main/java/io/streamlines/UI/MainGameUI.java index 02b28b79..bb2479af 100644 --- a/core/src/main/java/io/streamlines/UI/MainGameUI.java +++ b/core/src/main/java/io/streamlines/UI/MainGameUI.java @@ -16,15 +16,14 @@ import io.streamlines.flight.Terminal; import io.streamlines.inventory.*; import io.streamlines.map.*; -import io.streamlines.network.Client; -import io.streamlines.network.Packet; +import io.streamlines.network.*; import java.util.Collections; import java.util.*; public class MainGameUI implements FullScreenWidget { private final UIStage stage; - private TextureRegion stageTexture; + private final TextureRegion stageTexture; private boolean initial = true; private VisProgressBar progressBar; private VisLabel dayPercTxt; @@ -199,14 +198,9 @@ private StaticVisTable sidebar() { releaseButton.addListener(new ClickListener() { @Override public void clicked(InputEvent event, float x, float y) { - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("action", new JsonValue("gamble")); - jsonValue.addChild("id", new JsonValue(item.getAirportId())); - jsonValue.addChild("player_id", new JsonValue(item.getOwner())); - jsonValue.addChild("keep", new JsonValue(false)); - client.getPeerListener().writePacket(jsonValue); - client.getPeerListener().handleTerminalData(); - client.broadcast(Packet.PacketType.TERMINAL_DATA, jsonValue.toJson(JsonWriter.OutputType.json)); + var dto = new GambleTerminalDto(item.getOwner(), item.getAirportId(), false); + dto.apply(client.getPeerListener().getGameMap(), true); + client.broadcast(Packet.PacketType.GAMBLE_TERMINAL, dto); refresh(); } }); @@ -218,14 +212,9 @@ public void clicked(InputEvent event, float x, float y) { keepButton.addListener(new ClickListener() { @Override public void clicked(InputEvent event, float x, float y) { - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("action", new JsonValue("gamble")); - jsonValue.addChild("id", new JsonValue(item.getAirportId())); - jsonValue.addChild("player_id", new JsonValue(client.getPlayer().getId())); - jsonValue.addChild("keep", new JsonValue(true)); - client.getPeerListener().writePacket(jsonValue); - client.getPeerListener().handleTerminalData(); - client.broadcast(Packet.PacketType.TERMINAL_DATA, jsonValue.toJson(JsonWriter.OutputType.json)); + var dto = new GambleTerminalDto(client.getPlayer().getId(), item.getAirportId(), true); + dto.apply(client.getPeerListener().getGameMap(), true); + client.broadcast(Packet.PacketType.GAMBLE_TERMINAL, dto); refresh(); } }); @@ -317,43 +306,34 @@ public void notifySelection(Object data) { if (selectedInventoryItemType == InventoryItemType.Expand_Terminal) { if (!(data instanceof Optional)) return; if (!(((Optional) data).isPresent() && ((Optional) data).get() instanceof Terminal terminal)) return; - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("player_id", new JsonValue(terminal.getOwner())); - jsonValue.addChild("id", new JsonValue(terminal.getAirportId())); - jsonValue.addChild("action", new JsonValue("expand")); - client.getPeerListener().writePacket(jsonValue); - client.getPeerListener().handleTerminalData(); - client.broadcast(Packet.PacketType.TERMINAL_DATA, jsonValue.toJson(JsonWriter.OutputType.json)); + TerminalUserInventoryActionDto dto = new TerminalUserInventoryActionDto( + terminal.getOwner(), terminal.getAirportId(), InventoryItemType.Expand_Airplane + ); + dto.apply(client.getPeerListener().getGameMap(), true); + client.broadcast(Packet.PacketType.TERMINAL_USER_INVENTORY_ACTION, dto); } else if (selectedInventoryItemType == InventoryItemType.Expand_Airplane) { if (!(data instanceof Airplane planeObj)) return; - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("id", new JsonValue(planeObj.getId())); - jsonValue.addChild("player_id", new JsonValue(client.getPlayer().getId())); - jsonValue.addChild("action", new JsonValue("expand")); - client.getPeerListener().writePacket(jsonValue); - client.getPeerListener().handleAirplaneData(); - client.broadcast(Packet.PacketType.AIRPLANE_DATA, jsonValue.toJson(JsonWriter.OutputType.json)); + var dto = new AirplaneUserInventoryActionDto( + client.getPlayer().getId(), + planeObj.getId(), + planeObj.getLocation(), + "", + InventoryItemType.Expand_Airplane); + dto.apply(client.getPeerListener().getGameMap(), true); + client.broadcast(Packet.PacketType.AIRPLANE_USER_INVENTORY_ACTION, dto); } else if (selectedInventoryItemType == InventoryItemType.Resilience) { if (!(data instanceof Airplane planeObj)) return; - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("id", new JsonValue(planeObj.getId())); - jsonValue.addChild("player_id", new JsonValue(client.getPlayer().getId())); - jsonValue.addChild("action", new JsonValue("resilience")); - client.getPeerListener().writePacket(jsonValue); - client.getPeerListener().handleAirplaneData(); - client.broadcast(Packet.PacketType.AIRPLANE_DATA, jsonValue.toJson(JsonWriter.OutputType.json)); + var dto = new AirplaneUserInventoryActionDto( + client.getPlayer().getId(), planeObj.getId(), planeObj.getLocation(), "", InventoryItemType.Resilience + ); + dto.apply(client.getPeerListener().getGameMap(), true); + client.broadcast(Packet.PacketType.AIRPLANE_USER_INVENTORY_ACTION, dto); } else if (selectedInventoryItemType == InventoryItemType.Top_Dog) { if (!(data instanceof PlaceAirplaneCommand placeAirplaneCommand)) return; - - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - jsonValue.addChild("infrastructureType", new JsonValue("plane")); - jsonValue.addChild("action", new JsonValue("use")); - jsonValue.addChild("airplane", new JsonValue(placeAirplaneCommand.getPlane().getId())); - jsonValue.addChild("path", new JsonValue(placeAirplaneCommand.getPath().getId())); - jsonValue.addChild("victim", new JsonValue(placeAirplaneCommand.getPlane().getOwner())); - client.getPeerListener().writePacket(jsonValue); - client.getPeerListener().handleTopDog(); - client.broadcast(Packet.PacketType.TOP_DOG, jsonValue.toJson(JsonWriter.OutputType.json)); + var dto = new TopDogPlaceAirplaneDto(placeAirplaneCommand.getPlane().getId(), + placeAirplaneCommand.getPath().getId(), placeAirplaneCommand.getPlane().getOwner()); + dto.apply(client.getPeerListener().getGameMap(), true); + client.broadcast(Packet.PacketType.TOP_DOG_PLACE_AIRPLANE, dto); } else if (selectedInventoryItemType != InventoryItemType.Empty) { StreamlinesLogger.logger.warn("Attempted to apply power up to non-existent terminal"); } diff --git a/core/src/main/java/io/streamlines/network/Client.java b/core/src/main/java/io/streamlines/network/Client.java index b61d3f23..0d2484b0 100644 --- a/core/src/main/java/io/streamlines/network/Client.java +++ b/core/src/main/java/io/streamlines/network/Client.java @@ -23,6 +23,7 @@ public class Client implements Broadcastable, Disposable { private final GameMap gameMap; public Main gameLoop; private final PeerListener peerListener; + private final Json json; public static String getHostIP() { return Main.hostIP; @@ -38,6 +39,7 @@ public Client(GameMap gameMap, GameLoop gameLoop, int port) { socket.setSendGracefully(true); this.gameMap = gameMap; peerListener = new PeerListener(gameMap); + json = new Json(); GameServerListener listener = new GameServerListener(); socket.addListener(listener); @@ -62,6 +64,10 @@ public Client(GameMap gameMap, GameLoop gameLoop, int port) { player = new Player(gameMap, ""); // Placeholder to prevent NPEs } + private boolean parseDataAndApplyAction(Class type, String data) { + return json.fromJson(type, data).apply(gameMap, true); + } + public PeerListener getPeerListener() { return peerListener; } public Player getPlayer() { @@ -125,7 +131,7 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { break; case DAY_DATA: gameLoop.disablePaths(); - peerListener.parseData(NewDayDto.class, packet).apply(gameMap, true); + parseDataAndApplyAction(NewDayDto.class, packet); gameLoop.reenablePaths(); LogMessage.setMessage("A new day has begun!"); @@ -145,75 +151,74 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { }); break; case PATH_EDIT: - peerListener.editPath(); + parseDataAndApplyAction(EditPathDto.class, packet); break; case NEW_PATH: - peerListener.newPath(); + parseDataAndApplyAction(NewPathDto.class, packet); break; - case AIRPLANE_DATA: - if (pathParameters.getString("action").equals("takeoff")) { - gameMap.getAirplaneById(pathParameters.getString("id")).takeoff(); - } else { - peerListener.handleAirplaneData(); - } + case AIRPLANE_SYSTEM_ACTION: + parseDataAndApplyAction(AirplaneSystemActionDto.class, packet); break; - case TERMINAL_DATA: - peerListener.handleTerminalData(); + case AIRPLANE_USER_INVENTORY_ACTION: + parseDataAndApplyAction(AirplaneUserInventoryActionDto.class, packet); break; + case TERMINAL_USER_INVENTORY_ACTION: + parseDataAndApplyAction(TerminalUserInventoryActionDto.class, packet); + break; + case GAMBLE_TERMINAL: + parseDataAndApplyAction(GambleTerminalDto.class, packet); case INITIALIZED: gameLoop.nextStage(); break; case UNLOAD: - LandingDto landingParameters = peerListener.parseData(LandingDto.class, packet); - landingParameters.apply(gameMap, true); + parseDataAndApplyAction(LandingDto.class, packet); break; - case SURPASSED: - if (pathParameters.getString("action").equals("prompt") && - pathParameters.getString("player").equals(getPlayer().getId())) { - StreamlinesLogger.logger.info("DOUBLE QUOTA UI", "Received surpassed message to start double quota"); - String powerupStr = pathParameters.getString("powerups"); - String infrastructureStr = pathParameters.getString("infrastructure"); - Arrays.stream(powerupStr.split(",")).map(InventoryItemType::valueOf).forEach(gameLoop::doubleQuotaAddItem); - Arrays.stream(infrastructureStr.split(",")).map(InventoryItemType::valueOf).forEach(gameLoop::doubleQuotaAddItem); - Gdx.app.postRunnable(() -> gameLoop.refresh("doubleQuota")); - } else if (pathParameters.getString("action").equals("chose")) { - peerListener.addNewInventoryItems(); - } + case SURPASSED_CHOSE: + parseDataAndApplyAction(SurpassedChoseDto.class, packet); + break; + case SURPASSED_PROMPT: + StreamlinesLogger.logger.info("DOUBLE QUOTA UI", "Received surpassed message to start double quota"); + String powerupStr = pathParameters.getString("powerups"); + String infrastructureStr = pathParameters.getString("infrastructure"); + Arrays.stream(powerupStr.split(",")).map(InventoryItemType::valueOf).forEach(gameLoop::doubleQuotaAddItem); + Arrays.stream(infrastructureStr.split(",")).map(InventoryItemType::valueOf).forEach(gameLoop::doubleQuotaAddItem); + Gdx.app.postRunnable(() -> gameLoop.refresh("doubleQuota")); break; case NEW_BID: - gameMap - .getAirportById(pathParameters.getString("terminal").substring(0, 3)) - .findTerminalById(pathParameters.getString("terminal")) + var newBidDto = json.fromJson(NewBidDto.class, packet); + gameMap.getAirportById(newBidDto.terminalId().substring(0, 3)) + .findTerminalById(newBidDto.terminalId()) .ifPresent(item -> { - if (item.getOwner().equals(player.getId()) && !pathParameters.getString("owner").equals(player.getId())) { + if (item.getOwner().equals(player.getId()) && !newBidDto.owner().equals(player.getId())) { LogMessage.setMessage("You were outbid at " + item.getId() + " by " + - gameMap.getPlayer(pathParameters.getString("owner")).getName() + gameMap.getPlayer(newBidDto.owner()).getName() ); } }); - peerListener.handleNewBid(); + newBidDto.apply(gameMap, true); break; - case TOP_DOG: - if (pathParameters.getString("action").equals("identify")) { - LogMessage.setMessage(gameMap.getPlayer(pathParameters.getString("id")).getName() + " is Top Dog!"); - } else if (pathParameters.getString("action").equals("use")) { - if (pathParameters.getString("victim").equals(player.getId())) { - LogMessage.setMessage(gameMap.getPlayer(gameMap.getTopDog()).getName() + " stole your " + pathParameters.getString( - "infrastructureType")); - } - } - peerListener.handleTopDog(); + case TOP_DOG_IDENTIFY: + var dto = json.fromJson(SetTopDogDto.class, packet); + LogMessage.setMessage(gameMap.getPlayer(dto.playerId()).getName() + " is Top Dog!"); + break; + case TOP_DOG_PLACE_ROUTE: + LogMessage.setMessage(gameMap.getPlayer(gameMap.getTopDog()).getName() + " stole your route!"); + parseDataAndApplyAction(TopDogPlaceRouteDto.class, packet); + break; + case TOP_DOG_PLACE_AIRPLANE: + LogMessage.setMessage(gameMap.getPlayer(gameMap.getTopDog()).getName() + " stole your plane!"); + parseDataAndApplyAction(TopDogPlaceAirplaneDto.class, packet); break; case PATH_RMV: - peerListener.removeAirportFromPath(); + parseDataAndApplyAction(RemovePathDto.class, packet); break; case WEATHER_EVENT: - WeatherDto parameters = peerListener.parseData(WeatherDto.class, packet); - if (parameters.status().ordinal() > WeatherEventSeverity.FINE.ordinal()) { - LogMessage.setMessage("A level " + parameters.status().ordinal() + " weather event is " + - "underway at " + parameters.airportId()); + WeatherDto weatherDto = json.fromJson(WeatherDto.class, packet); + if (weatherDto.status().ordinal() > WeatherEventSeverity.FINE.ordinal()) { + LogMessage.setMessage("A level " + weatherDto.status().ordinal() + " weather event is " + + "underway at " + weatherDto.airportId()); } - parameters.apply(gameMap, true); + weatherDto.apply(gameMap, true); break; case RETURN_PASSENGER_COUNT: String planeId = pathParameters.getString("plane_id"); @@ -239,8 +244,9 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { Gdx.app.postRunnable(() -> gameLoop.getWidget("stats").refresh()); break; case UNASSIGNED: - LogMessage.setMessage(pathParameters.getString("player") + " has left the game!"); - new LosePlayerDto(pathParameters.getString("player")).apply(gameMap, true); + var unassignedDto = json.fromJson(LosePlayerDto.class, packet); + LogMessage.setMessage(unassignedDto.playerId() + " has left the game!"); + unassignedDto.apply(gameMap, true); if(gameMap.hasEnded()) { Gdx.app.postRunnable(() -> gameLoop.nextStage()); } diff --git a/server/src/main/java/io/streamlines/map/Spawner.java b/server/src/main/java/io/streamlines/map/Spawner.java index 097dd57c..2cf8b448 100644 --- a/server/src/main/java/io/streamlines/map/Spawner.java +++ b/server/src/main/java/io/streamlines/map/Spawner.java @@ -73,10 +73,8 @@ public void run() { broadcaster.broadcast(Packet.PacketType.DAY_DATA, dayData); dayData.apply(Spawner.this, false); - 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 topDogData = new SetTopDogDto(getTopDog()); + broadcaster.broadcast(Packet.PacketType.TOP_DOG_IDENTIFY, topDogData); } }, 0, GameMap.DAY_RATE * 1000); @@ -158,11 +156,8 @@ 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 @@ -175,7 +170,6 @@ public void notifyLanding(Airplane airplane, boolean surpassed) { 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 @@ -199,7 +193,7 @@ public void notifyLanding(Airplane airplane, boolean surpassed) { .map(InventoryItemType::name) .collect(Collectors.joining(",")))); - broadcaster.broadcast(Packet.PacketType.SURPASSED, jsonValue.toJson(JsonWriter.OutputType.json)); + broadcaster.broadcast(Packet.PacketType.SURPASSED_PROMPT, jsonValue.toJson(JsonWriter.OutputType.json)); } }; newAirport.registerObserver(observer); diff --git a/server/src/main/java/io/streamlines/network/ServerLauncher.java b/server/src/main/java/io/streamlines/network/ServerLauncher.java index 478d7c5f..87cebf02 100644 --- a/server/src/main/java/io/streamlines/network/ServerLauncher.java +++ b/server/src/main/java/io/streamlines/network/ServerLauncher.java @@ -1,7 +1,6 @@ package io.streamlines.network; -import com.badlogic.gdx.utils.JsonValue; -import com.badlogic.gdx.utils.JsonWriter; +import com.badlogic.gdx.utils.*; import io.streamlines.*; import io.streamlines.flight.Airplane; import io.streamlines.flight.Airport; @@ -35,6 +34,7 @@ public class ServerLauncher extends WebSocketServer implements Broadcastable { private final PIDTracker pidTracker; private final PeerListener peerListener; + private final Json json; public ServerLauncher(int port) throws UnknownHostException { super(new InetSocketAddress(port)); @@ -43,6 +43,7 @@ public ServerLauncher(int port) throws UnknownHostException { gameMap = new Spawner(this); pidTracker = new PIDTracker(4); peerListener = new PeerListener(gameMap); + json = new Json(JsonWriter.OutputType.javascript); } @Override @@ -122,12 +123,9 @@ private void sendPacketTo(Packet.PacketType type, String data, WebSocket conn) { @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); - new LosePlayerDto(data.getString("player")).apply(gameMap, false); - + 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); } @@ -151,18 +149,16 @@ public void onMessage(WebSocket conn, String message) { // 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 AIRPLANE_USER_INVENTORY_ACTION -> parseAndApplyDto(AirplaneUserInventoryActionDto.class, message); + case TERMINAL_USER_INVENTORY_ACTION -> parseAndApplyDto(TerminalUserInventoryActionDto.class, message); + case GAMBLE_TERMINAL -> parseAndApplyDto(GambleTerminalDto.class, message); + case PATH_EDIT -> parseAndApplyDto(EditPathDto.class, message); + case NEW_PATH -> parseAndApplyDto(NewPathDto.class, message); + case PATH_RMV -> parseAndApplyDto(RemovePathDto.class, message); + case NEW_BID -> parseAndApplyDto(NewBidDto.class, message); + case TOP_DOG_PLACE_AIRPLANE -> parseAndApplyDto(TopDogPlaceAirplaneDto.class, message); + case TOP_DOG_PLACE_ROUTE -> parseAndApplyDto(TopDogPlaceRouteDto.class, message); + case TOP_DOG_IDENTIFY -> parseAndApplyDto(SetTopDogDto.class, message); case REQUEST_PASSENGER_COUNT -> { String planeId = params.getString("plane_id"); Airplane plane = gameMap.getAirplaneById(planeId); @@ -182,7 +178,7 @@ public void onMessage(WebSocket conn, String message) { sendPacketTo(Packet.PacketType.RETURN_PASSENGER_COUNT, jsonValue.toJson(JsonWriter.OutputType.json), conn); yield false; } - case SURPASSED -> peerListener.addNewInventoryItems(); + case SURPASSED_CHOSE -> parseAndApplyDto(SurpassedChoseDto.class, message); case PLAYER_NAME -> setUpNewPlayer(conn, packet.parse().getString("pname")); default -> false; }; @@ -199,6 +195,10 @@ public static InventoryItemType randomItemRequest(Random random, Class boolean parseAndApplyDto(Class type, String data) { + return json.fromJson(type, data).apply(gameMap, false); + } + @Override public void onMessage(WebSocket conn, ByteBuffer message) { broadcast(message.array()); diff --git a/shared/src/main/java/io/streamlines/flight/PathService.java b/shared/src/main/java/io/streamlines/flight/PathService.java index 6fe69bd3..74f4d9fb 100644 --- a/shared/src/main/java/io/streamlines/flight/PathService.java +++ b/shared/src/main/java/io/streamlines/flight/PathService.java @@ -246,11 +246,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; 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..7840d21c --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/AirplaneSystemActionDto.java @@ -0,0 +1,15 @@ +package io.streamlines.network; + +import io.streamlines.map.GameMap; + +public record AirplaneSystemActionDto(String airplaneId, AirplaneSystemAction action) + implements DtoActionable { + + @Override + public Boolean apply(GameMap gameMap, Boolean isClient) { + if (action == AirplaneSystemAction.TAKEOFF) { + gameMap.getAirplaneById(airplaneId).takeoff(); + } + return false; + } +} 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..106a4a38 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/AirplaneUserInventoryActionDto.java @@ -0,0 +1,47 @@ +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.util.Optional; + +/** + * Uses airplane-related (infrastructure and powerup) items from inventory + */ +public record AirplaneUserInventoryActionDto(String playerId, String airplaneId, Vector2 position, String pathId, + InventoryItemType action) + implements DtoActionable { + @Override + public Boolean apply(GameMap gameMap, Boolean isClient) { + Player player = gameMap.getPlayer(playerId); + if (action == InventoryItemType.Expand_Airplane) { + player.getInventory().useItem(new ExpandAirplaneCommand( + gameMap.getAirplaneById(airplaneId) + )); + } else if (action == InventoryItemType.Resilience) { + gameMap.getPlayer(playerId).getInventory().useItem(new ResilienceCommand( + gameMap.getAirplaneById(airplaneId) + )); + } 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; + } +} diff --git a/shared/src/main/java/io/streamlines/network/DtoActionable.java b/shared/src/main/java/io/streamlines/network/DtoActionable.java index 0e110891..d8e9e94a 100644 --- a/shared/src/main/java/io/streamlines/network/DtoActionable.java +++ b/shared/src/main/java/io/streamlines/network/DtoActionable.java @@ -11,5 +11,5 @@ public interface DtoActionable extends Serializable, BiFunction { @Override - Boolean apply(GameMap gameMap, Boolean servile); + 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..b0ee6abb --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/EditPathDto.java @@ -0,0 +1,31 @@ +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.util.Optional; + +public record EditPathDto(String playerId, String pathId, String airportId1, String airportId2) + implements DtoActionable { + @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; + } +} 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..7fc809e6 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/GambleTerminalDto.java @@ -0,0 +1,22 @@ +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.util.Optional; + +public record GambleTerminalDto(String playerId, String airportId, boolean keep) implements DtoActionable { + @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; + } + terminal.get().finishPending(keep); + return true; + } +} diff --git a/shared/src/main/java/io/streamlines/network/LandingDto.java b/shared/src/main/java/io/streamlines/network/LandingDto.java index 7f661a97..016cf006 100644 --- a/shared/src/main/java/io/streamlines/network/LandingDto.java +++ b/shared/src/main/java/io/streamlines/network/LandingDto.java @@ -12,11 +12,11 @@ public record LandingDto(int throughput, String airportId, String airplaneId, St implements DtoActionable { /** * @param gameMap The current state of the game map - * @param servile true for setting the throughput through parameters, false if not (already set internally) + * @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 servile) { + public Boolean apply(GameMap gameMap, Boolean isClient) { Airport airport = gameMap.getAirportById(airportId); if (airport.findTerminalByOwner(playerId).isEmpty()) { @@ -25,7 +25,7 @@ public Boolean apply(GameMap gameMap, Boolean servile) { return false; } - if (servile) { + if (isClient) { airport.findTerminalByOwner(playerId).get().updateThroughput(throughput); } gameMap.getPlayer(playerId).addScore(throughput); diff --git a/shared/src/main/java/io/streamlines/network/LosePlayerDto.java b/shared/src/main/java/io/streamlines/network/LosePlayerDto.java index 4caf8f0c..964108f9 100644 --- a/shared/src/main/java/io/streamlines/network/LosePlayerDto.java +++ b/shared/src/main/java/io/streamlines/network/LosePlayerDto.java @@ -4,7 +4,7 @@ public record LosePlayerDto(String playerId) implements DtoActionable { @Override - public Boolean apply(GameMap gameMap, Boolean servile) { + public Boolean apply(GameMap gameMap, Boolean isClient) { gameMap.removePlayer(playerId); if(gameMap.shouldEnd()) { gameMap.end(); 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..55c5310c --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/NewBidDto.java @@ -0,0 +1,22 @@ +package io.streamlines.network; + +import io.streamlines.flight.Airport; +import io.streamlines.flight.Terminal; +import io.streamlines.map.GameMap; + +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 record NewBidDto(String terminalId, String owner) implements DtoActionable { + @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(); + } +} diff --git a/shared/src/main/java/io/streamlines/network/NewDayDto.java b/shared/src/main/java/io/streamlines/network/NewDayDto.java index 29236ac9..250caabf 100644 --- a/shared/src/main/java/io/streamlines/network/NewDayDto.java +++ b/shared/src/main/java/io/streamlines/network/NewDayDto.java @@ -6,7 +6,7 @@ public record NewDayDto(int dayNumber, int time) implements DtoActionable { @Override - public Boolean apply(GameMap gameMap, Boolean servile) { + public Boolean apply(GameMap gameMap, Boolean isClient) { gameMap.startNewDay(dayNumber); GameMap.timeAtDayStart = (TimeUtils.millis() / 1000) - time; StreamlinesLogger.logger.info("Time", "Day " + dayNumber + " has begun"); 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..cc4afb1e --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/NewPathDto.java @@ -0,0 +1,24 @@ +package io.streamlines.network; + +import io.streamlines.StreamlinesLogger; +import io.streamlines.flight.Airport; +import io.streamlines.map.GameMap; + +public record NewPathDto(String playerId, String pathId, String airportId1, String airportId2, String airplaneId) implements DtoActionable { + 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); + var 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; + } +} diff --git a/shared/src/main/java/io/streamlines/network/Packet.java b/shared/src/main/java/io/streamlines/network/Packet.java index ffe7483c..e3986f89 100644 --- a/shared/src/main/java/io/streamlines/network/Packet.java +++ b/shared/src/main/java/io/streamlines/network/Packet.java @@ -20,52 +20,28 @@ public final class Packet { } 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"); + 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; - PacketType(String callsign) { - this.callsign = callsign; - } + 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; @@ -73,9 +49,6 @@ public PacketType getPacketType() { private transient final String contents; - private PacketType packetType; - private DtoActionable dto; - /** * Creates packet for receiving serialized contents, so it can get parsed. * @param rawContents String representation of data @@ -90,13 +63,10 @@ public Packet(String rawContents) { * @param contents String representation of data. */ public Packet(PacketType type, String contents) { - packetType = type; this.contents = type.callsign + "=" + contents; } public Packet(PacketType type, DtoActionable data) { - packetType = type; - dto = data; this.contents = type.callsign + "=" + json.toJson(data); } diff --git a/shared/src/main/java/io/streamlines/network/PeerListener.java b/shared/src/main/java/io/streamlines/network/PeerListener.java index ca0183ab..41f0bc5f 100644 --- a/shared/src/main/java/io/streamlines/network/PeerListener.java +++ b/shared/src/main/java/io/streamlines/network/PeerListener.java @@ -1,14 +1,7 @@ package io.streamlines.network; -import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.utils.*; -import io.streamlines.StreamlinesLogger; -import io.streamlines.flight.*; -import io.streamlines.inventory.*; +import com.badlogic.gdx.utils.JsonValue; import io.streamlines.map.GameMap; -import io.streamlines.map.Player; - -import java.util.Optional; /** * Shared actions between game server and game clients when a message is received over web socket. @@ -16,12 +9,13 @@ */ public class PeerListener { private final GameMap gameMap; - private JsonValue params; - private final Json json; public PeerListener(GameMap map) { gameMap = map; - json = new Json(JsonWriter.OutputType.javascript); + } + + public GameMap getGameMap() { + return gameMap; } /** @@ -29,167 +23,7 @@ public PeerListener(GameMap map) { * @param packet The packet object containing JSON-serialized key-value pairs */ JsonValue readPacket(Packet packet) { - return params = packet.parse(); - } - - T parseData(Class type, String data) { - return json.fromJson(type, data); - } - - @Deprecated - public void writePacket(JsonValue packet) { - params = packet; - } - - private Player getPlayer() { - return gameMap.getPlayer(params.getString("player")); - } - - /** - * 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; + return packet.parse(); } - 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; - } } 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..44a3b8cb --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/RemovePathDto.java @@ -0,0 +1,19 @@ +package io.streamlines.network; + +import io.streamlines.flight.Airport; +import io.streamlines.flight.PathService; +import io.streamlines.map.GameMap; +import io.streamlines.map.Player; + +public record RemovePathDto(String pathId, String airportId, String playerId) implements DtoActionable { + @Override + public Boolean apply(GameMap gameMap, Boolean isClient) { + PathService path = gameMap.getPathServiceById(pathId, playerId); + Airport toRemove = gameMap.getAirportById(airportId); + Player player = gameMap.getPlayer(playerId); + if (gameMap.removeAirportFromPath(path, toRemove, player)) { + player.removePath(path); + } + return true; // Assume success either way + } +} 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..2671746f --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/SetTopDogDto.java @@ -0,0 +1,12 @@ +package io.streamlines.network; + +import io.streamlines.map.GameMap; + +public record SetTopDogDto(String playerId) + implements DtoActionable { + @Override + public Boolean apply(GameMap gameMap, Boolean isClient) { + gameMap.setTopDog(playerId); + return true; + } +} 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..1c1d4ceb --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/SurpassedChoseDto.java @@ -0,0 +1,14 @@ +package io.streamlines.network; + +import io.streamlines.inventory.InventoryItemType; +import io.streamlines.map.GameMap; + +public record SurpassedChoseDto(String playerId, InventoryItemType item) implements DtoActionable { + @Override + public Boolean apply(GameMap gameMap, Boolean isClient) { + var player = gameMap.getPlayer(playerId); + player.getInventory().addItem(item); + player.getInventory().addItem(InventoryItemType.Place_Route); + return true; + } +} 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..d8c8e7d7 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/TerminalUserInventoryActionDto.java @@ -0,0 +1,27 @@ +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.util.Optional; + +public record TerminalUserInventoryActionDto(String playerId, String airportId, InventoryItemType action) + implements DtoActionable { + @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; + } +} 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..0134c391 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/TopDogPlaceAirplaneDto.java @@ -0,0 +1,28 @@ +package io.streamlines.network; + +import io.streamlines.StreamlinesLogger; +import io.streamlines.inventory.PlaceAirplaneCommand; +import io.streamlines.inventory.TopDogCommand; +import io.streamlines.map.GameMap; +import io.streamlines.map.Player; + +public record TopDogPlaceAirplaneDto(String airplaneId, String pathId, String victimId) + implements DtoActionable { + @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()) { + return false; + } + var command = new PlaceAirplaneCommand(gameMap.getAirplaneById(airplaneId), victim.getPathById(pathId).get()); + gameMap.getPlayer(gameMap.getTopDog()).getInventory().useItem(new TopDogCommand(victim.getId(), + command)); + + return true; + } +} 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..43eef28e --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/TopDogPlaceRouteDto.java @@ -0,0 +1,30 @@ +package io.streamlines.network; + +import io.streamlines.StreamlinesLogger; +import io.streamlines.inventory.PlaceRouteCommand; +import io.streamlines.inventory.TopDogCommand; +import io.streamlines.map.GameMap; +import io.streamlines.map.Player; + +public record TopDogPlaceRouteDto(String victimId, String pathId, String airportStart, String airportEnd) + implements DtoActionable { + @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; + } + + var 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; + } +} diff --git a/shared/src/main/java/io/streamlines/network/WeatherDto.java b/shared/src/main/java/io/streamlines/network/WeatherDto.java index 4b1f7b03..dbfab4e5 100644 --- a/shared/src/main/java/io/streamlines/network/WeatherDto.java +++ b/shared/src/main/java/io/streamlines/network/WeatherDto.java @@ -6,7 +6,7 @@ public record WeatherDto(WeatherEventSeverity status, String airportId) implements DtoActionable { @Override - public Boolean apply(GameMap gameMap, Boolean servile) { + public Boolean apply(GameMap gameMap, Boolean isClient) { Airport affectedAirport = gameMap.getAirportById(airportId); if (status == WeatherEventSeverity.FINE) { affectedAirport.clearWeatherEvent(); From 6f9e984cefefba2a07180b056bd611a2b5d1b619 Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Wed, 20 Aug 2025 19:23:19 -0400 Subject: [PATCH 04/46] Finish creating DTOs for server and client, remove PeerListener.java --- .../main/java/io/streamlines/GameLoop.java | 19 +-- core/src/main/java/io/streamlines/Main.java | 18 ++- .../java/io/streamlines/RouteManager.java | 19 ++- .../java/io/streamlines/UI/AirportUI.java | 4 +- .../java/io/streamlines/UI/DoubleQuotaUI.java | 2 +- .../java/io/streamlines/UI/MainGameUI.java | 32 ++--- .../java/io/streamlines/UI/StatisticsUI.java | 23 ++-- .../java/io/streamlines/network/Client.java | 117 ++++++++---------- .../io/streamlines/lwjgl3/StartupHelper.java | 98 +++++++-------- .../main/java/io/streamlines/map/Spawner.java | 50 ++++---- .../streamlines/network/ServerLauncher.java | 97 +++++++-------- .../java/io/streamlines/map/PlayerTest.java | 9 +- .../java/io/streamlines/map/SpawnerTest.java | 25 ++-- .../network/ServerLauncherTest.java | 24 ---- .../java/io/streamlines/flight/Airplane.java | 29 +++-- .../java/io/streamlines/flight/Airport.java | 62 ++-------- .../io/streamlines/flight/PathService.java | 38 +++--- .../io/streamlines/inventory/Inventory.java | 10 +- .../main/java/io/streamlines/map/GameMap.java | 16 +-- .../map/GraphAlgorithmObserver.java | 36 +++--- .../main/java/io/streamlines/map/Player.java | 11 ++ .../network/AirplaneSystemActionDto.java | 3 +- .../AirplaneUserInventoryActionDto.java | 25 ++-- .../streamlines/network/AirportListDto.java | 20 +++ .../io/streamlines/network/Broadcastable.java | 4 +- .../io/streamlines/network/DtoActionable.java | 3 +- .../java/io/streamlines/network/Packet.java | 32 +++-- .../io/streamlines/network/PeerListener.java | 29 ----- .../streamlines/network/PidAssignmentDto.java | 7 ++ .../network/RequestPassengerCountDto.java | 8 ++ .../network/ReturnPassengerCountDto.java | 16 +++ .../network/SurpassedPromptDto.java | 8 ++ .../network/TopDogPlaceAirplaneDto.java | 5 +- .../io/streamlines/network/UpdateTraffic.java | 15 +++ .../network/UpdateTrafficListDto.java | 14 +++ 35 files changed, 457 insertions(+), 471 deletions(-) delete mode 100644 server/src/test/java/io/streamlines/network/ServerLauncherTest.java create mode 100644 shared/src/main/java/io/streamlines/network/AirportListDto.java delete mode 100644 shared/src/main/java/io/streamlines/network/PeerListener.java create mode 100644 shared/src/main/java/io/streamlines/network/PidAssignmentDto.java create mode 100644 shared/src/main/java/io/streamlines/network/RequestPassengerCountDto.java create mode 100644 shared/src/main/java/io/streamlines/network/ReturnPassengerCountDto.java create mode 100644 shared/src/main/java/io/streamlines/network/SurpassedPromptDto.java create mode 100644 shared/src/main/java/io/streamlines/network/UpdateTraffic.java create mode 100644 shared/src/main/java/io/streamlines/network/UpdateTrafficListDto.java diff --git a/core/src/main/java/io/streamlines/GameLoop.java b/core/src/main/java/io/streamlines/GameLoop.java index 4b0eb70f..f3dcce01 100644 --- a/core/src/main/java/io/streamlines/GameLoop.java +++ b/core/src/main/java/io/streamlines/GameLoop.java @@ -14,8 +14,8 @@ import io.streamlines.network.Packet; import java.lang.StringBuilder; -import java.util.Queue; import java.util.*; +import java.util.Queue; import static io.streamlines.map.GameMapAccessible.VIRTUAL_HEIGHT; import static io.streamlines.map.GameMapAccessible.VIRTUAL_WIDTH; @@ -26,6 +26,11 @@ */ public abstract class GameLoop extends Game { private static GameLoop instance; + + /** + * Gets the singleton for the game loop + * @return + */ public static GameLoop get() { return instance; } private int mainPort; private LobbyUI lobbyUI; @@ -294,13 +299,11 @@ public static Viewport getViewport() { public static ShapeRenderer getShapeRenderer() { return instance.shape; } - /** - * Queues audio loading. Loaded audio is passed via - * callback when ready. Only necessary when - * audio status is unknown (i.e at the start of the game) - * @param key The identifier (filename) for the audio - * @param callback The procedure to execute once audio is queued - */ + /// Queues audio loading. Loaded audio is passed via + /// callback when ready. **Only necessary when + /// audio status is unknown (i.e at the start of the game)** + /// @param key The identifier (filename) for the audio + /// @param callback The procedure to execute once audio is queued public static void queueLoadAudio(String key, io.streamlines.EventListener callback) { Gdx.app.log("AUDIO-QUEUE", key); instance.audioQueue.add(new Audio.QueuedAudioData(key, callback)); diff --git a/core/src/main/java/io/streamlines/Main.java b/core/src/main/java/io/streamlines/Main.java index 005cd9fa..b5688e33 100644 --- a/core/src/main/java/io/streamlines/Main.java +++ b/core/src/main/java/io/streamlines/Main.java @@ -1,16 +1,16 @@ package io.streamlines; -import com.badlogic.gdx.*; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Input; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.utils.*; +import com.badlogic.gdx.utils.Scaling; import io.streamlines.UI.*; import io.streamlines.flight.*; import io.streamlines.inventory.InventoryItemType; import io.streamlines.inventory.PlaceAirplaneCommand; import io.streamlines.map.*; -import io.streamlines.network.Broadcastable; -import io.streamlines.network.Packet; +import io.streamlines.network.*; import java.util.HashMap; import java.util.Map; @@ -44,7 +44,7 @@ public void load() { widgets.put("stats", new StatisticsUI()); userCursor = new Vector2(-100, -100); - routeManager = new RouteManager(userCursor, gameClient.getPlayer(), gameClient, gameClient.getPeerListener()); + routeManager = new RouteManager(userCursor, gameClient.getPlayer(), gameClient, gameClient.getGameMap()); for (FullScreenWidget widget : widgets.values()) { widget.getStage().addDependency("main", this); @@ -212,12 +212,8 @@ public void update() { // Check if planes need updated info for (Airplane a : r.getPlanes()) { if (a.requiresPassengerUpdate()) { - JsonValue json = new JsonValue(JsonValue.ValueType.object); - json.addChild("plane_id", new JsonValue(a.getId())); - // The server also verifies that this player is authorized to see the passenger counts (owns the - // plane) - json.addChild("player", new JsonValue(gameClient.getPlayer().getId())); - gameClient.broadcast(Packet.PacketType.REQUEST_PASSENGER_COUNT, json.toJson(JsonWriter.OutputType.json)); + var dto = new RequestPassengerCountDto(a.getId(), gameClient.getPlayer().getId()); + gameClient.broadcast(Packet.PacketType.REQUEST_PASSENGER_COUNT, dto); a.acknowledgePassengerUpdate(); } diff --git a/core/src/main/java/io/streamlines/RouteManager.java b/core/src/main/java/io/streamlines/RouteManager.java index c201d995..e0604f16 100644 --- a/core/src/main/java/io/streamlines/RouteManager.java +++ b/core/src/main/java/io/streamlines/RouteManager.java @@ -9,8 +9,7 @@ import io.streamlines.UI.MainGameUI; import io.streamlines.flight.*; import io.streamlines.inventory.InventoryItemType; -import io.streamlines.map.GameMapAccessible; -import io.streamlines.map.Player; +import io.streamlines.map.*; import io.streamlines.network.*; import java.util.*; @@ -27,7 +26,7 @@ public class RouteManager { private final Vector2 userCursor; private final Player clientPlayer; private final Broadcastable broadcaster; - private final PeerListener peerListener; + private final GameMap gameMap; private final Color transparentPreviewColor; @@ -57,12 +56,12 @@ public void execute() { if (path.getFirstPlane() != null) { var dto = new NewPathDto(clientPlayer.getId(), path.getId(), routeBuilderStart.getId(), routeBuilderEnd.getId(), path.getFirstPlane().getId()); - dto.apply(peerListener.getGameMap(), true); + dto.apply(gameMap, true); broadcaster.broadcast(Packet.PacketType.NEW_PATH, dto); } else { var dto = new NewPathDto(clientPlayer.getId(), path.getId(), routeBuilderStart.getId(), routeBuilderEnd.getId()); - dto.apply(peerListener.getGameMap(), true); + dto.apply(gameMap, true); broadcaster.broadcast(Packet.PacketType.NEW_PATH, dto); } } @@ -96,7 +95,7 @@ public void execute() { var dto = new EditPathDto( clientPlayer.getId(), selectedPath.getId(), routeBuilderStart.getId(), routeBuilderEnd.getId() ); - dto.apply(peerListener.getGameMap(), true); + dto.apply(gameMap, true); broadcaster.broadcast(Packet.PacketType.PATH_EDIT, dto); } }; @@ -120,11 +119,11 @@ public void execute() {} private RouteActionable currentRouteMode = UNSET; - RouteManager(Vector2 cursor, Player player, Broadcastable broadcastable, PeerListener pl) { + RouteManager(Vector2 cursor, Player player, Broadcastable broadcastable, GameMap map) { userCursor = cursor; clientPlayer = player; broadcaster = broadcastable; - peerListener = pl; + gameMap = map; transparentPreviewColor = new Color(clientPlayer.getColor()); transparentPreviewColor.a = 0.5f; } @@ -249,7 +248,7 @@ private void handleAirplanePlacement(PathService r) { var data = new AirplaneUserInventoryActionDto(clientPlayer.getId(), Airplane.generateNewId(), userCursor, r.getId(), InventoryItemType.Place_Airplane); broadcaster.broadcast(Packet.PacketType.AIRPLANE_USER_INVENTORY_ACTION, data); - data.apply(peerListener.getGameMap(), false); + data.apply(gameMap, false); } public void run(GameMapAccessible map, MainGameUI mainGame, InventoryItemType selection) { @@ -259,7 +258,7 @@ public void run(GameMapAccessible map, MainGameUI mainGame, InventoryItemType se if (Gdx.input.isKeyJustPressed(Input.Keys.D) && selectedPath != null) { map.getAirports().stream().filter(ScreenGraphicUtils::isHoveringOver).findFirst().ifPresent(a -> { var dto = new RemovePathDto(selectedPath.getId(), a.getId(), clientPlayer.getId()); - if (dto.apply(peerListener.getGameMap(), true)) { + if (dto.apply(gameMap, true)) { mainGame.refresh(); broadcaster.broadcast(Packet.PacketType.PATH_RMV, dto); selectedPath = null; diff --git a/core/src/main/java/io/streamlines/UI/AirportUI.java b/core/src/main/java/io/streamlines/UI/AirportUI.java index 274e9a72..f96edf36 100644 --- a/core/src/main/java/io/streamlines/UI/AirportUI.java +++ b/core/src/main/java/io/streamlines/UI/AirportUI.java @@ -32,7 +32,7 @@ private void bidOn(int term, Client gameClient) { Terminal terminal = airportToRepresent.getTerminals()[term]; StreamlinesLogger.logger.info("Bidding","Sending terminal bid for " + terminal.getId()); var dto = new NewBidDto(terminal.getId(), gameClient.getPlayer().getId()); - if (!dto.apply(gameClient.getPeerListener().getGameMap(), true)) { + if (!dto.apply(gameClient.getGameMap(), true)) { LogMessage.setMessage("Bid was unsuccessful"); return; } @@ -121,7 +121,7 @@ public void clicked(InputEvent event, float x, float y) { int destsToShow = Math.min(3, airportToRepresent.getRankedDestinations().length); StringBuilder topDestinationTxt = new StringBuilder("\nTop Destinations: "); for (int i = 0; i < destsToShow; i++) { - topDestinationTxt.append(airportToRepresent.getRankedDestinations()[i].getAirportCode()).append(", "); + topDestinationTxt.append(airportToRepresent.getRankedDestinations()[i].airportCode()).append(", "); } VisLabel top = UIManager.getWhiteLabel(topDestinationTxt.toString()); listTable.add(top).row(); diff --git a/core/src/main/java/io/streamlines/UI/DoubleQuotaUI.java b/core/src/main/java/io/streamlines/UI/DoubleQuotaUI.java index 5f4057f3..54679e61 100644 --- a/core/src/main/java/io/streamlines/UI/DoubleQuotaUI.java +++ b/core/src/main/java/io/streamlines/UI/DoubleQuotaUI.java @@ -82,7 +82,7 @@ public void clicked(InputEvent event, float x, float y) { " double quota exchange)"); var dto = new SurpassedChoseDto(client.getPlayer().getId(), curItem); client.broadcast(Packet.PacketType.SURPASSED_CHOSE, dto); - dto.apply(client.getPeerListener().getGameMap(), true); + dto.apply(client.getGameMap(), true); close(); } }); diff --git a/core/src/main/java/io/streamlines/UI/MainGameUI.java b/core/src/main/java/io/streamlines/UI/MainGameUI.java index bb2479af..fa221a32 100644 --- a/core/src/main/java/io/streamlines/UI/MainGameUI.java +++ b/core/src/main/java/io/streamlines/UI/MainGameUI.java @@ -7,19 +7,18 @@ import com.badlogic.gdx.scenes.scene2d.InputEvent; import com.badlogic.gdx.scenes.scene2d.ui.*; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; -import com.badlogic.gdx.utils.Timer; import com.badlogic.gdx.utils.*; +import com.badlogic.gdx.utils.Timer; import com.kotcrab.vis.ui.widget.*; -import io.streamlines.Main; -import io.streamlines.StreamlinesLogger; +import io.streamlines.*; import io.streamlines.flight.Airplane; import io.streamlines.flight.Terminal; import io.streamlines.inventory.*; import io.streamlines.map.*; import io.streamlines.network.*; -import java.util.Collections; import java.util.*; +import java.util.Collections; public class MainGameUI implements FullScreenWidget { private final UIStage stage; @@ -48,13 +47,13 @@ public MainGameUI() { private final PlayerObserver playerObserver = new PlayerObserver() { @Override public void notifyScore(int newScore) { - Collections.sort(client.gameLoop.getGameMap().getActivePlayers()); + Collections.sort(client.getGameMap().getActivePlayers()); Gdx.app.postRunnable(MainGameUI.this::refresh); } @Override public void notifyComplaint(int newComplaint) { - Collections.sort(client.gameLoop.getGameMap().getActivePlayers()); + Collections.sort(client.getGameMap().getActivePlayers()); Gdx.app.postRunnable(MainGameUI.this::refresh); } @@ -63,7 +62,8 @@ public void notifyLoss(String playerId) { if (playerId.equals(client.getPlayer().getId())) { LogMessage.setMessage("You have been eliminated! You are now a spectator"); } else { - LogMessage.setMessage("Player " + client.gameLoop.getGameMap().getPlayer(playerId).getName() + " has been ELIMINATED"); + LogMessage.setMessage("Player " + client.getGameMap().getPlayer(playerId).getName() + " has been " + + "ELIMINATED"); } Gdx.app.postRunnable(MainGameUI.this::refresh); } @@ -71,7 +71,7 @@ public void notifyLoss(String playerId) { @Override public void notifyGameEnd() { Gdx.app.postRunnable(MainGameUI.this::refresh); - client.gameLoop.nextStage(); + GameLoop.get().nextStage(); LogMessage.setMessage("GAME OVER"); } }; @@ -145,7 +145,7 @@ private StaticVisTable sidebar() { sidebar.add(leaderboard).colspan(NUM_COLUMNS).row(); leaderboard.setFontScale(0.8f); int rank = 1; - for (Player player : client.gameLoop.getGameMap().getActivePlayers()) { + for (Player player : client.getGameMap().getActivePlayers()) { Label rankLabel = UIManager.getBlackLabel(rank + "."); rankLabel.setFontScale(0.4f); Label name = UIManager.getBlackLabel(player.getName()); @@ -199,7 +199,7 @@ private StaticVisTable sidebar() { @Override public void clicked(InputEvent event, float x, float y) { var dto = new GambleTerminalDto(item.getOwner(), item.getAirportId(), false); - dto.apply(client.getPeerListener().getGameMap(), true); + dto.apply(client.getGameMap(), true); client.broadcast(Packet.PacketType.GAMBLE_TERMINAL, dto); refresh(); } @@ -213,7 +213,7 @@ public void clicked(InputEvent event, float x, float y) { @Override public void clicked(InputEvent event, float x, float y) { var dto = new GambleTerminalDto(client.getPlayer().getId(), item.getAirportId(), true); - dto.apply(client.getPeerListener().getGameMap(), true); + dto.apply(client.getGameMap(), true); client.broadcast(Packet.PacketType.GAMBLE_TERMINAL, dto); refresh(); } @@ -293,7 +293,7 @@ public TextureRegion getStageTexture() { @Override public boolean shouldShow() { - return client.gameLoop.getGameMap().containsActivePlayer(client.getPlayer().getId()); + return client.getGameMap().containsActivePlayer(client.getPlayer().getId()); } @Override @@ -309,7 +309,7 @@ public void notifySelection(Object data) { TerminalUserInventoryActionDto dto = new TerminalUserInventoryActionDto( terminal.getOwner(), terminal.getAirportId(), InventoryItemType.Expand_Airplane ); - dto.apply(client.getPeerListener().getGameMap(), true); + dto.apply(client.getGameMap(), true); client.broadcast(Packet.PacketType.TERMINAL_USER_INVENTORY_ACTION, dto); } else if (selectedInventoryItemType == InventoryItemType.Expand_Airplane) { if (!(data instanceof Airplane planeObj)) return; @@ -319,20 +319,20 @@ public void notifySelection(Object data) { planeObj.getLocation(), "", InventoryItemType.Expand_Airplane); - dto.apply(client.getPeerListener().getGameMap(), true); + dto.apply(client.getGameMap(), true); client.broadcast(Packet.PacketType.AIRPLANE_USER_INVENTORY_ACTION, dto); } else if (selectedInventoryItemType == InventoryItemType.Resilience) { if (!(data instanceof Airplane planeObj)) return; var dto = new AirplaneUserInventoryActionDto( client.getPlayer().getId(), planeObj.getId(), planeObj.getLocation(), "", InventoryItemType.Resilience ); - dto.apply(client.getPeerListener().getGameMap(), true); + dto.apply(client.getGameMap(), true); client.broadcast(Packet.PacketType.AIRPLANE_USER_INVENTORY_ACTION, dto); } else if (selectedInventoryItemType == InventoryItemType.Top_Dog) { if (!(data instanceof PlaceAirplaneCommand placeAirplaneCommand)) return; var dto = new TopDogPlaceAirplaneDto(placeAirplaneCommand.getPlane().getId(), placeAirplaneCommand.getPath().getId(), placeAirplaneCommand.getPlane().getOwner()); - dto.apply(client.getPeerListener().getGameMap(), true); + dto.apply(client.getGameMap(), true); client.broadcast(Packet.PacketType.TOP_DOG_PLACE_AIRPLANE, dto); } else if (selectedInventoryItemType != InventoryItemType.Empty) { StreamlinesLogger.logger.warn("Attempted to apply power up to non-existent terminal"); diff --git a/core/src/main/java/io/streamlines/UI/StatisticsUI.java b/core/src/main/java/io/streamlines/UI/StatisticsUI.java index 1790d6e2..c04d2ee3 100644 --- a/core/src/main/java/io/streamlines/UI/StatisticsUI.java +++ b/core/src/main/java/io/streamlines/UI/StatisticsUI.java @@ -4,15 +4,10 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.scenes.scene2d.*; import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane; -import com.badlogic.gdx.scenes.scene2d.ui.TextButton; -import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; -import com.kotcrab.vis.ui.util.InputValidator; -import com.kotcrab.vis.ui.widget.*; +import com.kotcrab.vis.ui.widget.VisLabel; +import com.kotcrab.vis.ui.widget.VisTable; import io.streamlines.Main; -import io.streamlines.StreamlinesLogger; import io.streamlines.flight.Airport; -import io.streamlines.flight.Terminal; -import io.streamlines.map.GameMap; import java.util.*; import java.util.stream.Collectors; @@ -42,7 +37,7 @@ public void notifySelection(Object data) { refresh(); } - private InputListener scrollCursorListener = new InputListener() { + private final InputListener scrollCursorListener = new InputListener() { @Override public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) { super.enter(event, x, y, pointer, fromActor); @@ -123,16 +118,16 @@ public void refresh() { HashMap destinationInfoMap = new HashMap<>(); for (Airport airport : airports) { for (Airport.DestinationInfo localDInfo : airport.getRankedDestinations()) { - if (destinationInfoMap.containsKey(localDInfo.getAirportCode())) { + if (destinationInfoMap.containsKey(localDInfo.airportCode())) { // Combine Pop - int combinedPop = destinationInfoMap.get(localDInfo.getAirportCode()) + localDInfo.getPopularity(); - destinationInfoMap.put(localDInfo.getAirportCode(), combinedPop); + int combinedPop = destinationInfoMap.get(localDInfo.airportCode()) + localDInfo.popularity(); + destinationInfoMap.put(localDInfo.airportCode(), combinedPop); } else { - destinationInfoMap.put(localDInfo.getAirportCode(), localDInfo.getPopularity()); + destinationInfoMap.put(localDInfo.airportCode(), localDInfo.popularity()); } } } - PriorityQueue destinationInfo = new PriorityQueue<>(Comparator.comparingInt(Airport.DestinationInfo::getPopularity).reversed()); + PriorityQueue destinationInfo = new PriorityQueue<>(Comparator.comparingInt(Airport.DestinationInfo::popularity).reversed()); for (String code : destinationInfoMap.keySet()) destinationInfo.add(new Airport.DestinationInfo(code, destinationInfoMap.get(code))); @@ -144,7 +139,7 @@ public void refresh() { topDestContent.add(topDestLabel).expandX().row(); for (Airport.DestinationInfo d : destinationInfo) { VisTable childTable = new VisTable(); - childTable.add(d.getAirportCode() + ": " + d.getPopularity() + " passengers").row(); + childTable.add(d.airportCode() + ": " + d.popularity() + " passengers").row(); topDestContent.add(childTable).expandX().fillX().row(); } ScrollPane topDestPane = new ScrollPane(topDestContent); diff --git a/core/src/main/java/io/streamlines/network/Client.java b/core/src/main/java/io/streamlines/network/Client.java index 0d2484b0..ba840182 100644 --- a/core/src/main/java/io/streamlines/network/Client.java +++ b/core/src/main/java/io/streamlines/network/Client.java @@ -2,17 +2,15 @@ import com.badlogic.gdx.Application; import com.badlogic.gdx.Gdx; -import com.badlogic.gdx.graphics.Color; -import com.badlogic.gdx.utils.*; +import com.badlogic.gdx.utils.Disposable; +import com.badlogic.gdx.utils.Json; import com.github.czyzby.websocket.*; import com.github.czyzby.websocket.data.WebSocketException; import io.streamlines.*; import io.streamlines.UI.LogMessage; -import io.streamlines.flight.Airport; -import io.streamlines.inventory.InventoryItemType; import io.streamlines.map.*; -import java.util.Arrays; +import java.io.Serializable; /** * Communicates with websocket server, stores the local player, local representation of game state and game ui @@ -21,8 +19,7 @@ public class Client implements Broadcastable, Disposable { private final WebSocket socket; private Player player; private final GameMap gameMap; - public Main gameLoop; - private final PeerListener peerListener; + private final Main gameLoop; private final Json json; public static String getHostIP() { @@ -38,7 +35,6 @@ public Client(GameMap gameMap, GameLoop gameLoop, int port) { socket = WebSockets.newSocket(socketURl); socket.setSendGracefully(true); this.gameMap = gameMap; - peerListener = new PeerListener(gameMap); json = new Json(); GameServerListener listener = new GameServerListener(); socket.addListener(listener); @@ -61,27 +57,20 @@ public Client(GameMap gameMap, GameLoop gameLoop, int port) { StreamlinesLogger.logger.error("NETWORK CONNECTION", e.getMessage()); } - player = new Player(gameMap, ""); // Placeholder to prevent NPEs + player = new Player(gameMap, ""); // Placeholder to prevent NullPointerException's } - private boolean parseDataAndApplyAction(Class type, String data) { - return json.fromJson(type, data).apply(gameMap, true); + private void parseDataAndApplyAction(Class type, String data) { + json.fromJson(type, data).apply(gameMap, true); } - public PeerListener getPeerListener() { return peerListener; } + public GameMap getGameMap() { return gameMap; } public Player getPlayer() { return player; } private class GameServerListener extends WebSocketAdapter implements WebSocketListener { - private final PeerListener peerListener; - - GameServerListener() { - super(); - peerListener = new PeerListener(gameMap); - } - @Override public boolean onError(WebSocket webSocket, Throwable error) { if (webSocket.isClosed()) return FULLY_HANDLED; @@ -108,11 +97,9 @@ public boolean onClose(WebSocket webSocket, int closeCode, String reason) { public boolean onMessage(final WebSocket webSocket, final String packet) { Packet parseablePacket = new Packet(packet); String packetData = parseablePacket.getData(); - peerListener.readPacket(parseablePacket); - Gdx.app.log("WS", "Got message: " + parseablePacket.getContents()); + Gdx.app.log("WS", "Got message: " + parseablePacket.contents()); - final JsonValue pathParameters = parseablePacket.parse(); if (parseablePacket.getPacketType() == null) { StreamlinesLogger.logger.warn("Undefined packet type detected"); return FULLY_HANDLED; @@ -121,12 +108,7 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { switch (parseablePacket.getPacketType()) { case AIRPORT_DATA: gameLoop.hideAirports(); - // New data found to ADD TO old airport data (less error-prone than clearing & replacing) - StreamlinesLogger.logger.info("WS", "New Airport Data. Handling..."); - for (JsonValue airport : pathParameters) { - Airport newAirport = Airport.parse(airport.toJson(JsonWriter.OutputType.json)); - gameMap.addAirport(newAirport); - } + parseDataAndApplyAction(AirportListDto.class, packet); gameLoop.showAirports(); break; case DAY_DATA: @@ -142,13 +124,7 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { LogMessage.setMessage(modifiedPlayer.getName() + " has joined the game!"); break; case PID_ASSIGNMENT: - player = gameMap.recreatePlayer(pathParameters.getString("id"), pathParameters.getString("name"), - Color.valueOf(pathParameters.getString("color"))); - LogMessage.setMessage(player.getName() + " has joined the game!"); - Gdx.app.postRunnable(() -> { - gameLoop.load(); - gameLoop.markClientReady(); - }); + handlePidAssignment(packet); break; case PATH_EDIT: parseDataAndApplyAction(EditPathDto.class, packet); @@ -177,12 +153,7 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { parseDataAndApplyAction(SurpassedChoseDto.class, packet); break; case SURPASSED_PROMPT: - StreamlinesLogger.logger.info("DOUBLE QUOTA UI", "Received surpassed message to start double quota"); - String powerupStr = pathParameters.getString("powerups"); - String infrastructureStr = pathParameters.getString("infrastructure"); - Arrays.stream(powerupStr.split(",")).map(InventoryItemType::valueOf).forEach(gameLoop::doubleQuotaAddItem); - Arrays.stream(infrastructureStr.split(",")).map(InventoryItemType::valueOf).forEach(gameLoop::doubleQuotaAddItem); - Gdx.app.postRunnable(() -> gameLoop.refresh("doubleQuota")); + handleSurpassedPrompt(packet); break; case NEW_BID: var newBidDto = json.fromJson(NewBidDto.class, packet); @@ -199,11 +170,13 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { break; case TOP_DOG_IDENTIFY: var dto = json.fromJson(SetTopDogDto.class, packet); + dto.apply(gameMap, true); LogMessage.setMessage(gameMap.getPlayer(dto.playerId()).getName() + " is Top Dog!"); break; case TOP_DOG_PLACE_ROUTE: - LogMessage.setMessage(gameMap.getPlayer(gameMap.getTopDog()).getName() + " stole your route!"); - parseDataAndApplyAction(TopDogPlaceRouteDto.class, packet); + var topDogPlaceRouteDto = json.fromJson(TopDogPlaceRouteDto.class, packet); + topDogPlaceRouteDto.apply(gameMap, true); + LogMessage.setMessage(gameMap.getPlayer(topDogPlaceRouteDto.victimId()).getName() + " stole your route!"); break; case TOP_DOG_PLACE_AIRPLANE: LogMessage.setMessage(gameMap.getPlayer(gameMap.getTopDog()).getName() + " stole your plane!"); @@ -221,24 +194,10 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { weatherDto.apply(gameMap, true); break; case RETURN_PASSENGER_COUNT: - String planeId = pathParameters.getString("plane_id"); - int passengerCount = pathParameters.getInt("p_count"); - gameMap.getAirplaneById(planeId).setGenericPassengerCount(passengerCount); + parseDataAndApplyAction(ReturnPassengerCountDto.class, packet); break; case UPDATE_TRAFFIC: - for (JsonValue item : pathParameters) { - String airportId = item.getString("airport"); - int trafficCount = item.getInt("count"); - String destinationEncoding = item.getString("destinations"); - String[] destinations = destinationEncoding.split("`"); - - Airport toUpdate = gameMap.getAirportById(airportId); - toUpdate.updateTrafficCount(trafficCount); - toUpdate.clearDestinations(); - for (String dest : destinations) { - toUpdate.addDestination(new Airport.DestinationInfo(dest)); - } - } + parseDataAndApplyAction(UpdateTrafficListDto.class, packet); // Refresh StatisticsUI Gdx.app.postRunnable(() -> gameLoop.getWidget("stats").refresh()); @@ -248,7 +207,7 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { LogMessage.setMessage(unassignedDto.playerId() + " has left the game!"); unassignedDto.apply(gameMap, true); if(gameMap.hasEnded()) { - Gdx.app.postRunnable(() -> gameLoop.nextStage()); + Gdx.app.postRunnable(gameLoop::nextStage); } break; default: @@ -262,6 +221,40 @@ public boolean onMessage(final WebSocket webSocket, final String packet) { } } + /** + * Handles the surpassed prompt. + * @param packet + */ + private void handleSurpassedPrompt(String packet) { + StreamlinesLogger.logger.info("DOUBLE QUOTA UI", "Received surpassed message to start double quota"); + var surpassedPromptDto = json.fromJson(SurpassedPromptDto.class, packet); + surpassedPromptDto.powerups().forEach(gameLoop::doubleQuotaAddItem); + surpassedPromptDto.infrastructure().forEach(gameLoop::doubleQuotaAddItem); + Gdx.app.postRunnable(() -> gameLoop.refresh("doubleQuota")); + } + + /// + /// @apiNote This method has side effects: + /// + /// 1. sets the player + /// + /// 2. logs a message + /// + /// 3. Queues to the main thread to + /// load the game loop and + /// mark the client as ready + /// @param packet The data + /// + private void handlePidAssignment(String packet) { + var dto = json.fromJson(PidAssignmentDto.class, packet); + player = gameMap.recreatePlayer(dto.id(), dto.name(), dto.color()); + LogMessage.setMessage(player.getName() + " has joined the game!"); + Gdx.app.postRunnable(() -> { + gameLoop.load(); + gameLoop.markClientReady(); + }); + } + @Override public void dispose() { WebSockets.closeGracefully(socket); // Null-safe closing method that catches and logs any exceptions. @@ -269,11 +262,11 @@ public void dispose() { @Override public void broadcast(Packet.PacketType type, String data) { - socket.send(new Packet(type, data).getContents()); + socket.send(new Packet(type, data).contents()); } @Override - public void broadcast(Packet.PacketType packetType, DtoActionable data) { + public void broadcast(Packet.PacketType packetType, Serializable data) { socket.send(new Packet(packetType, data)); } 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/src/main/java/io/streamlines/map/Spawner.java b/server/src/main/java/io/streamlines/map/Spawner.java index 2cf8b448..c4a6517b 100644 --- a/server/src/main/java/io/streamlines/map/Spawner.java +++ b/server/src/main/java/io/streamlines/map/Spawner.java @@ -1,18 +1,15 @@ package io.streamlines.map; import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.utils.*; +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. @@ -52,13 +49,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(); } }; @@ -119,11 +116,11 @@ public void run() { TimerTask broadcastAirportTraffic = new TimerTask() { @Override public void run() { - JsonValue packet = new JsonValue(JsonValue.ValueType.object); + UpdateTrafficListDto dto = new UpdateTrafficListDto(); for (Airport airport : getAirports()) { - packet.addChild(airport.getTrafficDetails()); + dto.add(airport.getTrafficDetails()); } - broadcaster.broadcast(Packet.PacketType.UPDATE_TRAFFIC, packet.toJson(JsonWriter.OutputType.json)); + broadcaster.broadcast(Packet.PacketType.UPDATE_TRAFFIC, dto); } }; timer.scheduleAtFixedRate(broadcastAirportTraffic, 2000, 5000); @@ -132,8 +129,7 @@ public void run() { 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); @@ -169,38 +165,42 @@ public void notifyLanding(Airplane airplane, boolean surpassed) { broadcaster.broadcast(Packet.PacketType.UNLOAD, dto); if (!surpassed) return; - JsonValue jsonValue = new JsonValue(JsonValue.ValueType.object); - 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_PROMPT, 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 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) { diff --git a/server/src/main/java/io/streamlines/network/ServerLauncher.java b/server/src/main/java/io/streamlines/network/ServerLauncher.java index 87cebf02..56694f72 100644 --- a/server/src/main/java/io/streamlines/network/ServerLauncher.java +++ b/server/src/main/java/io/streamlines/network/ServerLauncher.java @@ -1,10 +1,9 @@ package io.streamlines.network; -import com.badlogic.gdx.utils.*; +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; @@ -18,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; @@ -33,7 +31,6 @@ 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 { @@ -42,7 +39,6 @@ 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); } @@ -54,32 +50,27 @@ 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, String 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 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 -> { @@ -103,12 +94,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, DtoActionable data) { - broadcast(new Packet(packetType, data).getContents()); + public void broadcast(Packet.PacketType packetType, Serializable data) { + broadcast(new Packet(packetType, data).contents()); } /** @@ -118,7 +109,11 @@ public void broadcast(Packet.PacketType packetType, DtoActionable 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 @@ -144,7 +139,6 @@ public void onMessage(WebSocket conn, String message) { } logger.info("SERVER", "{}: {}", conn.getRemoteSocketAddress().getHostName(), message); Packet packet = new Packet(message); - JsonValue params = peerListener.readPacket(packet); if (packet.getPacketType() == null) return; // Only certain packets should be bounced back to the other clients @@ -159,25 +153,7 @@ public void onMessage(WebSocket conn, String message) { case TOP_DOG_PLACE_AIRPLANE -> parseAndApplyDto(TopDogPlaceAirplaneDto.class, message); case TOP_DOG_PLACE_ROUTE -> parseAndApplyDto(TopDogPlaceRouteDto.class, message); case TOP_DOG_IDENTIFY -> parseAndApplyDto(SetTopDogDto.class, message); - 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 REQUEST_PASSENGER_COUNT -> returnPassengerCount(message, conn); case SURPASSED_CHOSE -> parseAndApplyDto(SurpassedChoseDto.class, message); case PLAYER_NAME -> setUpNewPlayer(conn, packet.parse().getString("pname")); default -> false; @@ -187,18 +163,28 @@ public void onMessage(WebSocket conn, String message) { } } - public static InventoryItemType randomItemRequest(Random random, Class 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())); - } - 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 public void onMessage(WebSocket conn, ByteBuffer message) { broadcast(message.array()); @@ -210,6 +196,7 @@ 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]); @@ -226,7 +213,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!"); @@ -264,12 +251,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/PlayerTest.java index d1e8487c..72aa81cc 100644 --- a/server/src/test/java/io/streamlines/map/PlayerTest.java +++ b/server/src/test/java/io/streamlines/map/PlayerTest.java @@ -3,9 +3,12 @@ import com.badlogic.gdx.math.Vector2; import io.streamlines.flight.Airport; import io.streamlines.flight.PathService; -import io.streamlines.network.*; +import io.streamlines.network.Broadcastable; +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; @@ -41,7 +44,7 @@ public void broadcast(String data) {} public void broadcast(Packet.PacketType packetType, String data) {} @Override - public void broadcast(Packet.PacketType packetType, DtoActionable data) { + public void broadcast(Packet.PacketType packetType, Serializable data) { } }); Player player = gameMap.newPlayer(PLAYER_ID); @@ -122,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/src/main/java/io/streamlines/flight/Airplane.java b/shared/src/main/java/io/streamlines/flight/Airplane.java index 20749f41..7587f03c 100644 --- a/shared/src/main/java/io/streamlines/flight/Airplane.java +++ b/shared/src/main/java/io/streamlines/flight/Airplane.java @@ -261,19 +261,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..d87e9c42 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; @@ -55,43 +56,12 @@ public void mutatePopularity() { private transient final List finishPendingObservers; private int currentTrafficCount; - public static class DestinationInfo { - private final String airportCode; - private final int popularity; - - 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]); - } - - public String encode() { - return airportCode + "~" + popularity; - } - - public String getAirportCode() { - return airportCode; - } - - public int getPopularity() { - return popularity; - } - - @Override - public boolean equals(Object obj) { - return airportCode.equals(((DestinationInfo) obj).airportCode); + public record DestinationInfo(String airportCode, int popularity) { + @Override + public boolean equals(Object obj) { + return obj instanceof DestinationInfo && airportCode.equals(((DestinationInfo) obj).airportCode); + } } - } private transient final List rankedDestinations = new ArrayList<>(); public DestinationInfo[] getRankedDestinations() { @@ -321,7 +291,7 @@ public void clearWeatherEvent() { weatherStatus = WeatherEventSeverity.FINE; } - public JsonValue getTrafficDetails() { + public UpdateTraffic getTrafficDetails() { ArrayList allPassengers = new ArrayList<>(unassignedPassengers); allPassengers.addAll(isolatedPassengers); @@ -340,17 +310,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 74f4d9fb..d384877b 100644 --- a/shared/src/main/java/io/streamlines/flight/PathService.java +++ b/shared/src/main/java/io/streamlines/flight/PathService.java @@ -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 should 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) : diff --git a/shared/src/main/java/io/streamlines/inventory/Inventory.java b/shared/src/main/java/io/streamlines/inventory/Inventory.java index 6f13fa21..a5addfd4 100644 --- a/shared/src/main/java/io/streamlines/inventory/Inventory.java +++ b/shared/src/main/java/io/streamlines/inventory/Inventory.java @@ -114,12 +114,10 @@ public boolean returnItem(InfrastructureItem 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 - */ + /// Adds item to inventory. Use the .class 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); } diff --git a/shared/src/main/java/io/streamlines/map/GameMap.java b/shared/src/main/java/io/streamlines/map/GameMap.java index 97ae28cb..dd99d822 100644 --- a/shared/src/main/java/io/streamlines/map/GameMap.java +++ b/shared/src/main/java/io/streamlines/map/GameMap.java @@ -78,17 +78,11 @@ public PathService getPathServiceById(String id, String owner) { * @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) -> { 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..dcaf843e 100644 --- a/shared/src/main/java/io/streamlines/map/Player.java +++ b/shared/src/main/java/io/streamlines/map/Player.java @@ -199,6 +199,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. diff --git a/shared/src/main/java/io/streamlines/network/AirplaneSystemActionDto.java b/shared/src/main/java/io/streamlines/network/AirplaneSystemActionDto.java index 7840d21c..933b0ed4 100644 --- a/shared/src/main/java/io/streamlines/network/AirplaneSystemActionDto.java +++ b/shared/src/main/java/io/streamlines/network/AirplaneSystemActionDto.java @@ -1,5 +1,6 @@ package io.streamlines.network; +import io.streamlines.flight.Airplane; import io.streamlines.map.GameMap; public record AirplaneSystemActionDto(String airplaneId, AirplaneSystemAction action) @@ -8,7 +9,7 @@ public record AirplaneSystemActionDto(String airplaneId, AirplaneSystemAction ac @Override public Boolean apply(GameMap gameMap, Boolean isClient) { if (action == AirplaneSystemAction.TAKEOFF) { - gameMap.getAirplaneById(airplaneId).takeoff(); + gameMap.getAirplaneById(airplaneId).ifPresent(Airplane::takeoff); } return false; } diff --git a/shared/src/main/java/io/streamlines/network/AirplaneUserInventoryActionDto.java b/shared/src/main/java/io/streamlines/network/AirplaneUserInventoryActionDto.java index 106a4a38..e1a5071e 100644 --- a/shared/src/main/java/io/streamlines/network/AirplaneUserInventoryActionDto.java +++ b/shared/src/main/java/io/streamlines/network/AirplaneUserInventoryActionDto.java @@ -5,7 +5,6 @@ import io.streamlines.flight.*; import io.streamlines.inventory.*; import io.streamlines.map.GameMap; -import io.streamlines.map.Player; import java.util.Optional; @@ -15,17 +14,27 @@ public record AirplaneUserInventoryActionDto(String playerId, String airplaneId, Vector2 position, String pathId, InventoryItemType action) implements DtoActionable { + private static final String AIRPLANE_NOT_FOUND_MESSAGE = "Airplane not found"; + @Override public Boolean apply(GameMap gameMap, Boolean isClient) { - Player player = gameMap.getPlayer(playerId); + var player = gameMap.getPlayer(playerId); + + if (action == InventoryItemType.Expand_Airplane) { - player.getInventory().useItem(new ExpandAirplaneCommand( - gameMap.getAirplaneById(airplaneId) - )); + var 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) { - gameMap.getPlayer(playerId).getInventory().useItem(new ResilienceCommand( - gameMap.getAirplaneById(airplaneId) - )); + var airplane = gameMap.getAirplaneById(airplaneId); + if (airplane.isEmpty()) { + StreamlinesLogger.logger.error("DTO", AIRPLANE_NOT_FOUND_MESSAGE); + return false; + } + gameMap.getPlayer(playerId).getInventory().useItem(new ResilienceCommand(airplane.get())); } else if (action == InventoryItemType.Place_Airplane) { Optional onPathOpt = player.getPathById(pathId); if (onPathOpt.isEmpty()) { 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..34b24cc7 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/AirportListDto.java @@ -0,0 +1,20 @@ +package io.streamlines.network; + +import io.streamlines.StreamlinesLogger; +import io.streamlines.flight.Airport; +import io.streamlines.map.GameMap; + +/** + * New data found to ADD TO old airport data (less error-prone than clearing & replacing) + * @param airports + */ +public record AirportListDto(Airport[] airports) implements DtoActionable { + @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; + } +} diff --git a/shared/src/main/java/io/streamlines/network/Broadcastable.java b/shared/src/main/java/io/streamlines/network/Broadcastable.java index 08bc57bf..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. */ @@ -22,5 +24,5 @@ public interface Broadcastable { */ void broadcast(Packet.PacketType packetType, String data); - void broadcast(Packet.PacketType packetType, DtoActionable 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 index d8e9e94a..8e42cb27 100644 --- a/shared/src/main/java/io/streamlines/network/DtoActionable.java +++ b/shared/src/main/java/io/streamlines/network/DtoActionable.java @@ -8,8 +8,7 @@ /** * For all data transfer objects. Takes in a game map and outputs whether to bounce */ -public interface DtoActionable - extends Serializable, BiFunction { +public interface DtoActionable extends Serializable, BiFunction { @Override Boolean apply(GameMap gameMap, Boolean isClient); } diff --git a/shared/src/main/java/io/streamlines/network/Packet.java b/shared/src/main/java/io/streamlines/network/Packet.java index e3986f89..875037e6 100644 --- a/shared/src/main/java/io/streamlines/network/Packet.java +++ b/shared/src/main/java/io/streamlines/network/Packet.java @@ -2,15 +2,16 @@ import com.badlogic.gdx.utils.*; +import java.io.Serializable; + /** * Packets are sent with the following format: * PACKET_TYPE=Data * Every broadcast can only have ONE packet type. Parameters use param=value format but SHOULD NOT be included in * PacketType enumeration. But there can be name overlap (for example a parameter called player and a packet type * called player). - * @apiNote Must be used with the corresponding DTO */ -public final class Packet { +public record Packet(String contents) { private static final JsonReader jsonReader; private static final Json json; @@ -41,46 +42,43 @@ public enum PacketType { public PacketType getPacketType() { for (PacketType packetType : PacketType.values()) { - if (contents.startsWith(packetType.name() + "=")) + if (contents.startsWith(packetType.name() + "=")) { return packetType; + } } return null; } - private transient 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 { } /** * 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, DtoActionable data) { - this.contents = type.callsign + "=" + json.toJson(data); + 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()); } public JsonValue parse() { return jsonReader.parse(getData()); } - - public String getContents() { - return contents; - } } 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 41f0bc5f..00000000 --- a/shared/src/main/java/io/streamlines/network/PeerListener.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.streamlines.network; - -import com.badlogic.gdx.utils.JsonValue; -import io.streamlines.map.GameMap; - -/** - * 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; - - public PeerListener(GameMap map) { - gameMap = map; - } - - public GameMap getGameMap() { - return gameMap; - } - - /** - * Parses parameters from packet - * @param packet The packet object containing JSON-serialized key-value pairs - */ - JsonValue readPacket(Packet packet) { - return packet.parse(); - } - -} 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..22d6f7be --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/PidAssignmentDto.java @@ -0,0 +1,7 @@ +package io.streamlines.network; + +import com.badlogic.gdx.graphics.Color; + +import java.io.Serializable; + +public record PidAssignmentDto(String id, String name, Color color) implements Serializable { } 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..e019e0e6 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/RequestPassengerCountDto.java @@ -0,0 +1,8 @@ +package io.streamlines.network; + +import java.io.Serializable; + +/// The server also verifies that this player is authorized to see the passenger counts (owns the +/// plane) +public record RequestPassengerCountDto(String planeId, String player) implements Serializable { +} 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..2bc73fc4 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/ReturnPassengerCountDto.java @@ -0,0 +1,16 @@ +package io.streamlines.network; + +import io.streamlines.map.GameMap; + +/** + * Returns the passenger count to the specific player who requested it. Does not bounce. + * @param planeId The id of the plane + * @param passengerCount The number of passengers for that plane + */ +public record ReturnPassengerCountDto(String planeId, int passengerCount) implements DtoActionable { + @Override + public Boolean apply(GameMap gameMap, Boolean isClient) { + gameMap.getAirplaneById(planeId).ifPresent(airplane -> airplane.setGenericPassengerCount(passengerCount)); + return false; + } +} 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..7267a609 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/SurpassedPromptDto.java @@ -0,0 +1,8 @@ +package io.streamlines.network; + +import io.streamlines.inventory.InventoryItemType; + +import java.io.Serializable; +import java.util.List; + +public record SurpassedPromptDto(List powerups, List infrastructure) implements Serializable { } diff --git a/shared/src/main/java/io/streamlines/network/TopDogPlaceAirplaneDto.java b/shared/src/main/java/io/streamlines/network/TopDogPlaceAirplaneDto.java index 0134c391..11431741 100644 --- a/shared/src/main/java/io/streamlines/network/TopDogPlaceAirplaneDto.java +++ b/shared/src/main/java/io/streamlines/network/TopDogPlaceAirplaneDto.java @@ -16,10 +16,11 @@ public Boolean apply(GameMap gameMap, Boolean isClient) { return false; } - if (victim.getPathById(pathId).isEmpty()) { + if (victim.getPathById(pathId).isEmpty() || gameMap.getAirplaneById(airplaneId).isEmpty()) { return false; } - var command = new PlaceAirplaneCommand(gameMap.getAirplaneById(airplaneId), victim.getPathById(pathId).get()); + var command = new PlaceAirplaneCommand(gameMap.getAirplaneById(airplaneId).get(), + victim.getPathById(pathId).get()); gameMap.getPlayer(gameMap.getTopDog()).getInventory().useItem(new TopDogCommand(victim.getId(), command)); 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..1ce223b8 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/UpdateTraffic.java @@ -0,0 +1,15 @@ +package io.streamlines.network; + +import io.streamlines.flight.Airport; +import io.streamlines.map.GameMap; + +import java.util.List; + +public record UpdateTraffic(String airport, int trafficCount, List destinations) { + void accept(GameMap gameMap) { + Airport toUpdate = gameMap.getAirportById(airport()); + toUpdate.updateTrafficCount(trafficCount()); + toUpdate.clearDestinations(); + destinations().forEach(toUpdate::addDestination); + } +} 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..332b09a2 --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/UpdateTrafficListDto.java @@ -0,0 +1,14 @@ +package io.streamlines.network; + +import com.badlogic.gdx.utils.Array; +import io.streamlines.map.GameMap; + +public class UpdateTrafficListDto extends Array implements DtoActionable { + @Override + public Boolean apply(GameMap gameMap, Boolean isClient) { + if (isClient) { + forEach(x -> x.accept(gameMap)); + } + return false; + } +} From a1a393c7e66e0648930b658bd72ea91359c7fdce Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Fri, 22 Aug 2025 13:28:16 -0400 Subject: [PATCH 05/46] Make playername a DTO record - last use of json value outside of player parse --- core/src/main/java/io/streamlines/GameLoop.java | 12 ++++-------- .../java/io/streamlines/network/ServerLauncher.java | 7 ++++--- .../java/io/streamlines/network/PlayerNameDto.java | 5 +++++ 3 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 shared/src/main/java/io/streamlines/network/PlayerNameDto.java diff --git a/core/src/main/java/io/streamlines/GameLoop.java b/core/src/main/java/io/streamlines/GameLoop.java index f3dcce01..2cfb67dc 100644 --- a/core/src/main/java/io/streamlines/GameLoop.java +++ b/core/src/main/java/io/streamlines/GameLoop.java @@ -4,18 +4,15 @@ import com.badlogic.gdx.graphics.*; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; -import com.badlogic.gdx.utils.*; +import com.badlogic.gdx.utils.ScreenUtils; import com.badlogic.gdx.utils.viewport.FitViewport; import com.badlogic.gdx.utils.viewport.Viewport; import io.streamlines.UI.*; import io.streamlines.inventory.InventoryItemType; import io.streamlines.map.*; -import io.streamlines.network.Client; -import io.streamlines.network.Packet; +import io.streamlines.network.*; -import java.lang.StringBuilder; import java.util.*; -import java.util.Queue; import static io.streamlines.map.GameMapAccessible.VIRTUAL_HEIGHT; import static io.streamlines.map.GameMapAccessible.VIRTUAL_WIDTH; @@ -262,9 +259,8 @@ public void nextStage() { gameState = GameStates.SERVER_INITIALIZATION; lobbyUI.getStage().stopInput(); } else if (gameState == GameStates.SERVER_INITIALIZATION) { - JsonValue value = new JsonValue(JsonValue.ValueType.object); - value.addChild("pname", new JsonValue(lobbyUI.getUsername())); - gameClient.broadcast(Packet.PacketType.PLAYER_NAME, value.toJson(JsonWriter.OutputType.json)); + var usernameInfo = new PlayerNameDto(lobbyUI.getUsername()); + gameClient.broadcast(Packet.PacketType.PLAYER_NAME, usernameInfo); gameState = GameStates.ASSET_INITIALIZATION; } else { gameState = GameStates.values()[gameState.ordinal() + 1]; diff --git a/server/src/main/java/io/streamlines/network/ServerLauncher.java b/server/src/main/java/io/streamlines/network/ServerLauncher.java index 56694f72..bd861e33 100644 --- a/server/src/main/java/io/streamlines/network/ServerLauncher.java +++ b/server/src/main/java/io/streamlines/network/ServerLauncher.java @@ -56,7 +56,8 @@ public void onOpen(WebSocket conn, ClientHandshake handshake) { sendPacketTo(Packet.PacketType.DAY_DATA, newDayDto, conn); } - private 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(); @@ -155,7 +156,7 @@ public void onMessage(WebSocket conn, String message) { case TOP_DOG_IDENTIFY -> parseAndApplyDto(SetTopDogDto.class, message); case REQUEST_PASSENGER_COUNT -> returnPassengerCount(message, conn); case SURPASSED_CHOSE -> parseAndApplyDto(SurpassedChoseDto.class, message); - case PLAYER_NAME -> setUpNewPlayer(conn, packet.parse().getString("pname")); + case PLAYER_NAME -> setUpNewPlayer(conn, json.fromJson(PlayerNameDto.class, message)); default -> false; }; if (bounce) { @@ -163,7 +164,7 @@ public void onMessage(WebSocket conn, String message) { } } - boolean parseAndApplyDto(Class type, String data) { + private boolean parseAndApplyDto(Class type, String data) { return json.fromJson(type, data).apply(gameMap, false); } 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..73cf722b --- /dev/null +++ b/shared/src/main/java/io/streamlines/network/PlayerNameDto.java @@ -0,0 +1,5 @@ +package io.streamlines.network; + +import java.io.Serializable; + +public record PlayerNameDto(String name) implements Serializable { } From d305c61a64ea56c2f488158f72da55b8b6ce5b62 Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Sun, 24 Aug 2025 11:17:27 -0400 Subject: [PATCH 06/46] Write integration tests for newday and airportlist dtos --- html/build.gradle | 4 +- .../streamlines/network/ServerLauncher.java | 3 +- .../java/io/streamlines/flight/Airport.java | 7 +- .../main/java/io/streamlines/map/GameMap.java | 4 +- .../io/streamlines/network/NewDayDto.java | 5 ++ .../streamlines/network/PeerListenerTest.java | 24 ------ .../streamlines/network/PeerMessageTest.java | 75 +++++++++++++++++++ 7 files changed, 92 insertions(+), 30 deletions(-) delete mode 100644 shared/src/test/java/io/streamlines/network/PeerListenerTest.java create mode 100644 shared/src/test/java/io/streamlines/network/PeerMessageTest.java diff --git a/html/build.gradle b/html/build.gradle index b453d440..8b370d8f 100644 --- a/html/build.gradle +++ b/html/build.gradle @@ -151,7 +151,7 @@ tasks.compileGwt.dependsOn(addSource) tasks.draftCompileGwt.dependsOn(addSource) tasks.checkGwt.dependsOn(addSource) -java.sourceCompatibility = JavaVersion.VERSION_17 -java.targetCompatibility = JavaVersion.VERSION_17 +java.sourceCompatibility = clientJdkVersion +java.targetCompatibility = clientJdkVersion sourceSets.main.java.srcDirs = [ "src/main/java/" ] diff --git a/server/src/main/java/io/streamlines/network/ServerLauncher.java b/server/src/main/java/io/streamlines/network/ServerLauncher.java index bd861e33..21e67c29 100644 --- a/server/src/main/java/io/streamlines/network/ServerLauncher.java +++ b/server/src/main/java/io/streamlines/network/ServerLauncher.java @@ -71,7 +71,8 @@ private boolean setUpNewPlayer(WebSocket conn, PlayerNameDto userInfo) { gameMap.assignPlayerInitialTerminals(PID); } // Broadcast to everyone so other players can see this one's initial game state - broadcast(Packet.PacketType.AIRPORT_DATA, new AirportListDto(gameMap.getAirports().toArray(Airport[]::new))); + 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 -> { diff --git a/shared/src/main/java/io/streamlines/flight/Airport.java b/shared/src/main/java/io/streamlines/flight/Airport.java index d87e9c42..a27a2bbd 100644 --- a/shared/src/main/java/io/streamlines/flight/Airport.java +++ b/shared/src/main/java/io/streamlines/flight/Airport.java @@ -16,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 diff --git a/shared/src/main/java/io/streamlines/map/GameMap.java b/shared/src/main/java/io/streamlines/map/GameMap.java index dd99d822..da3555b1 100644 --- a/shared/src/main/java/io/streamlines/map/GameMap.java +++ b/shared/src/main/java/io/streamlines/map/GameMap.java @@ -136,7 +136,8 @@ public int nextDay() { /** * 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; @@ -238,7 +239,6 @@ public void losePlayer(String id) { if (!players.containsKey(id)) return; players.get(id).dispose(); updatePlayersIterable(); - } /** diff --git a/shared/src/main/java/io/streamlines/network/NewDayDto.java b/shared/src/main/java/io/streamlines/network/NewDayDto.java index 250caabf..3352dd7f 100644 --- a/shared/src/main/java/io/streamlines/network/NewDayDto.java +++ b/shared/src/main/java/io/streamlines/network/NewDayDto.java @@ -4,6 +4,11 @@ import io.streamlines.StreamlinesLogger; import io.streamlines.map.GameMap; +/** + * @see GameMap#startNewDay(int) + * @param dayNumber The day + * @param time The offset time due to network delays to keep time synced across peers + */ public record NewDayDto(int dayNumber, int time) implements DtoActionable { @Override public Boolean apply(GameMap gameMap, Boolean isClient) { 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 5866d76d..00000000 --- a/shared/src/test/java/io/streamlines/network/PeerListenerTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.streamlines.network; - -import io.streamlines.map.GameMap; -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 final static String PLAYER_ID = "abc"; - - @BeforeEach - public void initialPlayerGameSetup() { - gameMap = new GameMap(); - } - @Test - public void testHandlePlayerLoss_removesAllPlayerResources() { - new LosePlayerDto(PLAYER_ID).apply(gameMap, false); - - 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..52e01fea --- /dev/null +++ b/shared/src/test/java/io/streamlines/network/PeerMessageTest.java @@ -0,0 +1,75 @@ +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.BeforeEach; +import org.junit.jupiter.api.Test; +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.mock; +import static org.mockito.Mockito.verify; + +/** + * Integration tests on data transfer objects when messages are sent over websockets + */ +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(); + } + + @Test + public void testLosePlayer_apply_removesAllPlayerResources() { + gameMap.newPlayer(PLAYER_ID); + 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); + } +} From c6c6dd6960b96c180df989dbcd4dc9fa50b1a556 Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Sun, 24 Aug 2025 13:01:23 -0400 Subject: [PATCH 07/46] rollback build.gradle change and upgrade GWT --- gradle.properties | 2 +- html/build.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index b5b78e64..35c7cefc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.daemon=true org.gradle.jvmargs=-Xms512M -Xmx1G org.gradle.configureondemand=false -gwtFrameworkVersion=2.12.1 +gwtFrameworkVersion=2.12.2 gwtPluginVersion=1.1.29 graalHelperVersion=2.0.1 enableGraalNative=false diff --git a/html/build.gradle b/html/build.gradle index 8b370d8f..b453d440 100644 --- a/html/build.gradle +++ b/html/build.gradle @@ -151,7 +151,7 @@ tasks.compileGwt.dependsOn(addSource) tasks.draftCompileGwt.dependsOn(addSource) tasks.checkGwt.dependsOn(addSource) -java.sourceCompatibility = clientJdkVersion -java.targetCompatibility = clientJdkVersion +java.sourceCompatibility = JavaVersion.VERSION_17 +java.targetCompatibility = JavaVersion.VERSION_17 sourceSets.main.java.srcDirs = [ "src/main/java/" ] From 450a4a69cf821f99c5dbeef451c22d945932e5c5 Mon Sep 17 00:00:00 2001 From: Varun Singh Date: Mon, 25 Aug 2025 11:31:29 -0400 Subject: [PATCH 08/46] Use Array more --- .run/Run Web App.run.xml | 4 +- build.gradle | 16 -- core/build.gradle | 2 +- .../java/io/streamlines/UI/AirportUI.java | 6 +- gradle.properties | 2 +- gradle/libs.versions.toml | 2 + html/build.gradle | 154 +++++------------- .../java/io/streamlines/GdxDefinition.gwt.xml | 5 +- .../streamlines/GdxDefinitionSuperdev.gwt.xml | 2 +- .../java/io/streamlines/flight/Airport.java | 6 +- .../streamlines/network/AirportListDto.java | 3 +- .../io/streamlines/network/UpdateTraffic.java | 5 +- .../streamlines/network/PeerMessageTest.java | 5 +- 13 files changed, 61 insertions(+), 151 deletions(-) diff --git a/.run/Run Web App.run.xml b/.run/Run Web App.run.xml index 2932db08..64520def 100644 --- a/.run/Run Web App.run.xml +++ b/.run/Run Web App.run.xml @@ -10,7 +10,7 @@