365 lines
14 KiB
Java
365 lines
14 KiB
Java
package uulm.teamname.marvelous.server.lobby;
|
|
|
|
import org.tinylog.Logger;
|
|
import uulm.teamname.marvelous.gamelibrary.entities.EntityID;
|
|
import uulm.teamname.marvelous.gamelibrary.entities.EntityType;
|
|
import uulm.teamname.marvelous.gamelibrary.events.Event;
|
|
import uulm.teamname.marvelous.gamelibrary.events.EventBuilder;
|
|
import uulm.teamname.marvelous.gamelibrary.events.EventType;
|
|
import uulm.teamname.marvelous.gamelibrary.gamelogic.GameInstance;
|
|
import uulm.teamname.marvelous.gamelibrary.messages.ParticipantType;
|
|
import uulm.teamname.marvelous.gamelibrary.config.CharacterConfig;
|
|
import uulm.teamname.marvelous.gamelibrary.config.PartyConfig;
|
|
import uulm.teamname.marvelous.gamelibrary.config.ScenarioConfig;
|
|
import uulm.teamname.marvelous.gamelibrary.requests.Request;
|
|
import uulm.teamname.marvelous.server.lobby.pipelining.*;
|
|
import uulm.teamname.marvelous.server.lobbymanager.LobbyConnection;
|
|
import uulm.teamname.marvelous.server.lobbymanager.LobbyManager;
|
|
import uulm.teamname.marvelous.server.lobbymanager.Participant;
|
|
|
|
import java.util.*;
|
|
|
|
public class Lobby {
|
|
private final String gameID;
|
|
private final LobbyConnection connection;
|
|
private final GameInstance game;
|
|
private final Pipeline pipeline;
|
|
private Participant activePlayer;
|
|
private int badRequests;
|
|
private PauseSegment pauseSegment;
|
|
private final TurnTimeoutTimer turnTimeoutTimer;
|
|
private final TimeoutTimer timeoutTimer;
|
|
private final LifetimeTimer lifetimeTimer;
|
|
|
|
/**
|
|
* The {@link Lobby} is where the magic happens. In this class is a whole {@link GameInstance game} is processed. To
|
|
* initialize the game it gets the following parameters
|
|
*
|
|
* @param gameID a String to identify the game
|
|
* @param connection the Connection to the {@link LobbyManager}
|
|
* @param partyConfig declared in Editor
|
|
* @param characterConfig declared in Editor
|
|
* @param scenarioConfig declared in Editor
|
|
*/
|
|
public Lobby(
|
|
String gameID,
|
|
LobbyConnection connection,
|
|
PartyConfig partyConfig,
|
|
CharacterConfig characterConfig,
|
|
ScenarioConfig scenarioConfig,
|
|
List<Integer> player1Characters,
|
|
List<Integer> player2Characters
|
|
) {
|
|
this.gameID = gameID;
|
|
this.connection = connection;
|
|
|
|
this.game = new GameInstance(
|
|
partyConfig,
|
|
characterConfig,
|
|
scenarioConfig
|
|
);
|
|
|
|
this.pipeline = new Pipeline();
|
|
|
|
var reqSegment = new RequestGameStateSegment(this.game);
|
|
this.pauseSegment = new PauseSegment();
|
|
var filterEndRoundRequestSegment = new FilterEndRoundRequestSegment(game.state::getActiveCharacter);
|
|
var disconnectSegment = new DisconnectSegment(this);
|
|
var playerFilterSegment = new PlayerFilterSegment();
|
|
var gameLogicSegment = new GameLogicSegment(this.game);
|
|
|
|
pipeline.addSegment(reqSegment)
|
|
.addSegment(pauseSegment)
|
|
.addSegment(filterEndRoundRequestSegment)
|
|
.addSegment(disconnectSegment)
|
|
.addSegment(playerFilterSegment)
|
|
.addSegment(gameLogicSegment);
|
|
|
|
Logger.trace("Instantiating timers...");
|
|
|
|
this.turnTimeoutTimer = new TurnTimeoutTimer(partyConfig.maxRoundTime, this::turnTimeout);
|
|
updateTurnTimer();
|
|
|
|
this.timeoutTimer = new TimeoutTimer(partyConfig.maxResponseTime, this::soonTimeout, this::timeout);
|
|
refreshTimeoutTimer(connection.getPlayer1());
|
|
refreshTimeoutTimer(connection.getPlayer2());
|
|
|
|
this.lifetimeTimer = new LifetimeTimer(
|
|
partyConfig.maxGameTime,
|
|
() -> triggerWin(getParticipantForEntityType(game.state.getCurrentOvertimeWinner()).get()));
|
|
|
|
this.connection.broadcastEvents(this.game.startGame(player1Characters, player2Characters));
|
|
|
|
}
|
|
|
|
/**
|
|
* Called by {@link LobbyConnection} to handle requests
|
|
*
|
|
* @param requests to be processed
|
|
* @param source the player executing the requests
|
|
*/
|
|
public synchronized void receiveRequests(Request[] requests, Participant source) {
|
|
Logger.trace("Received {} requests from participant '{}' of type {}",
|
|
requests.length,
|
|
source.id.getName(),
|
|
source.type);
|
|
|
|
refreshTimeoutTimer(source);
|
|
|
|
if (activePlayer != source && source.type != ParticipantType.Spectator) {
|
|
Logger.trace("Resetting bad requests as new participant sent data");
|
|
activePlayer = source;
|
|
badRequests = 0;
|
|
}
|
|
|
|
Logger.info("got {} requests from participant {}",
|
|
requests.length,
|
|
source.id.getName());
|
|
|
|
Logger.trace("Processing requests through pipeline");
|
|
Optional<List<Event>> resultingEvents = pipeline.processRequests(requests, source);
|
|
Logger.debug("generated {} events from the pipeline", resultingEvents.map(List::size).orElse(0));
|
|
|
|
//resultingEvents isEmpty when a wrong request appeared
|
|
Logger.trace("Checking whether resultingEvents (an optional) is empty");
|
|
if (resultingEvents.isEmpty()) {
|
|
Logger.debug("Rejecting requests from participant '{}'", source.id.getName());
|
|
reject(source);
|
|
} else {
|
|
if (game.state.isWon()) { // If game was won in the current turn
|
|
Logger.info("Game is won, terminating lobby");
|
|
var events = resultingEvents.get();
|
|
events.add(new EventBuilder(EventType.DisconnectEvent).buildGameEvent());
|
|
accept(source, events);
|
|
connection.terminate();
|
|
return;
|
|
} else { // If not (normally)
|
|
accept(source, resultingEvents.get());
|
|
}
|
|
}
|
|
updateTurnTimer();
|
|
}
|
|
|
|
/**
|
|
* Called by {@link LobbyConnection} when a client disconnects
|
|
*
|
|
* @param source the player disconnecting
|
|
*/
|
|
public synchronized void handleDisconnect(Participant source) {
|
|
|
|
}
|
|
|
|
/**
|
|
* Called by {@link LobbyConnection} when a client reconnects
|
|
*
|
|
* @param source the player reconnecting
|
|
*/
|
|
public synchronized void handleReconnect(Participant source) {
|
|
|
|
}
|
|
|
|
/**
|
|
* This method is called at the end of receiveRequests, to start a timer. The active player has now a specific
|
|
* amount of time to do his moves.
|
|
*/
|
|
void updateTurnTimer() {
|
|
var currentlyActiveParticipant =
|
|
getParticipantForEntityType(game.state.getActiveCharacter());
|
|
Logger.trace("Updating turnTimer...");
|
|
if (pauseSegment.isPaused()) {
|
|
Logger.trace("Game is paused, clearing turnTimer");
|
|
turnTimeoutTimer.clear();
|
|
|
|
} else if (currentlyActiveParticipant.isPresent()) {
|
|
var participant = currentlyActiveParticipant.get();
|
|
Logger.trace("Scheduling turnTimer for Player1");
|
|
turnTimeoutTimer.startTurnTimer(participant);
|
|
|
|
} else {
|
|
Logger.trace("Currently active participant was NPC, clearing TurnTimer");
|
|
turnTimeoutTimer.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns an {@link Optional} of the {@link Participant} for the given {@link EntityType}, or an empty {@link
|
|
* Optional} if it was an NPC
|
|
*/
|
|
Optional<Participant> getParticipantForEntityType(EntityID entityID) {
|
|
if (entityID == null) {
|
|
Logger.trace("cannot get participant for empty EntityType, returning empty Optional");
|
|
return Optional.empty();
|
|
} else {
|
|
return getParticipantForEntityType(entityID.type);
|
|
}
|
|
}
|
|
|
|
Optional<Participant> getParticipantForEntityType(EntityType type) {
|
|
if (type == EntityType.P1) {
|
|
return Optional.ofNullable(connection.getPlayer1());
|
|
} else if (type == EntityType.P2) {
|
|
return Optional.ofNullable(connection.getPlayer2());
|
|
} else {
|
|
return Optional.empty();
|
|
}
|
|
}
|
|
|
|
/** Method meant for updating a TurnTimer. Said TurnTimer will be refreshed with the given participant. */
|
|
void refreshTimeoutTimer(Participant participant) {
|
|
if (participant.type == ParticipantType.Spectator) {
|
|
Logger.trace("Tried to refresh timeoutTimer for Spectator, ignoring...");
|
|
} else {
|
|
Logger.debug("Refreshing timeoutTimer for Participant '{}'", participant.id.getName());
|
|
timeoutTimer.refreshTimer(participant);
|
|
}
|
|
|
|
}
|
|
|
|
private void accept(Participant source, List<Event> accepted) {
|
|
if (!accepted.isEmpty()) {
|
|
Logger.debug("Accepting requests from participant '{}', broadcasting events to all except source",
|
|
source.id.getName());
|
|
accepted.add(game.getGameStateEvent());
|
|
connection.broadcastToAllExcept(source, accepted.toArray(new Event[0]));
|
|
|
|
Logger.trace("Adding ack and sending back to originParticipant");
|
|
accepted.add(0, new EventBuilder(EventType.Ack).buildGameEvent());
|
|
connection.sendEvents(source, accepted.toArray(new Event[0]));
|
|
}
|
|
badRequests = 0;
|
|
}
|
|
|
|
/**
|
|
* If the player executed a false request the request gets rejected.
|
|
*
|
|
* @param source the executing player
|
|
*/
|
|
private void reject(Participant source) {
|
|
connection.sendEvents(source, new EventBuilder(EventType.Nack).buildGameEvent(), game.getGameStateEvent());
|
|
badRequests++;
|
|
//if the player sends 2 bad messages after one another, the player gets kicked out of the lobby.
|
|
if (badRequests >= 5) {
|
|
Logger.info("Participant '{}' has sent too many bad requests, disconnecting...", source.id.getName());
|
|
connection.removeParticipant(source);
|
|
if (connection.hasPlayer1()) {
|
|
Logger.debug("Triggering win for Player 1");
|
|
triggerWin(connection.getPlayer1());
|
|
} else if (connection.hasPlayer2()) {
|
|
Logger.debug("Triggering win for Player 2");
|
|
triggerWin(connection.getPlayer2());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Warns the player he get timeouted soon.
|
|
*
|
|
* @param source soon to be timeouted player
|
|
*/
|
|
public synchronized void soonTimeout(Participant source, int timeLeft) {
|
|
connection.sendEvents(
|
|
source,
|
|
new EventBuilder(EventType.TimeoutWarningEvent)
|
|
.withTimeLeft(timeLeft)
|
|
.withMessage("If you do not send a message soon, you will be time-outed")
|
|
.buildGameEvent());
|
|
}
|
|
|
|
/**
|
|
* If a player times out the other player automatically wins.
|
|
*
|
|
* @param source is the timeouted player
|
|
*/
|
|
public synchronized void timeout(Participant source) {
|
|
connection.sendEvents(
|
|
source,
|
|
new EventBuilder(EventType.TimeoutEvent).buildGameEvent());
|
|
connection.removeParticipant(source);
|
|
|
|
if (connection.hasPlayer1() && !connection.hasPlayer2()) {
|
|
triggerWin(connection.getPlayer1());
|
|
} else if (!connection.hasPlayer1() && connection.hasPlayer2()) {
|
|
triggerWin(connection.getPlayer2());
|
|
} else {
|
|
throw new IllegalStateException("Spectator was time-outed which is impossible");
|
|
}
|
|
}
|
|
|
|
/** Skips the current turn, and starts a new one. Exclusively called in the {@link TurnTimeoutTimer}. */
|
|
private synchronized void turnTimeout(Participant source) {
|
|
var nextTurnEvents = game.endTurn();
|
|
nextTurnEvents.add(game.getGameStateEvent());
|
|
connection.broadcastEvents(nextTurnEvents.toArray(new Event[0]));
|
|
updateTurnTimer();
|
|
}
|
|
|
|
/**
|
|
* The method triggers a winEvent for a {@link Participant}, and broadcasts said event. Afterwards it sends a
|
|
* DisconnectRequest to everyone, and terminates the connection.
|
|
*
|
|
* @param winner is the {@link Participant} that won
|
|
*/
|
|
public synchronized void triggerWin(Participant winner) {
|
|
Logger.debug("Triggering win. Building events and broadcasting for winner of type '{}'", winner);
|
|
connection.broadcastEvents(
|
|
new EventBuilder(EventType.WinEvent)
|
|
.withPlayerWon(winner.type.equals(ParticipantType.PlayerOne) ? 1 : 2)
|
|
.buildGameEvent(),
|
|
new EventBuilder(EventType.DisconnectEvent)
|
|
.buildGameEvent());
|
|
connection.terminate();
|
|
}
|
|
|
|
public PauseSegment getPauseSegment() {
|
|
return pauseSegment;
|
|
}
|
|
|
|
public String getGameID() {
|
|
return gameID;
|
|
}
|
|
|
|
public LobbyConnection getConnection() {
|
|
return connection;
|
|
}
|
|
|
|
public GameInstance getGame() {
|
|
return game;
|
|
}
|
|
|
|
public Pipeline getPipeline() {
|
|
return pipeline;
|
|
}
|
|
|
|
public Participant getActivePlayer() {
|
|
return activePlayer;
|
|
}
|
|
|
|
// Note: DO NOT ADD the connection to the equals and hashcode here, otherwise they recursively call each other
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
Lobby lobby = (Lobby) o;
|
|
return badRequests == lobby.badRequests && Objects.equals(gameID, lobby.gameID) && Objects.equals(game, lobby.game) && Objects.equals(pipeline, lobby.pipeline) && Objects.equals(activePlayer, lobby.activePlayer) && Objects.equals(pauseSegment, lobby.pauseSegment) && Objects.equals(turnTimeoutTimer, lobby.turnTimeoutTimer);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(gameID, game, pipeline, activePlayer, badRequests, pauseSegment, turnTimeoutTimer);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "Lobby{" +
|
|
"gameID='" + gameID + '\'' +
|
|
", game=" + game +
|
|
", pipeline=" + pipeline +
|
|
", activePlayer=" + activePlayer +
|
|
", badRequests=" + badRequests +
|
|
", pauseSegment=" + pauseSegment +
|
|
", turnTimer=" + turnTimeoutTimer +
|
|
'}';
|
|
}
|
|
}
|