From a04ff1f2cb341f55d69612adc894896c477013cd Mon Sep 17 00:00:00 2001 From: Jonah Bauer Date: Thu, 13 Jan 2022 22:05:47 +0100 Subject: [PATCH] rejoin support for libgdx client --- .../libgdx/actions/overlay/TrumpOverlay.java | 1 - .../client/libgdx/actors/CardStack.java | 25 +- .../client/libgdx/actors/PadOfTruth.java | 14 ++ .../client/libgdx/screens/GameScreen.java | 82 ++++++ .../client/libgdx/screens/LobbyScreen.java | 29 ++- .../client/libgdx/screens/RejoinScreen.java | 150 +++++++++++ .../client/libgdx/state/AwaitingGameLog.java | 60 +++++ .../libgdx/state/AwaitingJoinSession.java | 56 +++-- .../wizard/client/libgdx/state/Game.java | 236 ++++++++++++------ .../wizard/client/libgdx/state/Lobby.java | 37 ++- .../main/resources/i18n/messages.properties | 8 + .../resources/i18n/messages_de.properties | 8 + .../eu/jonahbauer/wizard/server/Session.java | 1 + .../server/machine/states/LobbyState.java | 3 - 14 files changed, 593 insertions(+), 117 deletions(-) create mode 100644 wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/RejoinScreen.java create mode 100644 wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingGameLog.java diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/overlay/TrumpOverlay.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/overlay/TrumpOverlay.java index 0953c0a..90680aa 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/overlay/TrumpOverlay.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actions/overlay/TrumpOverlay.java @@ -85,7 +85,6 @@ public class TrumpOverlay extends Overlay { animateSuit = suit != null && suit != CardUtil.getDefaultTrumpSuit(card); if (animateSuit) { trumpSuitActor.setRotation(0); - trumpSuitActor.setOrigin(0, 0); cardGroup.addActor(trumpSuitActor); trumpSuitActor.setCard(suit); } else { diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/CardStack.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/CardStack.java index 133f802..4e5faeb 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/CardStack.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/CardStack.java @@ -1,9 +1,11 @@ package eu.jonahbauer.wizard.client.libgdx.actors; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.scenes.scene2d.*; import eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings; import eu.jonahbauer.wizard.client.libgdx.WizardGame; import eu.jonahbauer.wizard.client.libgdx.screens.GameScreen; +import eu.jonahbauer.wizard.common.model.Card; import lombok.Data; import java.util.*; @@ -52,12 +54,33 @@ public class CardStack extends Group { (float) random.nextGaussian(0, COLLAPSED_ROTATION_DEVIATION), (float) random.nextGaussian(0, EXPANDED_ROTATION_DEVIATION) ); - addActor(card); + super.addActor(card); if (expanded) entry.expand(); else entry.collapse(); cards.add(entry); } + public void add(GameScreen.Seat seat, Card card, TextureAtlas atlas) { + var actor = new CardActor(card, atlas); + + var x = (float) random.nextGaussian((WizardGame.WIDTH - actor.getWidth()) / 2, COLLAPSED_POSITION_DEVIATION); + var y = (float) random.nextGaussian((WizardGame.HEIGHT - actor.getHeight()) / 2, COLLAPSED_POSITION_DEVIATION); + var collapsedRotation = (float) random.nextGaussian(0, COLLAPSED_ROTATION_DEVIATION); + var expandedRotation = (float) random.nextGaussian(0, EXPANDED_ROTATION_DEVIATION); + + if (expanded) { + actor.setPosition(seat.getFrontX(), seat.getFrontY()); + actor.setRotation(expandedRotation); + } else { + actor.setPosition(x, y); + actor.setRotation(collapsedRotation); + } + + var entry = new Entry(actor, seat, x, y, collapsedRotation, expandedRotation); + super.addActor(actor); + cards.add(entry); + } + @Override protected void childrenChanged() { hover.toFront(); diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/PadOfTruth.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/PadOfTruth.java index 416281d..54523f0 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/PadOfTruth.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/actors/PadOfTruth.java @@ -109,4 +109,18 @@ public class PadOfTruth extends Table { if (round < 0 || round >= predictions.length || round >= scores.length) throw new ArrayIndexOutOfBoundsException(round); if (player < 0 || player >= predictions[round].length || player >= scores[round].length) throw new ArrayIndexOutOfBoundsException(player); } + + public void clearValues() { + for (var row : predictions) { + for (var label : row) { + label.setText(null); + } + } + + for (var row : scores) { + for (var label : row) { + label.setText(null); + } + } + } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/GameScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/GameScreen.java index 9fdd9c3..f373375 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/GameScreen.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/GameScreen.java @@ -22,6 +22,8 @@ import eu.jonahbauer.wizard.client.libgdx.actors.CardsGroup; import eu.jonahbauer.wizard.client.libgdx.actors.PadOfTruth; import eu.jonahbauer.wizard.client.libgdx.state.Game; import eu.jonahbauer.wizard.client.libgdx.util.AnimationTimings; +import eu.jonahbauer.wizard.client.libgdx.util.CardUtil; +import eu.jonahbauer.wizard.client.libgdx.util.Pair; import eu.jonahbauer.wizard.client.libgdx.util.WizardAssetManager; import eu.jonahbauer.wizard.common.messages.observer.UserInputMessage; import eu.jonahbauer.wizard.common.model.Card; @@ -626,6 +628,86 @@ public class GameScreen extends WizardScreen { })); execute(sequence); } + + public void setTrump(@Nullable Card card, @Nullable Card.Suit suit) { + initTrumpCards(); + + if (suit != null && suit != CardUtil.getDefaultTrumpSuit(card)) { + trumpSuitActor.setPosition(TRUMP_SUIT_POSITION.x, TRUMP_SUIT_POSITION.y); + trumpSuitActor.setRotation(TRUMP_SUIT_ROTATION); + trumpSuitActor.setCard(suit); + contentRoot.addActor(trumpSuitActor); + } else { + trumpSuitActor.remove(); + } + + if (card != null) { + trumpCardActor.setPosition(TRUMP_CARD_POSITION.x, TRUMP_CARD_POSITION.y); + trumpCardActor.setCard(card); + contentRoot.addActor(trumpCardActor); + } else { + trumpCardActor.remove(); + } + } + + public void setStack(@NotNull List> stack) { + cardStack.clearChildren(); + + for (var pair : stack) { + cardStack.add(seats.getOrDefault(pair.first(), Seat.FALLBACK), pair.second(), atlas); + } + } + + public void setPredictions(@NotNull Map> predictions) { + predictions.forEach((round, roundPredictions) -> { + roundPredictions.forEach((player, prediction) -> { + var index = orderedPlayers.indexOf(player); + if (index == -1) throw new NoSuchElementException(); + + padOfTruth.setPrediction(index, round, prediction); + }); + }); + } + + public void setScores(@NotNull Map> scores) { + scores.forEach((round, roundScores) -> { + roundScores.forEach((player, score) -> { + var index = orderedPlayers.indexOf(player); + if (index == -1) throw new NoSuchElementException(); + + padOfTruth.setScore(index, round, score); + }); + }); + } + + public void clearMessages() { + messageStack.clear(); + persistentMessage = null; + } + + public void clear() { + // stop all actions + pendingActions.clear(); + stage.getRoot().removeAction(currentAction); + currentAction = null; + + pendingSync.set(false); + + // remove ui elements + clearMessages(); + dim(LAYER_BELOW_OVERLAY, false); + overlayRoot.clear(); + cardStack.clearChildren(); + handCards.clearChildren(); + if (trumpCardActor != null) { + trumpCardActor.remove(); + } + if (trumpSuitActor != null) { + trumpSuitActor.remove(); + } + padOfTruth.clearValues(); + nameLabels.values().forEach(label -> label.setStyle(labelStyleDefault)); + } // // diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LobbyScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LobbyScreen.java index 26ef380..0dc35a5 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LobbyScreen.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/LobbyScreen.java @@ -17,6 +17,7 @@ public class LobbyScreen extends MenuScreen { private TextButton buttonBack; private TextButton buttonJoin; + private TextButton buttonRejoin; private TextButton buttonCreate; private TextField playerName; @@ -24,7 +25,7 @@ public class LobbyScreen extends MenuScreen { private Label labelSessionPlayerCount; private Label labelSessionConfiguration; - private SessionData selectedSession; + private UUID selectedSession; private List sessions; private ScrollPane sessionListContainer; @@ -40,6 +41,9 @@ public class LobbyScreen extends MenuScreen { } else if (actor == buttonCreate) { game.getClient().execute(Lobby.class, Lobby::showCreateScreen); sfxClick(); + } else if (actor == buttonRejoin) { + game.getClient().execute(Lobby.class, Lobby::showRejoinScreen); + sfxClick(); } } }; @@ -60,6 +64,10 @@ public class LobbyScreen extends MenuScreen { buttonCreate.addListener(listener); getButtonGroup().addActor(buttonCreate); + buttonRejoin = new TextButton(messages.get("menu.lobby.rejoin"), skin); + buttonRejoin.addListener(listener); + getButtonGroup().addActor(buttonRejoin); + buttonJoin = new TextButton(messages.get("menu.lobby.join"), skin); buttonJoin.addListener(listener); getButtonGroup().addActor(buttonJoin); @@ -93,10 +101,13 @@ public class LobbyScreen extends MenuScreen { content.layout(); stage.addActor(content); - stage.addCaptureListener(new KeyboardFocusManager(sessions, playerName, buttonBack, buttonCreate, buttonJoin)); + stage.addCaptureListener(new KeyboardFocusManager( + sessions, playerName, buttonBack, buttonCreate, buttonRejoin, buttonJoin + )); buttonBack.setName("button_back"); buttonJoin.setName("button_join"); + buttonJoin.setName("button_rejoin"); buttonCreate.setName("button_create"); sessions.setName("session_list"); playerName.setName("player_name"); @@ -154,7 +165,7 @@ public class LobbyScreen extends MenuScreen { this.sessions.invalidateHierarchy(); } - if (selectedSession != null && selectedSession.getUuid().equals(session)) { + if (selectedSession != null && selectedSession.equals(session)) { updateData(null); } } @@ -164,7 +175,7 @@ public class LobbyScreen extends MenuScreen { this.sessions.getItems().set(index, session); this.sessions.invalidateHierarchy(); - if (selectedSession != null && selectedSession.getUuid().equals(session.getUuid())) { + if (selectedSession != null && selectedSession.equals(session.getUuid())) { updateData(session); } } @@ -182,7 +193,7 @@ public class LobbyScreen extends MenuScreen { labelSessionName.setText(data.getName()); labelSessionPlayerCount.setText(Integer.toString(data.getPlayerCount())); labelSessionConfiguration.setText(data.getConfiguration().toString()); - selectedSession = data; + selectedSession = data.getUuid(); } else { labelSessionName.setText(""); labelSessionPlayerCount.setText(""); @@ -209,7 +220,13 @@ public class LobbyScreen extends MenuScreen { if (!error) { var client = game.getClient(); - client.execute(Lobby.class, (s, c) -> s.joinSession(client, selectedSession, playerName)); + try { + client.execute(Lobby.class, (s, c) -> s.joinSession(c, selectedSession, playerName)); + } catch (IllegalArgumentException e) { + // only if session is not known + log.warn(e.getMessage()); + this.sessionListContainer.setStyle(getScrollPaneErrorStyle()); + } } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/RejoinScreen.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/RejoinScreen.java new file mode 100644 index 0000000..e50ba0f --- /dev/null +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/screens/RejoinScreen.java @@ -0,0 +1,150 @@ +package eu.jonahbauer.wizard.client.libgdx.screens; + +import com.badlogic.gdx.scenes.scene2d.Actor; +import com.badlogic.gdx.scenes.scene2d.ui.*; +import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; +import eu.jonahbauer.wizard.client.libgdx.WizardGame; +import eu.jonahbauer.wizard.client.libgdx.listeners.KeyboardFocusManager; +import eu.jonahbauer.wizard.client.libgdx.listeners.ResetErrorListener; +import eu.jonahbauer.wizard.client.libgdx.state.Lobby; +import lombok.extern.log4j.Log4j2; + +import java.util.UUID; + +@Log4j2 +public class RejoinScreen extends MenuScreen { + + private TextButton buttonBack; + private TextButton buttonContinue; + + private TextField sessionUUID; + private TextField playerUUID; + private TextField secret; + + private final ChangeListener listener = new ChangeListener() { + @Override + public void changed(ChangeEvent event, Actor actor) { + if (actor == buttonBack) { + game.getClient().execute(Lobby.class, Lobby::showListScreen); + sfxClick(); + } else if (actor == buttonContinue) { + rejoin(); + sfxClick(); + } + } + }; + + public RejoinScreen(WizardGame game) { + super(game); + } + + @Override + public void show() { + super.show(); + + var credentials = game.storage.credentials; + + buttonBack = new TextButton(messages.get("menu.rejoin.back"), skin); + buttonBack.addListener(listener); + getButtonGroup().addActor(buttonBack); + + buttonContinue = new TextButton(messages.get("menu.rejoin.continue"), skin); + buttonContinue.addListener(listener); + getButtonGroup().addActor(buttonContinue); + + var errorListener = new ResetErrorListener(skin); + + sessionUUID = new TextField(credentials != null ? credentials.session().toString() : null , skin); + sessionUUID.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.5f); + sessionUUID.setSize(0.4f * WizardGame.WIDTH, 64); + sessionUUID.addListener(errorListener); + sessionUUID.setProgrammaticChangeEvents(true); + + playerUUID = new TextField(credentials != null ? credentials.player().toString() : null, skin); + playerUUID.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.45f); + playerUUID.setSize(0.4f * WizardGame.WIDTH, 64); + playerUUID.addListener(errorListener); + + secret = new TextField(credentials != null ? credentials.secret() : null, skin); + secret.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.4f); + secret.setSize(0.4f * WizardGame.WIDTH, 64); + secret.addListener(errorListener); + + var contentTable = new Table(skin).center().left(); + contentTable.columnDefaults(0).growX().width(0.4f * WizardGame.WIDTH - 20); + contentTable.setSize(0.4f * WizardGame.WIDTH - 20, 400); + contentTable.setPosition(WizardGame.WIDTH * 0.3f, WizardGame.HEIGHT * 0.3f); + + contentTable.add(messages.get("menu.rejoin.session_uuid.label")).row(); + contentTable.add(sessionUUID).row(); + contentTable.add(messages.get("menu.rejoin.player_uuid.label")).row(); + contentTable.add(playerUUID).row(); + contentTable.add(messages.get("menu.rejoin.player_secret.label")).row(); + contentTable.add(secret).row(); + + stage.addActor(contentTable); + stage.addCaptureListener(new KeyboardFocusManager(sessionUUID, playerUUID, secret, buttonBack, buttonContinue)); + + buttonBack.setName("button_back"); + buttonContinue.setName("button_continue"); + sessionUUID.setName("session_uuid"); + playerUUID.setName("player_uuid"); + secret.setName("player_secret"); + } + + private void rejoin() { + boolean error = false; + + String sessionUUIDText = this.sessionUUID.getText(); + UUID sessionUUID = null; + if (sessionUUIDText.isBlank()) { + log.warn("Please enter the session uuid."); + this.sessionUUID.setStyle(getTextFieldErrorStyle()); + error = true; + } else { + try { + sessionUUID = UUID.fromString(sessionUUIDText); + } catch (IllegalArgumentException e) { + log.warn("Please enter a valid session uuid."); + this.sessionUUID.setStyle(getTextFieldErrorStyle()); + error = true; + } + } + + String playerUUIDText = this.playerUUID.getText(); + UUID playerUUID = null; + if (playerUUIDText.isBlank()) { + log.warn("Please enter the player uuid."); + this.playerUUID.setStyle(getTextFieldErrorStyle()); + error = true; + } else { + try { + playerUUID = UUID.fromString(playerUUIDText); + } catch (IllegalArgumentException e) { + log.warn("Please enter a valid player uuid."); + this.playerUUID.setStyle(getTextFieldErrorStyle()); + error = true; + } + } + + String playerSecret = this.secret.getText(); + if (playerSecret.isBlank()) { + log.warn("Please enter the player secret."); + this.secret.setStyle(getTextFieldErrorStyle()); + error = true; + } + + var fPlayerUUID = playerUUID; + var fSessionUUID = sessionUUID; + if (!error) { + var client = game.getClient(); + try { + client.execute(Lobby.class, (s, c) -> s.rejoinSession(c, fSessionUUID, fPlayerUUID, playerSecret)); + } catch (IllegalArgumentException e) { + // only if session is not known + log.warn(e.getMessage()); + this.sessionUUID.setStyle(getTextFieldErrorStyle()); + } + } + } +} diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingGameLog.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingGameLog.java new file mode 100644 index 0000000..51d7304 --- /dev/null +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingGameLog.java @@ -0,0 +1,60 @@ +package eu.jonahbauer.wizard.client.libgdx.state; + +import eu.jonahbauer.wizard.client.libgdx.Client; +import eu.jonahbauer.wizard.client.libgdx.screens.LoadingScreen; +import eu.jonahbauer.wizard.common.messages.observer.ObserverMessage; +import eu.jonahbauer.wizard.common.messages.server.AckMessage; +import eu.jonahbauer.wizard.common.messages.server.GameMessage; +import eu.jonahbauer.wizard.common.messages.server.ServerMessage; +import eu.jonahbauer.wizard.common.messages.server.StartingGameMessage; +import eu.jonahbauer.wizard.common.model.Configuration; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +import java.util.*; + +@Log4j2 +@RequiredArgsConstructor +public final class AwaitingGameLog extends BaseState { + private static final int TIMEOUT_MILLIS = 10_000; + + private final UUID self; + private final UUID session; + private final String sessionName; + private final Configuration configuration; + + private final LinkedHashMap players; + + private final List messages = new ArrayList<>(); + + private boolean started = false; + + @Override + public Optional onEnter(Client client) { + log.info("Waiting for game log..."); + client.getGame().setScreen(new LoadingScreen(client.getGame(), "menu.loading.rejoining")); + client.timeout(this, TIMEOUT_MILLIS); + return Optional.empty(); + } + + @Override + public Optional onMessage(Client client, ServerMessage message) { + if (!started) { + if (message instanceof StartingGameMessage) { + started = true; + return Optional.empty(); + } else { + return unexpectedMessage(message); + } + } else if (message instanceof GameMessage gameMessage) { + messages.add(gameMessage.getObserverMessage()); + return Optional.empty(); + } else if (message instanceof AckMessage) { + var game = new Game(self, session, sessionName, configuration, players); + game.init(messages); + return Optional.of(game); + } else { + return unexpectedMessage(message); + } + } +} diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinSession.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinSession.java index 70170be..cef1259 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinSession.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/AwaitingJoinSession.java @@ -9,7 +9,10 @@ import eu.jonahbauer.wizard.common.model.Configuration; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.LinkedHashMap; import java.util.Optional; import java.util.UUID; @@ -18,10 +21,10 @@ import java.util.UUID; @RequiredArgsConstructor public final class AwaitingJoinSession extends Awaiting { - private final UUID session; - private final String sessionName; - private final Configuration configuration; - private final String playerName; + private final @Nullable UUID session; + private final @NotNull String sessionName; + private final @NotNull Configuration configuration; + private final @NotNull Source source; @Override public Optional onEnter(Client client) { @@ -33,24 +36,37 @@ public final class AwaitingJoinSession extends Awaiting { @Override public Optional onMessage(Client client, ServerMessage message) { if (message instanceof SessionJoinedMessage joined) { - if (session != null && !session.equals(joined.getSession())) { + var session = joined.getSession(); + if (this.session != null && !this.session.equals(session)) { return super.onMessage(client, message); } else { - log.info("There are {} players in this session.", joined.getPlayers().size()); - log.info("Your uuid is {}.", joined.getPlayer()); - log.info("Your secret is {}.", joined.getSecret()); + var players = joined.getPlayers(); + var player = joined.getPlayer(); + var secret = joined.getSecret(); - client.getGame().storage.credentials = new SavedData.SessionCredentials( - joined.getSession(), - joined.getPlayer(), - joined.getSecret() - ); + log.info("There are {} players in this session.", players.size()); + log.info("Your uuid is {}.", player); + log.info("Your secret is {}.", secret); - return Optional.of(new Session( - new SessionData(joined.getSession(), sessionName, -1, configuration), - joined.getPlayers(), - joined.getPlayer() - )); + client.getGame().storage.credentials = new SavedData.SessionCredentials(session, player, secret); + + if (source == Source.REJOIN) { + var playerMap = new LinkedHashMap(); + players.forEach(p -> playerMap.put(p.getUuid(), p.getName())); + return Optional.of(new AwaitingGameLog( + player, + session, + sessionName, + configuration, + playerMap + )); + } else { + return Optional.of(new Session( + new SessionData(session, sessionName, -1, configuration), + players, + player + )); + } } } else if (message instanceof NackMessage nack) { switch (nack.getCode()) { @@ -72,4 +88,8 @@ public final class AwaitingJoinSession extends Awaiting { return super.onMessage(client, message); } } + + public enum Source { + JOIN, CREATE, REJOIN + } } diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java index 51aee8b..a489762 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Game.java @@ -17,6 +17,7 @@ import eu.jonahbauer.wizard.common.model.Card; import eu.jonahbauer.wizard.common.model.Configuration; import lombok.Data; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.experimental.Accessors; import lombok.extern.log4j.Log4j2; import org.jetbrains.annotations.NotNull; @@ -35,17 +36,20 @@ import static eu.jonahbauer.wizard.common.messages.observer.UserInputMessage.Act @Log4j2 @Getter +@RequiredArgsConstructor public final class Game extends BaseState { private final UUID self; private final UUID session; private final String sessionName; private final Configuration configuration; + private List pendingMessages; + private final LinkedHashMap players; - private final Map scores = new HashMap<>(); + private final Map> scores = new HashMap<>(); + private final Map> predictions = new HashMap<>(); private int round = -1; - private final Map predictions = new HashMap<>(); private final Map> hands = new HashMap<>(); private final Map>> tricks = new HashMap<>(); @@ -57,25 +61,25 @@ public final class Game extends BaseState { private Card trumpCard; private Card.Suit trumpSuit; - private GameScreen gameScreen; + private @Nullable GameScreen gameScreen; private final AtomicBoolean sending = new AtomicBoolean(false); private boolean juggling; private Card juggleCard; private boolean werewolf; - public Game(UUID self, UUID session, String sessionName, Configuration configuration, LinkedHashMap players) { - this.self = self; - this.session = session; - this.sessionName = sessionName; - this.configuration = configuration; - this.players = players; + public void init(List messages) { + pendingMessages = messages; } @Override public Optional onEnter(Client client) { + var out = handlePendingMessages(); + if (out.isPresent()) return out; + gameScreen = new GameScreen(client.getGame(), self, players); client.getGame().setScreen(gameScreen); + updateScreen(); return super.onEnter(client); } @@ -85,42 +89,7 @@ public final class Game extends BaseState { try { if (message instanceof GameMessage game) { var observerMessage = game.getObserverMessage(); - if (observerMessage instanceof StateMessage state) { - switch (state.getState()) { - case "starting_round" -> onStartRound(); - case "starting_trick" -> onStartTrick(); - case "juggling" -> onJuggle(); - case "finished" -> { - onFinished(); - return returnToSession(); - } - case "error" -> { - onError(); - return returnToSession(); - } - } - } else if (observerMessage instanceof HandMessage hand) { - onHandMessage(hand.getPlayer(), hand.getHand()); - } else if (observerMessage instanceof PredictionMessage prediction) { - onPredictionMessage(prediction.getPlayer(), prediction.getPrediction()); - } else if (observerMessage instanceof TrumpMessage trump) { - onTrumpMessage(trump.getCard(), trump.getSuit()); - } else if (observerMessage instanceof TrickMessage trick) { - onTrickMessage(trick.getPlayer(), trick.getCards()); - } else if (observerMessage instanceof CardMessage card) { - onCardMessage(card.getPlayer(), card.getCard()); - } else if (observerMessage instanceof ScoreMessage score) { - onScoreMessage(score.getPoints()); - } else if (observerMessage instanceof UserInputMessage input) { - onUserInputMessage(input.getPlayer(), input.getAction(), input.getTimeout()); - } else if (observerMessage instanceof TimeoutMessage) { - onTimeoutMessage(); - } else { - log.fatal("Unknown observer message type {}.", observerMessage.getClass()); - // TODO user feedback - return Optional.of(new Menu()); - } - return Optional.empty(); + return onMessage(observerMessage); } else if (message instanceof NackMessage nack) { return onNackMessage(nack); } else if (message instanceof AckMessage) { @@ -130,23 +99,60 @@ public final class Game extends BaseState { return unexpectedMessage(message); } } finally { - if (pendingClearActivePlayer > 0 && --pendingClearActivePlayer == 0) { - finishInteraction(); + executeDelayedFinishInteraction(); + } + } + + private Optional onMessage(ObserverMessage message) { + if (message instanceof StateMessage state) { + switch (state.getState()) { + case "starting_round" -> onStartRound(); + case "starting_trick" -> onStartTrick(); + case "juggling" -> onJuggle(); + case "finished" -> { + onFinished(); + return returnToSession(); + } + case "error" -> { + onError(); + return returnToSession(); + } } + } else if (message instanceof HandMessage hand) { + onHandMessage(hand.getPlayer(), hand.getHand()); + } else if (message instanceof PredictionMessage prediction) { + onPredictionMessage(prediction.getPlayer(), prediction.getPrediction()); + } else if (message instanceof TrumpMessage trump) { + onTrumpMessage(trump.getCard(), trump.getSuit()); + } else if (message instanceof TrickMessage trick) { + onTrickMessage(trick.getPlayer(), trick.getCards()); + } else if (message instanceof CardMessage card) { + onCardMessage(card.getPlayer(), card.getCard()); + } else if (message instanceof ScoreMessage score) { + onScoreMessage(score.getPoints()); + } else if (message instanceof UserInputMessage input) { + onUserInputMessage(input.getPlayer(), input.getAction(), input.getTimeout()); + } else if (message instanceof TimeoutMessage) { + onTimeoutMessage(); + } else { + log.fatal("Unknown observer message type {}.", message.getClass()); + // TODO user feedback + return Optional.of(new Menu()); } + return Optional.empty(); } private void onStartRound() { log.info("Round {} is starting...", round + 1); round ++; - predictions.clear(); tricks.clear(); trumpSuit = null; trumpCard = null; stack.clear(); trick = -1; - gameScreen.startRound(round); + + if (gameScreen != null) gameScreen.startRound(round); } private void onStartTrick() { @@ -154,7 +160,7 @@ public final class Game extends BaseState { trick ++; stack.clear(); finishInteraction(); - gameScreen.startTrick(); + if (gameScreen != null) gameScreen.startTrick(); } private void onJuggle() { @@ -178,8 +184,10 @@ public final class Game extends BaseState { finishInteraction(); hands.put(player, new ArrayList<>(hand)); - gameScreen.setSelectedCard(null); - gameScreen.setHand(player, hand, juggling); + if (gameScreen != null) { + gameScreen.setSelectedCard(null); + gameScreen.setHand(player, hand, juggling); + } juggling = false; } @@ -191,8 +199,8 @@ public final class Game extends BaseState { boolean changed = currentInteraction != null && currentInteraction.action() == CHANGE_PREDICTION; finishInteraction(); - predictions.put(player, prediction); - gameScreen.addPrediction(round, player, prediction, changed); + predictions.computeIfAbsent(round, r -> new HashMap<>()).put(player, prediction); + if (gameScreen != null) gameScreen.addPrediction(round, player, prediction, changed); } private void onTrumpMessage(@Nullable Card trumpCard, @Nullable Card.Suit trumpSuit) { @@ -210,7 +218,7 @@ public final class Game extends BaseState { werewolf = true; } else { werewolf = false; - gameScreen.showTrumpOverlay(player, trumpCard, trumpSuit); + if (gameScreen != null) gameScreen.showTrumpOverlay(player, trumpCard, trumpSuit); } } @@ -221,7 +229,7 @@ public final class Game extends BaseState { this.stack.clear(); this.tricks.computeIfAbsent(player, p -> new ArrayList<>()) .add(cards); - gameScreen.finishTrick(player); + if (gameScreen != null) gameScreen.finishTrick(player); } private void onCardMessage(@NotNull UUID player, @NotNull Card card) { @@ -244,14 +252,16 @@ public final class Game extends BaseState { hand.remove(handCard); } - gameScreen.playCard(player, handCard); + if (gameScreen != null) gameScreen.playCard(player, handCard); } private void onScoreMessage(@Unmodifiable Map<@NotNull UUID, @NotNull Integer> points) { log.info("The scores are as follows: " + points); - points.forEach((player, p) -> scores.merge(player, p, Integer::sum)); - gameScreen.addScores(round, points); - gameScreen.showScoreOverlay(); + scores.put(round, points); + if (gameScreen != null) { + gameScreen.addScores(round, points); + gameScreen.showScoreOverlay(); + } } private void onUserInputMessage(@Nullable UUID player, @NotNull UserInputMessage.Action action, long timeout) { @@ -264,23 +274,11 @@ public final class Game extends BaseState { ); if (action == UserInputMessage.Action.SYNC) { - gameScreen.sync(); + if (gameScreen != null) gameScreen.sync(); } else { currentInteraction = new Interaction(player, action, timeout); - gameScreen.setActivePlayer(player, action, timeout); - if (player != null && werewolf && action == PICK_TRUMP) { - gameScreen.swapTrumpCard(player); - } - - if (isActive()) { - switch (action) { - case PICK_TRUMP -> currentInteraction.overlay(gameScreen.showPickTrumpOverlay(timeout, werewolf)); - case MAKE_PREDICTION -> currentInteraction.overlay(gameScreen.showMakePredictionOverlay(round, timeout)); - case CHANGE_PREDICTION -> currentInteraction.overlay(gameScreen.showChangePredictionOverlay(round, predictions.get(player), timeout)); - case PLAY_CARD -> gameScreen.setPersistentMessage("game.message.play_card.self"); - } - } + showCurrentInteraction(); if (werewolf && action == PICK_TRUMP) { werewolf = false; @@ -291,7 +289,7 @@ public final class Game extends BaseState { private void onTimeoutMessage() { log.info("The previous interaction timed out."); delayedFinishInteraction(); - gameScreen.timeout(); + if (gameScreen != null) gameScreen.timeout(); } private Optional onNackMessage(@NotNull NackMessage nack) { @@ -304,8 +302,10 @@ public final class Game extends BaseState { int code = nack.getCode(); if (code == NackMessage.ILLEGAL_ARGUMENT || code == NackMessage.ILLEGAL_STATE) { log.error(nack.getMessage()); - gameScreen.addMessage(true, "game.message.literal", nack.getMessage()); - gameScreen.ready(false); + if (gameScreen != null) { + gameScreen.addMessage(true, "game.message.literal", nack.getMessage()); + gameScreen.ready(false); + } return Optional.empty(); } else { return unexpectedMessage(nack); @@ -317,16 +317,17 @@ public final class Game extends BaseState { sending.set(false); if (isActive() && currentInteraction.action() == JUGGLE_CARD && juggleCard != null) { - gameScreen.setSelectedCard(juggleCard); + if (gameScreen != null) gameScreen.setSelectedCard(juggleCard); juggleCard = null; } - gameScreen.ready(true); + if (gameScreen != null) gameScreen.ready(true); } // // public Optional onCardClicked(Client client, @NotNull Card card) { + assert gameScreen != null; if (isActive()) { if (currentInteraction.action() == PLAY_CARD) { if (card == Card.CLOUD || card == Card.JUGGLER) { @@ -348,6 +349,7 @@ public final class Game extends BaseState { } public Optional onSuitClicked(Client client, @NotNull Card.Suit suit) { + assert gameScreen != null; if (isActive() && currentInteraction.action() == PICK_TRUMP) { send(client, new PickTrumpMessage(suit)); } else { @@ -357,6 +359,7 @@ public final class Game extends BaseState { } public Optional onPredictionMade(Client client, int prediction) { + assert gameScreen != null; if (isActive()) { if (currentInteraction.action() == MAKE_PREDICTION || currentInteraction.action() == CHANGE_PREDICTION) { send(client, new PredictMessage(prediction)); @@ -369,14 +372,15 @@ public final class Game extends BaseState { } public Optional sync(Client client) { + assert gameScreen != null; send(client, new ContinueMessage()); return Optional.empty(); } + // public Optional returnToMenu() { return Optional.of(new Menu()); } - // private Optional returnToSession() { return Optional.of(new Session( @@ -397,11 +401,12 @@ public final class Game extends BaseState { client.send(new InteractionMessage(message)); return true; } else { - gameScreen.addMessage(true, "game.message.nack.too_fast"); + if (gameScreen != null) gameScreen.addMessage(true, "game.message.nack.too_fast"); return false; } } + // /** * Checks whether some action from the player is expected. */ @@ -409,6 +414,29 @@ public final class Game extends BaseState { return currentInteraction != null && (currentInteraction.player() == null || self.equals(currentInteraction.player())); } + private void showCurrentInteraction() { + if (gameScreen == null) return; + + var player = currentInteraction.player(); + var action = currentInteraction.action(); + var timeout = currentInteraction.timeout(); + + gameScreen.setActivePlayer(player, action, timeout); + + if (player != null && werewolf && action == PICK_TRUMP) { + gameScreen.swapTrumpCard(player); + } + + if (isActive()) { + switch (action) { + case PICK_TRUMP -> currentInteraction.overlay(gameScreen.showPickTrumpOverlay(timeout, werewolf)); + case MAKE_PREDICTION -> currentInteraction.overlay(gameScreen.showMakePredictionOverlay(round, timeout)); + case CHANGE_PREDICTION -> currentInteraction.overlay(gameScreen.showChangePredictionOverlay(round, predictions.get(round).get(player), timeout )); + case PLAY_CARD -> gameScreen.setPersistentMessage("game.message.play_card.self"); + } + } + } + /** * Close the interaction overlay associated with the current interaction and reset the current interaction. */ @@ -419,7 +447,7 @@ public final class Game extends BaseState { } currentInteraction = null; - gameScreen.clearActivePlayer(); + if (gameScreen != null) gameScreen.clearActivePlayer(); } /** @@ -432,7 +460,49 @@ public final class Game extends BaseState { var overlay = currentInteraction.overlay(); if (overlay != null) overlay.close(); } - gameScreen.clearActivePlayer(); + if (gameScreen != null) gameScreen.clearActivePlayer(); + } + + private void executeDelayedFinishInteraction() { + if (pendingClearActivePlayer > 0 && --pendingClearActivePlayer == 0) { + finishInteraction(); + } + } + // + + private Optional handlePendingMessages() { + if (pendingMessages != null) { + for (var message : pendingMessages) { + var result = onMessage(message); + if (result.isPresent()) { + return result; + } + executeDelayedFinishInteraction(); + } + pendingMessages = null; + } + return Optional.empty(); + } + + private void updateScreen() { + if (gameScreen == null) return; + gameScreen.clear(); + + gameScreen.setPredictions(predictions); + gameScreen.setScores(scores); + gameScreen.setTrump(trumpCard, trumpSuit); + gameScreen.setStack(stack); + + var hand = hands.get(self); + if (hand != null) { + gameScreen.setHand(self, hand, false); + } + + if (currentInteraction != null) { + showCurrentInteraction(); + } else { + gameScreen.clearActivePlayer(); + } } // diff --git a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Lobby.java b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Lobby.java index f28f0f6..f76ac72 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Lobby.java +++ b/wizard-client/wizard-client-libgdx/core/src/main/java/eu/jonahbauer/wizard/client/libgdx/state/Lobby.java @@ -3,14 +3,19 @@ package eu.jonahbauer.wizard.client.libgdx.state; import eu.jonahbauer.wizard.client.libgdx.Client; import eu.jonahbauer.wizard.client.libgdx.screens.CreateGameScreen; import eu.jonahbauer.wizard.client.libgdx.screens.LobbyScreen; +import eu.jonahbauer.wizard.client.libgdx.screens.RejoinScreen; import eu.jonahbauer.wizard.common.messages.client.CreateSessionMessage; import eu.jonahbauer.wizard.common.messages.client.JoinSessionMessage; +import eu.jonahbauer.wizard.common.messages.client.RejoinMessage; import eu.jonahbauer.wizard.common.messages.data.SessionData; import eu.jonahbauer.wizard.common.messages.server.*; import eu.jonahbauer.wizard.common.model.Configuration; +import org.jetbrains.annotations.NotNull; import java.util.*; +import static eu.jonahbauer.wizard.client.libgdx.state.AwaitingJoinSession.Source.*; + public final class Lobby extends BaseState { private final Map sessions = new HashMap<>(); @@ -64,14 +69,29 @@ public final class Lobby extends BaseState { return Optional.of(new Menu()); } - public Optional createSession(Client client, String sessionName, Configuration config, long timeout, String playerName) { + public Optional createSession(Client client, @NotNull String sessionName, @NotNull Configuration config, long timeout, @NotNull String playerName) { client.send(new CreateSessionMessage(sessionName, playerName, timeout, config)); - return Optional.of(new AwaitingJoinSession(null, sessionName, config, playerName)); + return Optional.of(new AwaitingJoinSession(null, sessionName, config, CREATE)); + } + + public Optional joinSession(Client client, UUID sessionUUID, String playerName) { + var session = sessions.get(sessionUUID); + if (session != null) { + client.send(new JoinSessionMessage(sessionUUID, playerName)); + return Optional.of(new AwaitingJoinSession(session.getUuid(), session.getName(), session.getConfiguration(), JOIN)); + } else { + throw new IllegalArgumentException("Session does not exist."); + } } - public Optional joinSession(Client client, SessionData session, String playerName) { - client.send(new JoinSessionMessage(session.getUuid(), playerName)); - return Optional.of(new AwaitingJoinSession(session.getUuid(), session.getName(), session.getConfiguration(), playerName)); + public Optional rejoinSession(Client client, @NotNull UUID sessionUUID, @NotNull UUID playerUUID, @NotNull String secret) { + var session = sessions.get(sessionUUID); + if (session != null) { + client.send(new RejoinMessage(sessionUUID, playerUUID, secret)); + return Optional.of(new AwaitingJoinSession(sessionUUID, session.getName(), session.getConfiguration(), REJOIN)); + } else { + throw new IllegalArgumentException("Session does not exist."); + } } public Optional showCreateScreen(Client client) { @@ -88,4 +108,11 @@ public final class Lobby extends BaseState { lobbyScreen.setSessions(sessions.values().toArray(SessionData[]::new)); return Optional.empty(); } + + public Optional showRejoinScreen(Client client) { + var game = client.getGame(); + lobbyScreen = null; + game.setScreen(new RejoinScreen(game)); + return Optional.empty(); + } } \ No newline at end of file diff --git a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties index 0aeec35..4ec905c 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties +++ b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages.properties @@ -8,6 +8,7 @@ menu.connect.address.label=Enter Server Address menu.connect.uri.hint=Server Address menu.lobby.join=Join +menu.lobby.rejoin=Rejoin menu.lobby.create=Create menu.lobby.back=Back menu.lobby.player_name.label=Player Name @@ -23,8 +24,15 @@ menu.create_game.session_configuration.label=Configuration menu.create_game.back=Back menu.create_game.create=Create +menu.rejoin.session_uuid.label=Session ID +menu.rejoin.player_uuid.label=Player ID +menu.rejoin.player_secret.label=Secret +menu.rejoin.back=Back +menu.rejoin.continue=Join + menu.loading.loading=Loading... menu.loading.connecting=Connecting... +menu.loading.rejoining=Waiting for game log... menu.loading.joining_session=Joining session... menu.loading.joining_lobby=Joining lobby... menu.loading.back=Return To Main Menu diff --git a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties index 1700930..bf1920e 100644 --- a/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties +++ b/wizard-client/wizard-client-libgdx/core/src/main/resources/i18n/messages_de.properties @@ -8,6 +8,7 @@ menu.connect.address.label=Server-Adresse eingeben menu.connect.uri.hint=Server Address menu.lobby.join=Beitreten +menu.lobby.rejoin=Rejoin menu.lobby.create=Erstellen menu.lobby.back=Verlassen menu.lobby.player_name.label=Spielername @@ -23,8 +24,15 @@ menu.create_game.session_configuration.label=Spielvariante menu.create_game.back=Zurück menu.create_game.create=Erstellen +menu.rejoin.session_uuid.label=Sitzungs-ID +menu.rejoin.player_uuid.label=Spieler-ID +menu.rejoin.player_secret.label=Passwort +menu.rejoin.back=Zurück +menu.rejoin.continue=Beitreten + menu.loading.loading=Laden... menu.loading.connecting=Verbinde... +menu.loading.rejoining=Warte auf Spiellog... menu.loading.joining_session=Trete Sitzung bei... menu.loading.joining_lobby=Trete Warteraum bei... menu.loading.back=Zurück zum Hauptmenü diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java index 809fd77..e85d06c 100644 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java +++ b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/Session.java @@ -110,6 +110,7 @@ public class Session implements Observer { player.send(message.getValue()); } } + player.send(new AckMessage()); return sessionPlayer.toData(); } diff --git a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/LobbyState.java b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/LobbyState.java index e280c2a..6b1a379 100644 --- a/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/LobbyState.java +++ b/wizard-server/src/main/java/eu/jonahbauer/wizard/server/machine/states/LobbyState.java @@ -25,7 +25,6 @@ public class LobbyState implements ClientState { if (message instanceof CreateSessionMessage create) { Lobby.getInstance().leave(player); try { - player.buffer(); var session = Lobby.getInstance().createSession( create.getSessionName(), create.getTimeout(), @@ -42,7 +41,6 @@ public class LobbyState implements ClientState { } else if (message instanceof JoinSessionMessage join) { Lobby.getInstance().leave(player); try { - player.buffer(); var session = Lobby.getInstance().getSession(join.getSession()); if (session == null) { throw new NackException(NackMessage.NOT_FOUND, "Session not found."); @@ -59,7 +57,6 @@ public class LobbyState implements ClientState { } else if (message instanceof RejoinMessage rejoin) { Lobby.getInstance().leave(player); try { - player.buffer(); var session = Lobby.getInstance().getSession(rejoin.getSession()); if (session == null) { throw new NackException(NackMessage.NOT_FOUND, "Session not found.");