From 084087ed5ee66d52fc83783afd9abd089e2c69a5 Mon Sep 17 00:00:00 2001 From: odaakj Date: Mon, 13 Apr 2026 14:52:13 +0200 Subject: [PATCH 1/6] Fix build warning --- .../firebase/FirestoreSessionRepository.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java index 3722644..e66ff12 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java @@ -32,9 +32,7 @@ public void gamePinExists(String gamePin, FirebaseGateway.GamePinExistsCallback firestore.collection(SESSIONS) .whereEqualTo("gamePin", gamePin) .get() - .addOnSuccessListener(snapshot -> { - callback.onResult(!snapshot.isEmpty()); - }) + .addOnSuccessListener(snapshot -> callback.onResult(!snapshot.isEmpty())) .addOnFailureListener(callback::onFailure); } @@ -307,7 +305,16 @@ public void getQuestions( String songArtist = doc.getString("songArtist"); String previewUrl = doc.getString("previewUrl"); String albumArtUrl = doc.getString("albumArtUrl"); - List options = (List) doc.get("options"); + Object rawOptions = doc.get("options"); + List options = new ArrayList<>(); + + if (rawOptions instanceof List) { + for (Object item : (List) rawOptions) { + if (item instanceof String) { + options.add((String) item); + } + } + } Long roundIndexLong = doc.getLong("roundIndex"); int roundIndex = roundIndexLong != null ? roundIndexLong.intValue() : 0; From b51bbb4b877f772291240ab7f8d15eef7fcafb53 Mon Sep 17 00:00:00 2001 From: odaakj Date: Mon, 13 Apr 2026 20:11:04 +0200 Subject: [PATCH 2/6] Add alerts and backend logic for leaving sessions --- .../firebase/AndroidFirebaseGateway.java | 5 + .../firebase/FirestoreSessionRepository.java | 33 +++++- .../controller/LeaderboardController.java | 67 ++++++++++- .../controller/LobbyController.java | 23 +++- .../controller/RoundController.java | 108 ++++++++++++++++-- .../beatbattle/firebase/FirebaseGateway.java | 13 ++- .../firebase/NoOpFirebaseGateway.java | 5 + .../model/services/LobbyService.java | 7 +- .../beatbattle/ui/dialog/AlertDialogs.java | 36 ++++++ .../group07/beatbattle/view/GameOverView.java | 51 ++++++++- .../beatbattle/view/GameRoundView.java | 51 ++++++++- .../beatbattle/view/LeaderboardView.java | 50 +++++++- .../group07/beatbattle/view/LobbyView.java | 21 +++- 13 files changed, 442 insertions(+), 28 deletions(-) diff --git a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java index c6fe312..fd02576 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java @@ -73,6 +73,11 @@ public void removePlayerFromSession( sessionRepository.removePlayerFromSession(sessionId, playerId, callback); } + @Override + public void cancelSession(String sessionId, String endReason, SimpleCallback callback) { + sessionRepository.cancelSession(sessionId, endReason, callback); + } + @Override public void deleteSession(String sessionId, DeleteSessionCallback callback) { sessionRepository.deleteSession(sessionId, callback); diff --git a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java index e66ff12..4104069 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java @@ -50,6 +50,7 @@ public void createSession( session.put("state", state); session.put("currentRound", currentRound); session.put("totalRounds", totalRounds); + session.put("endReason", FirebaseGateway.END_REASON_NONE); session.put("createdAt", Timestamp.now()); firestore.collection(SESSIONS) @@ -206,6 +207,22 @@ public void removePlayerFromSession( .addOnFailureListener(callback::onFailure); } + public void cancelSession( + String sessionId, + String endReason, + FirebaseGateway.SimpleCallback callback + ) { + Map update = new HashMap<>(); + update.put("state", FirebaseGateway.STATE_CANCELLED); + update.put("endReason", endReason); + + firestore.collection(SESSIONS) + .document(sessionId) + .update(update) + .addOnSuccessListener(unused -> callback.onSuccess()) + .addOnFailureListener(callback::onFailure); + } + public void deleteSession( String sessionId, FirebaseGateway.DeleteSessionCallback callback @@ -353,12 +370,24 @@ public void listenToSessionState( listener.onFailure(error); return; } - if (snapshot == null || !snapshot.exists()) return; + + if (snapshot == null || !snapshot.exists()) { + listener.onFailure(new IllegalStateException("Session not found")); + return; + } + String state = snapshot.getString("state"); Long roundLong = snapshot.getLong("currentRound"); + String endReason = snapshot.getString("endReason"); + int currentRound = roundLong != null ? roundLong.intValue() : 0; + if (state != null) { - listener.onStateChanged(state, currentRound); + listener.onStateChanged( + state, + currentRound, + endReason != null ? endReason : "" + ); } }); } diff --git a/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java b/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java index 74a3607..5998c3b 100644 --- a/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java +++ b/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java @@ -65,7 +65,7 @@ public void onNextRound() { final int newRound = session.getCurrentRoundIndex(); game.getFirebaseGateway().updateRoundState( sessionId, - "in_round", + FirebaseGateway.STATE_IN_ROUND, newRound, new FirebaseGateway.SimpleCallback() { @Override @@ -89,16 +89,77 @@ private void launchNextRound() { StateManager.getInstance().setState(new InRoundState(game, roundController)); } - public void onBackToMenu() { + public void onSessionCancelled() { StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); } + public String getSessionId() { + return game.getLocalSessionId(); + } + + public void onBackToMenu() { + String sessionId = game.getLocalSessionId(); + String playerId = game.getLocalPlayerId(); + + if (sessionId == null || sessionId.isBlank()) { + StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); + return; + } + + if (game.isLocalHost()) { + game.getFirebaseGateway().cancelSession( + sessionId, + FirebaseGateway.END_REASON_HOST_LEFT, + new FirebaseGateway.SimpleCallback() { + @Override + public void onSuccess() { + Gdx.app.postRunnable(() -> + StateManager.getInstance().setState(new StartState(game, new LobbyController(game))) + ); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(() -> + StateManager.getInstance().setState(new StartState(game, new LobbyController(game))) + ); + } + } + ); + } else { + if (playerId == null || playerId.isBlank()) { + StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); + return; + } + + game.getFirebaseGateway().removePlayerFromSession( + sessionId, + playerId, + new FirebaseGateway.RemovePlayerCallback() { + @Override + public void onSuccess() { + Gdx.app.postRunnable(() -> + StateManager.getInstance().setState(new StartState(game, new LobbyController(game))) + ); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(() -> + StateManager.getInstance().setState(new StartState(game, new LobbyController(game))) + ); + } + } + ); + } + } + public void onGameOver() { String sessionId = game.getLocalSessionId(); if (game.isLocalHost() && sessionId != null && !sessionId.isBlank()) { game.getFirebaseGateway().updateSessionState( sessionId, - "game_over", + FirebaseGateway.STATE_GAME_OVER, new FirebaseGateway.SimpleCallback() { @Override public void onSuccess() { diff --git a/core/src/main/java/group07/beatbattle/controller/LobbyController.java b/core/src/main/java/group07/beatbattle/controller/LobbyController.java index 6986237..f388950 100644 --- a/core/src/main/java/group07/beatbattle/controller/LobbyController.java +++ b/core/src/main/java/group07/beatbattle/controller/LobbyController.java @@ -47,6 +47,10 @@ public void onJoinGame() { StateManager.getInstance().setState(new JoinCreateState(game, GameMode.JOIN, this)); } + public void onHostCancelledSession(GameMode mode) { + StateManager.getInstance().setState(new JoinCreateState(game, mode, this)); + } + public void onReady( GameMode mode, String playerName, @@ -265,22 +269,29 @@ public void onGameStarted( private void startJoinerSyncListener(String sessionId, GameSession session) { // Track the last state+round we handled to avoid duplicate transitions. // Round 0 is already handled by onGameStarted, so start there. - final String[] lastHandled = {"in_round:0"}; + final String[] lastHandled = {FirebaseGateway.STATE_IN_ROUND + ":0:" + FirebaseGateway.END_REASON_NONE}; game.getFirebaseGateway().listenToSessionState( sessionId, new FirebaseGateway.SessionStateListener() { @Override - public void onStateChanged(String state, int currentRound) { - String key = state + ":" + currentRound; + public void onStateChanged(String state, int currentRound, String endReason) { + String key = state + ":" + currentRound + ":" + endReason; if (key.equals(lastHandled[0])) return; lastHandled[0] = key; - if ("leaderboard".equals(state)) { + if (FirebaseGateway.STATE_CANCELLED.equals(state)) { + Gdx.app.postRunnable(() -> + StateManager.getInstance().setState(new StartState(game, new LobbyController(game))) + ); + return; + } + + if (FirebaseGateway.STATE_LEADERBOARD.equals(state)) { joinerTransitionToLeaderboard(sessionId, session); - } else if ("in_round".equals(state)) { + } else if (FirebaseGateway.STATE_IN_ROUND.equals(state)) { joinerTransitionToRound(session, currentRound); - } else if ("game_over".equals(state)) { + } else if (FirebaseGateway.STATE_GAME_OVER.equals(state)) { Gdx.app.postRunnable(() -> { Leaderboard lb = new Leaderboard(session.getPlayers()); LeaderboardController lc = new LeaderboardController(game, session, lb); diff --git a/core/src/main/java/group07/beatbattle/controller/RoundController.java b/core/src/main/java/group07/beatbattle/controller/RoundController.java index 2614fc3..46823a2 100644 --- a/core/src/main/java/group07/beatbattle/controller/RoundController.java +++ b/core/src/main/java/group07/beatbattle/controller/RoundController.java @@ -28,11 +28,14 @@ public class RoundController { private final Entity roundEntity; private boolean playerAnswered = false; private boolean transitioned = false; + private int activePlayerCount; + private int currentAnswerCount = 0; public RoundController(BeatBattle game, Question question, GameSession session) { this.game = game; this.question = question; this.session = session; + this.activePlayerCount = session.getPlayers().size(); this.roundEntity = RoundFactory.getInstance().createRound( question.getId(), question.getSong().getPreviewUrl() @@ -42,6 +45,7 @@ public RoundController(BeatBattle game, Question question, GameSession session) if (game.isLocalHost()) { startAllAnsweredListener(); + startActivePlayersListener(); } } @@ -86,12 +90,73 @@ public void onRoundExpired() { transitionToLeaderboard(); } - public void onLeaveSession() { + public void onSessionCancelled() { + leaveToStart(); + } + + public String getSessionId() { + return game.getLocalSessionId(); + } + + public boolean isHost() { + return game.isLocalHost(); + } + + private void leaveToStart() { AudioSystem.getInstance().stop(roundEntity); Engine.getInstance().removeEntity(roundEntity); StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); } + public void onLeaveSession() { + String sessionId = game.getLocalSessionId(); + String playerId = game.getLocalPlayerId(); + + if (sessionId == null || sessionId.isBlank()) { + leaveToStart(); + return; + } + + if (game.isLocalHost()) { + game.getFirebaseGateway().cancelSession( + sessionId, + FirebaseGateway.END_REASON_HOST_LEFT, + new FirebaseGateway.SimpleCallback() { + @Override + public void onSuccess() { + Gdx.app.postRunnable(RoundController.this::leaveToStart); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(RoundController.this::leaveToStart); + } + } + ); + } else { + if (playerId == null || playerId.isBlank()) { + leaveToStart(); + return; + } + + game.getFirebaseGateway().removePlayerFromSession( + sessionId, + playerId, + new FirebaseGateway.RemovePlayerCallback() { + @Override + public void onSuccess() { + Gdx.app.postRunnable(RoundController.this::leaveToStart); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.postRunnable(RoundController.this::leaveToStart); + } + } + ); + } + } + // --- Private helpers --- /** @@ -102,18 +167,14 @@ private void startAllAnsweredListener() { String sessionId = game.getLocalSessionId(); if (sessionId == null || sessionId.isBlank()) return; - int totalPlayers = session.getPlayers().size(); - if (totalPlayers == 0) return; - game.getFirebaseGateway().listenToRoundAnswerCount( sessionId, question.getRoundIndex(), new FirebaseGateway.AnswerCountCallback() { @Override public void onCountChanged(int count) { - if (count >= totalPlayers) { - Gdx.app.postRunnable(() -> transitionToLeaderboard()); - } + currentAnswerCount = count; + maybeAdvanceToLeaderboard(); } @Override @@ -125,6 +186,37 @@ public void onFailure(Exception exception) { ); } + private void startActivePlayersListener() { + String sessionId = game.getLocalSessionId(); + if (sessionId == null || sessionId.isBlank()) return; + + game.getFirebaseGateway().listenToPlayers( + sessionId, + new FirebaseGateway.PlayersListenerCallback() { + @Override + public void onPlayersChanged(List players) { + activePlayerCount = players != null ? players.size() : 0; + maybeAdvanceToLeaderboard(); + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.error("RoundController", + "Players listener failed: " + exception.getMessage()); + } + } + ); + } + + private void maybeAdvanceToLeaderboard() { + if (transitioned) return; + if (activePlayerCount <= 0) return; + + if (currentAnswerCount >= activePlayerCount) { + Gdx.app.postRunnable(this::transitionToLeaderboard); + } + } + private void recordAnswerInFirebase() { String sessionId = game.getLocalSessionId(); String playerId = game.getLocalPlayerId(); @@ -183,7 +275,7 @@ private void transitionToLeaderboard() { if (game.isLocalHost() && sessionId != null && !sessionId.isBlank()) { game.getFirebaseGateway().updateSessionState( sessionId, - "leaderboard", + FirebaseGateway.STATE_LEADERBOARD, new FirebaseGateway.SimpleCallback() { @Override public void onSuccess() { fetchAndLaunch(sessionId); } @Override public void onFailure(Exception e) { fetchAndLaunch(sessionId); } diff --git a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java index d539696..1f386fd 100644 --- a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java @@ -6,6 +6,15 @@ public interface FirebaseGateway { + String STATE_LOBBY = "lobby"; + String STATE_IN_ROUND = "in_round"; + String STATE_LEADERBOARD = "leaderboard"; + String STATE_GAME_OVER = "game_over"; + String STATE_CANCELLED = "cancelled"; + + String END_REASON_NONE = ""; + String END_REASON_HOST_LEFT = "host_left"; + void gamePinExists(String gamePin, GamePinExistsCallback callback); void createSession( @@ -49,6 +58,8 @@ void removePlayerFromSession( RemovePlayerCallback callback ); + void cancelSession(String sessionId, String endReason, SimpleCallback callback); + void deleteSession( String sessionId, DeleteSessionCallback callback @@ -130,7 +141,7 @@ interface GetQuestionsCallback { interface SessionStateListener { /** @param state current session state string * @param currentRound current round index stored in Firebase */ - void onStateChanged(String state, int currentRound); + void onStateChanged(String state, int currentRound, String endReason); void onFailure(Exception exception); } diff --git a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java index aee9965..cfd2c79 100644 --- a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java @@ -65,6 +65,11 @@ public void removePlayerFromSession( callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } + @Override + public void cancelSession(String sessionId, String endReason, SimpleCallback callback) { + callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); + } + @Override public void deleteSession( String sessionId, diff --git a/core/src/main/java/group07/beatbattle/model/services/LobbyService.java b/core/src/main/java/group07/beatbattle/model/services/LobbyService.java index c1cbbf2..96025f0 100644 --- a/core/src/main/java/group07/beatbattle/model/services/LobbyService.java +++ b/core/src/main/java/group07/beatbattle/model/services/LobbyService.java @@ -34,7 +34,7 @@ public interface LeaveLobbyCallback { private static final String PIN_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private static final int DEFAULT_TOTAL_ROUNDS = 20; private static final int INITIAL_ROUND = 0; - private static final String INITIAL_SESSION_STATE = "lobby"; + private static final String INITIAL_SESSION_STATE = FirebaseGateway.STATE_LOBBY; private final FirebaseGateway firebaseGateway; private final Random random; @@ -183,9 +183,10 @@ public void leaveLobby( } if (isHost) { - firebaseGateway.deleteSession( + firebaseGateway.cancelSession( sessionId, - new FirebaseGateway.DeleteSessionCallback() { + FirebaseGateway.END_REASON_HOST_LEFT, + new FirebaseGateway.SimpleCallback() { @Override public void onSuccess() { callback.onSuccess(); diff --git a/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java b/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java index e28221c..0c93d10 100644 --- a/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java +++ b/core/src/main/java/group07/beatbattle/ui/dialog/AlertDialogs.java @@ -64,6 +64,42 @@ public static void showInfo( centerDialog(dialog, stage, dialogWidth); } + public static void showInfo( + Stage stage, + BitmapFont font, + String title, + String message, + Runnable onDismiss + ) { + float dialogWidth = getDialogWidth(stage); + float contentWidth = dialogWidth - 140f; + + Skin skin = createSkin(font, BACKGROUND_DARK, BACKGROUND_DARK, dialogWidth); + + Dialog dialog = new Dialog(title, skin) { + @Override + protected void result(Object object) { + if (onDismiss != null) { + onDismiss.run(); + } + } + }; + + Label contentLabel = createContentLabel(message, skin); + + dialog.getContentTable() + .add(contentLabel) + .width(contentWidth) + .padTop(10) + .padBottom(10); + + dialog.button("OK", true); + + styleDialog(dialog); + dialog.show(stage); + centerDialog(dialog, stage, dialogWidth); + } + public static void showWarning( Stage stage, BitmapFont font, diff --git a/core/src/main/java/group07/beatbattle/view/GameOverView.java b/core/src/main/java/group07/beatbattle/view/GameOverView.java index eef29cd..925bb1c 100644 --- a/core/src/main/java/group07/beatbattle/view/GameOverView.java +++ b/core/src/main/java/group07/beatbattle/view/GameOverView.java @@ -21,6 +21,8 @@ import group07.beatbattle.i18n.Strings; import group07.beatbattle.model.LeaderboardEntry; import group07.beatbattle.ui.components.BackButton; +import group07.beatbattle.ui.dialog.AlertDialogs; +import group07.beatbattle.firebase.FirebaseGateway; public class GameOverView extends ScreenAdapter { @@ -34,6 +36,8 @@ public class GameOverView extends ScreenAdapter { private final Stage stage; private final Texture logoTexture; + private boolean sessionCancelledHandled = false; + public GameOverView(BeatBattle game, LeaderboardController controller) { this.game = game; this.controller = controller; @@ -86,12 +90,19 @@ public GameOverView(BeatBattle game, LeaderboardController controller) { backButton.addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { - controller.onBackToMenu(); + AlertDialogs.showConfirmation( + stage, + game.getMontserratFont(), + Strings.leaveSession(), + controller.isHost() ? Strings.leaveHostMsg() : Strings.leavePlayerMsg(), + controller::onBackToMenu + ); } }); root.add(backButton).width(600f).height(130f).row(); stage.addActor(root); + startListeningToSessionState(); } private Table buildRow(LeaderboardEntry entry, boolean isLocal) { @@ -145,4 +156,42 @@ public void dispose() { stage.dispose(); logoTexture.dispose(); } + + private void startListeningToSessionState() { + String sessionId = controller.getSessionId(); + if (sessionId == null || sessionId.isBlank()) return; + + game.getFirebaseGateway().listenToSessionState( + sessionId, + new FirebaseGateway.SessionStateListener() { + @Override + public void onStateChanged(String state, int currentRound, String endReason) { + if (sessionCancelledHandled) return; + + if (FirebaseGateway.STATE_CANCELLED.equals(state)) { + sessionCancelledHandled = true; + Gdx.app.postRunnable(() -> { + if (FirebaseGateway.END_REASON_HOST_LEFT.equals(endReason)) { + AlertDialogs.showInfo( + stage, + game.getMontserratFont(), + "Session ended", + "The host left the session.", + controller::onSessionCancelled + ); + } else { + controller.onSessionCancelled(); + } + }); + } + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.error("GameOverView", + "Session state listener failed: " + exception.getMessage()); + } + } + ); + } } diff --git a/core/src/main/java/group07/beatbattle/view/GameRoundView.java b/core/src/main/java/group07/beatbattle/view/GameRoundView.java index dff9e9a..9dc0324 100644 --- a/core/src/main/java/group07/beatbattle/view/GameRoundView.java +++ b/core/src/main/java/group07/beatbattle/view/GameRoundView.java @@ -19,6 +19,8 @@ import com.badlogic.gdx.utils.Align; import com.badlogic.gdx.utils.Scaling; import com.badlogic.gdx.utils.viewport.ScreenViewport; +import group07.beatbattle.firebase.FirebaseGateway; +import group07.beatbattle.ui.dialog.AlertDialogs; import group07.beatbattle.BeatBattle; import group07.beatbattle.controller.RoundController; @@ -65,6 +67,7 @@ public class GameRoundView extends ScreenAdapter { private Texture iconSoundTex; private Texture iconMuteTex; private Texture logoTexture; + private boolean sessionCancelledHandled = false; public GameRoundView(BeatBattle game, RoundController controller) { this.game = game; @@ -108,7 +111,13 @@ public GameRoundView(BeatBattle game, RoundController controller) { leaveButton.addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { - controller.onLeaveSession(); + AlertDialogs.showConfirmation( + stage, + game.getMontserratFont(), + Strings.leaveSession(), + controller.isHost() ? Strings.leaveHostMsg() : Strings.leavePlayerMsg(), + controller::onLeaveSession + ); } }); @@ -191,6 +200,7 @@ public void changed(ChangeEvent event, Actor actor) { root.add(grid).expand().center().row(); stage.addActor(root); + startListeningToSessionState(); } @Override @@ -346,4 +356,43 @@ private Texture solidTexture(Color color) { pixmap.dispose(); return tex; } + + private void startListeningToSessionState() { + String sessionId = controller.getSessionId(); + if (sessionId == null || sessionId.isBlank()) return; + + game.getFirebaseGateway().listenToSessionState( + sessionId, + new FirebaseGateway.SessionStateListener() { + @Override + public void onStateChanged(String state, int currentRound, String endReason) { + if (sessionCancelledHandled) return; + + if (FirebaseGateway.STATE_CANCELLED.equals(state)) { + sessionCancelledHandled = true; + + Gdx.app.postRunnable(() -> { + if (FirebaseGateway.END_REASON_HOST_LEFT.equals(endReason)) { + AlertDialogs.showInfo( + stage, + game.getMontserratFont(), + "Session ended", + "The host left the session.", + controller::onSessionCancelled + ); + } else { + controller.onSessionCancelled(); + } + }); + } + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.error("GameRoundView", + "Session state listener failed: " + exception.getMessage()); + } + } + ); + } } diff --git a/core/src/main/java/group07/beatbattle/view/LeaderboardView.java b/core/src/main/java/group07/beatbattle/view/LeaderboardView.java index adc3e6f..3693575 100644 --- a/core/src/main/java/group07/beatbattle/view/LeaderboardView.java +++ b/core/src/main/java/group07/beatbattle/view/LeaderboardView.java @@ -20,6 +20,8 @@ import group07.beatbattle.model.LeaderboardEntry; import group07.beatbattle.ui.components.BackButton; import group07.beatbattle.ui.components.JoinCreateButton; +import group07.beatbattle.ui.dialog.AlertDialogs; +import group07.beatbattle.firebase.FirebaseGateway; public class LeaderboardView extends ScreenAdapter { @@ -32,6 +34,7 @@ public class LeaderboardView extends ScreenAdapter { private Label countdownLabel; private float timeLeft = AUTO_ADVANCE_DELAY; private boolean advanced = false; + private boolean sessionCancelledHandled = false; public LeaderboardView(BeatBattle game, LeaderboardController controller) { this.game = game; @@ -101,12 +104,19 @@ public LeaderboardView(BeatBattle game, LeaderboardController controller) { leaveButton.addListener(new ChangeListener() { @Override public void changed(ChangeEvent event, Actor actor) { - controller.onBackToMenu(); + AlertDialogs.showConfirmation( + stage, + game.getMontserratFont(), + Strings.leaveSession(), + controller.isHost() ? Strings.leaveHostMsg() : Strings.leavePlayerMsg(), + controller::onBackToMenu + ); } }); root.add(leaveButton).width(500f).height(120f).row(); stage.addActor(root); + startListeningToSessionState(); } @Override @@ -186,4 +196,42 @@ private Label.LabelStyle subtitleStyle() { style.fontColor = Color.LIGHT_GRAY; return style; } + + private void startListeningToSessionState() { + String sessionId = controller.getSessionId(); + if (sessionId == null || sessionId.isBlank()) return; + + game.getFirebaseGateway().listenToSessionState( + sessionId, + new FirebaseGateway.SessionStateListener() { + @Override + public void onStateChanged(String state, int currentRound, String endReason) { + if (sessionCancelledHandled) return; + + if (FirebaseGateway.STATE_CANCELLED.equals(state)) { + sessionCancelledHandled = true; + Gdx.app.postRunnable(() -> { + if (FirebaseGateway.END_REASON_HOST_LEFT.equals(endReason)) { + AlertDialogs.showInfo( + stage, + game.getMontserratFont(), + "Session ended", + "The host left the session.", + controller::onSessionCancelled + ); + } else { + controller.onSessionCancelled(); + } + }); + } + } + + @Override + public void onFailure(Exception exception) { + Gdx.app.error("LeaderboardView", + "Session state listener failed: " + exception.getMessage()); + } + } + ); + } } diff --git a/core/src/main/java/group07/beatbattle/view/LobbyView.java b/core/src/main/java/group07/beatbattle/view/LobbyView.java index d00b23b..0818133 100644 --- a/core/src/main/java/group07/beatbattle/view/LobbyView.java +++ b/core/src/main/java/group07/beatbattle/view/LobbyView.java @@ -140,8 +140,25 @@ private void startListeningToSessionState() { sessionId, new FirebaseGateway.SessionStateListener() { @Override - public void onStateChanged(String state, int currentRound) { - if ("in_round".equals(state) && !gameStarted && !leavingLobby) { + public void onStateChanged(String state, int currentRound, String endReason) { + if (FirebaseGateway.STATE_CANCELLED.equals(state) && !leavingLobby) { + Gdx.app.postRunnable(() -> { + if (FirebaseGateway.END_REASON_HOST_LEFT.equals(endReason)) { + AlertDialogs.showInfo( + stage, + game.getMontserratFont(), + "Session ended", + "The host left the session.", + () -> controller.onHostCancelledSession(mode) + ); + } else { + controller.onHostCancelledSession(mode); + } + }); + return; + } + + if (FirebaseGateway.STATE_IN_ROUND.equals(state) && !gameStarted && !leavingLobby) { gameStarted = true; setStatusMessage(Strings.gameStarting()); game.getFirebaseGateway().getQuestions( From be382b1bfc64971a7230a000afc4e96951a2273d Mon Sep 17 00:00:00 2001 From: odaakj Date: Thu, 16 Apr 2026 14:02:36 +0200 Subject: [PATCH 3/6] Fix Player leaving session --- .../android/firebase/AndroidFirebaseGateway.java | 5 +++++ .../android/firebase/FirestoreSessionRepository.java | 10 +++++++++- .../beatbattle/controller/LeaderboardController.java | 2 ++ .../group07/beatbattle/controller/RoundController.java | 1 + .../group07/beatbattle/firebase/FirebaseGateway.java | 2 ++ .../beatbattle/firebase/NoOpFirebaseGateway.java | 5 +++++ 6 files changed, 24 insertions(+), 1 deletion(-) diff --git a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java index fd02576..116485d 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/AndroidFirebaseGateway.java @@ -103,6 +103,11 @@ public void listenToSessionState(String sessionId, SessionStateListener listener sessionRepository.listenToSessionState(sessionId, listener); } + @Override + public void stopListeningToSessionState(String sessionId) { + sessionRepository.stopListeningToSessionState(); + } + @Override public void updatePlayerScore(String sessionId, String playerId, int score, SimpleCallback callback) { sessionRepository.updatePlayerScore(sessionId, playerId, score, callback); diff --git a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java index 4104069..6a7f376 100644 --- a/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java +++ b/android/src/main/java/group07/beatbattle/android/firebase/FirestoreSessionRepository.java @@ -23,6 +23,7 @@ public class FirestoreSessionRepository { private static final String QUESTIONS = "Questions"; private final FirebaseFirestore firestore; + private ListenerRegistration sessionStateRegistration; public FirestoreSessionRepository(FirebaseFirestore firestore) { this.firestore = firestore; @@ -359,11 +360,18 @@ public void updateSessionState( .addOnFailureListener(callback::onFailure); } + public void stopListeningToSessionState() { + if (sessionStateRegistration != null) { + sessionStateRegistration.remove(); + sessionStateRegistration = null; + } + } + public void listenToSessionState( String sessionId, FirebaseGateway.SessionStateListener listener ) { - firestore.collection(SESSIONS) + sessionStateRegistration = firestore.collection(SESSIONS) .document(sessionId) .addSnapshotListener((snapshot, error) -> { if (error != null) { diff --git a/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java b/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java index 5998c3b..2bbfd78 100644 --- a/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java +++ b/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java @@ -128,10 +128,12 @@ public void onFailure(Exception exception) { ); } else { if (playerId == null || playerId.isBlank()) { + game.getFirebaseGateway().stopListeningToSessionState(sessionId); StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); return; } + game.getFirebaseGateway().stopListeningToSessionState(sessionId); game.getFirebaseGateway().removePlayerFromSession( sessionId, playerId, diff --git a/core/src/main/java/group07/beatbattle/controller/RoundController.java b/core/src/main/java/group07/beatbattle/controller/RoundController.java index 46823a2..3ec38ef 100644 --- a/core/src/main/java/group07/beatbattle/controller/RoundController.java +++ b/core/src/main/java/group07/beatbattle/controller/RoundController.java @@ -105,6 +105,7 @@ public boolean isHost() { private void leaveToStart() { AudioSystem.getInstance().stop(roundEntity); Engine.getInstance().removeEntity(roundEntity); + game.getFirebaseGateway().stopListeningToSessionState(game.getLocalSessionId()); StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); } diff --git a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java index 1f386fd..98acf0f 100644 --- a/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/FirebaseGateway.java @@ -78,6 +78,8 @@ void deleteSession( void listenToSessionState(String sessionId, SessionStateListener listener); + void stopListeningToSessionState(String sessionId); + void updatePlayerScore(String sessionId, String playerId, int score, SimpleCallback callback); /** Records that a player has submitted an answer for the given round. */ diff --git a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java index cfd2c79..29d8a11 100644 --- a/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java +++ b/core/src/main/java/group07/beatbattle/firebase/NoOpFirebaseGateway.java @@ -98,6 +98,11 @@ public void listenToSessionState(String sessionId, SessionStateListener listener listener.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); } + @Override + public void stopListeningToSessionState(String sessionId) { + // No-op: no listener was registered + } + @Override public void updatePlayerScore(String sessionId, String playerId, int score, SimpleCallback callback) { callback.onFailure(new UnsupportedOperationException("Firebase is not available on this platform")); From e01f090aae16d053a0633c90e1bbf93c91361eaa Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 16 Apr 2026 14:22:12 +0200 Subject: [PATCH 4/6] fix --- .../java/group07/beatbattle/BeatBattle.java | 5 +++++ .../controller/LeaderboardController.java | 2 ++ .../beatbattle/controller/LobbyController.java | 6 ++++++ .../beatbattle/controller/RoundController.java | 18 +++++++++++++++--- 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/group07/beatbattle/BeatBattle.java b/core/src/main/java/group07/beatbattle/BeatBattle.java index 9f2eb24..ae02204 100644 --- a/core/src/main/java/group07/beatbattle/BeatBattle.java +++ b/core/src/main/java/group07/beatbattle/BeatBattle.java @@ -60,6 +60,11 @@ public void setLocalSession(String sessionId, boolean isHost) { this.localIsHost = isHost; } + public void clearLocalSession() { + this.localSessionId = null; + this.localIsHost = false; + } + public String getLocalSessionId() { return localSessionId; } public boolean isLocalHost() { return localIsHost; } diff --git a/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java b/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java index 2bbfd78..0101698 100644 --- a/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java +++ b/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java @@ -128,11 +128,13 @@ public void onFailure(Exception exception) { ); } else { if (playerId == null || playerId.isBlank()) { + game.clearLocalSession(); game.getFirebaseGateway().stopListeningToSessionState(sessionId); StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); return; } + game.clearLocalSession(); game.getFirebaseGateway().stopListeningToSessionState(sessionId); game.getFirebaseGateway().removePlayerFromSession( sessionId, diff --git a/core/src/main/java/group07/beatbattle/controller/LobbyController.java b/core/src/main/java/group07/beatbattle/controller/LobbyController.java index f388950..49c8d94 100644 --- a/core/src/main/java/group07/beatbattle/controller/LobbyController.java +++ b/core/src/main/java/group07/beatbattle/controller/LobbyController.java @@ -280,6 +280,8 @@ public void onStateChanged(String state, int currentRound, String endReason) { if (key.equals(lastHandled[0])) return; lastHandled[0] = key; + if (game.getLocalSessionId() == null) return; + if (FirebaseGateway.STATE_CANCELLED.equals(state)) { Gdx.app.postRunnable(() -> StateManager.getInstance().setState(new StartState(game, new LobbyController(game))) @@ -293,6 +295,7 @@ public void onStateChanged(String state, int currentRound, String endReason) { joinerTransitionToRound(session, currentRound); } else if (FirebaseGateway.STATE_GAME_OVER.equals(state)) { Gdx.app.postRunnable(() -> { + if (game.getLocalSessionId() == null) return; Leaderboard lb = new Leaderboard(session.getPlayers()); LeaderboardController lc = new LeaderboardController(game, session, lb); StateManager.getInstance().setState(new GameOverState(game, lc)); @@ -316,6 +319,7 @@ private void joinerTransitionToLeaderboard(String sessionId, GameSession session @Override public void onPlayersChanged(List updated) { Gdx.app.postRunnable(() -> { + if (game.getLocalSessionId() == null) return; for (Player u : updated) { for (Player local : session.getPlayers()) { if (local.getId().equals(u.getId())) local.setScore(u.getScore()); @@ -330,6 +334,7 @@ public void onPlayersChanged(List updated) { @Override public void onFailure(Exception e) { Gdx.app.postRunnable(() -> { + if (game.getLocalSessionId() == null) return; Leaderboard lb = new Leaderboard(session.getPlayers()); LeaderboardController lc = new LeaderboardController(game, session, lb); StateManager.getInstance().setState(new LeaderboardState(game, lc)); @@ -342,6 +347,7 @@ public void onFailure(Exception e) { private void joinerTransitionToRound(GameSession session, int roundIndex) { if (roundIndex >= session.getQuestions().size()) return; Gdx.app.postRunnable(() -> { + if (game.getLocalSessionId() == null) return; session.setCurrentRoundIndex(roundIndex); Question question = session.getCurrentQuestion(); if (question == null) return; diff --git a/core/src/main/java/group07/beatbattle/controller/RoundController.java b/core/src/main/java/group07/beatbattle/controller/RoundController.java index 3ec38ef..d3966ca 100644 --- a/core/src/main/java/group07/beatbattle/controller/RoundController.java +++ b/core/src/main/java/group07/beatbattle/controller/RoundController.java @@ -105,7 +105,9 @@ public boolean isHost() { private void leaveToStart() { AudioSystem.getInstance().stop(roundEntity); Engine.getInstance().removeEntity(roundEntity); - game.getFirebaseGateway().stopListeningToSessionState(game.getLocalSessionId()); + String sid = game.getLocalSessionId(); + game.clearLocalSession(); + game.getFirebaseGateway().stopListeningToSessionState(sid); StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); } @@ -140,18 +142,28 @@ public void onFailure(Exception exception) { return; } + game.clearLocalSession(); + game.getFirebaseGateway().stopListeningToSessionState(sessionId); game.getFirebaseGateway().removePlayerFromSession( sessionId, playerId, new FirebaseGateway.RemovePlayerCallback() { @Override public void onSuccess() { - Gdx.app.postRunnable(RoundController.this::leaveToStart); + Gdx.app.postRunnable(() -> { + AudioSystem.getInstance().stop(roundEntity); + Engine.getInstance().removeEntity(roundEntity); + StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); + }); } @Override public void onFailure(Exception exception) { - Gdx.app.postRunnable(RoundController.this::leaveToStart); + Gdx.app.postRunnable(() -> { + AudioSystem.getInstance().stop(roundEntity); + Engine.getInstance().removeEntity(roundEntity); + StateManager.getInstance().setState(new StartState(game, new LobbyController(game))); + }); } } ); From ae9c544f47b9bcb3cedecb75ae16d0bd8fb23965 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 16 Apr 2026 14:30:50 +0200 Subject: [PATCH 5/6] fix leaderboard --- .../controller/LeaderboardController.java | 48 +++++++++++++++++-- .../controller/LobbyController.java | 16 ++----- .../controller/RoundController.java | 4 +- .../group07/beatbattle/model/GameSession.java | 17 +++++++ .../beatbattle/view/LeaderboardView.java | 38 ++++++++++----- 5 files changed, 90 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java b/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java index 0101698..bc749cb 100644 --- a/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java +++ b/core/src/main/java/group07/beatbattle/controller/LeaderboardController.java @@ -2,11 +2,14 @@ import com.badlogic.gdx.Gdx; +import java.util.List; + import group07.beatbattle.BeatBattle; import group07.beatbattle.firebase.FirebaseGateway; import group07.beatbattle.model.GameSession; import group07.beatbattle.model.Leaderboard; import group07.beatbattle.model.LeaderboardEntry; +import group07.beatbattle.model.Player; import group07.beatbattle.states.GameOverState; import group07.beatbattle.states.InRoundState; import group07.beatbattle.states.StartState; @@ -16,16 +19,51 @@ public class LeaderboardController { private final BeatBattle game; private final GameSession session; - private final Leaderboard leaderboard; - public LeaderboardController(BeatBattle game, GameSession session, Leaderboard leaderboard) { + private boolean playerListenerActive = false; + private Runnable onPlayersChangedCallback; + + public LeaderboardController(BeatBattle game, GameSession session) { this.game = game; this.session = session; - this.leaderboard = leaderboard; } public Leaderboard getLeaderboard() { - return leaderboard; + return new Leaderboard(session.getPlayers()); + } + + public void startPlayerListener(Runnable onChanged) { + this.playerListenerActive = true; + this.onPlayersChangedCallback = onChanged; + String sessionId = game.getLocalSessionId(); + if (sessionId == null || sessionId.isBlank()) return; + + game.getFirebaseGateway().listenToPlayers( + sessionId, + new FirebaseGateway.PlayersListenerCallback() { + @Override + public void onPlayersChanged(List updatedPlayers) { + if (!playerListenerActive) return; + Gdx.app.postRunnable(() -> { + if (!playerListenerActive) return; + session.syncPlayers(updatedPlayers); + if (onPlayersChangedCallback != null) { + onPlayersChangedCallback.run(); + } + }); + } + + @Override + public void onFailure(Exception e) { + Gdx.app.error("LeaderboardController", "Player listener failed: " + e.getMessage()); + } + } + ); + } + + public void stopPlayerListener() { + playerListenerActive = false; + onPlayersChangedCallback = null; } /** Returns the leaderboard entry for the local player, or null if not found. */ @@ -35,7 +73,7 @@ public LeaderboardEntry getLocalPlayerEntry() { localId = session.getPlayers().get(0).getId(); } if (localId == null) return null; - for (LeaderboardEntry entry : leaderboard.getEntries()) { + for (LeaderboardEntry entry : getLeaderboard().getEntries()) { if (entry.getPlayerId().equals(localId)) return entry; } return null; diff --git a/core/src/main/java/group07/beatbattle/controller/LobbyController.java b/core/src/main/java/group07/beatbattle/controller/LobbyController.java index 49c8d94..7a3390f 100644 --- a/core/src/main/java/group07/beatbattle/controller/LobbyController.java +++ b/core/src/main/java/group07/beatbattle/controller/LobbyController.java @@ -17,7 +17,6 @@ import group07.beatbattle.model.Song; import group07.beatbattle.model.services.LobbyService; import group07.beatbattle.service.MusicServiceCallback; -import group07.beatbattle.model.Leaderboard; import group07.beatbattle.states.GameOverState; import group07.beatbattle.states.InRoundState; import group07.beatbattle.states.JoinCreateState; @@ -296,8 +295,7 @@ public void onStateChanged(String state, int currentRound, String endReason) { } else if (FirebaseGateway.STATE_GAME_OVER.equals(state)) { Gdx.app.postRunnable(() -> { if (game.getLocalSessionId() == null) return; - Leaderboard lb = new Leaderboard(session.getPlayers()); - LeaderboardController lc = new LeaderboardController(game, session, lb); + LeaderboardController lc = new LeaderboardController(game, session); StateManager.getInstance().setState(new GameOverState(game, lc)); }); } @@ -320,13 +318,8 @@ private void joinerTransitionToLeaderboard(String sessionId, GameSession session public void onPlayersChanged(List updated) { Gdx.app.postRunnable(() -> { if (game.getLocalSessionId() == null) return; - for (Player u : updated) { - for (Player local : session.getPlayers()) { - if (local.getId().equals(u.getId())) local.setScore(u.getScore()); - } - } - Leaderboard lb = new Leaderboard(session.getPlayers()); - LeaderboardController lc = new LeaderboardController(game, session, lb); + session.syncPlayers(updated); + LeaderboardController lc = new LeaderboardController(game, session); StateManager.getInstance().setState(new LeaderboardState(game, lc)); }); } @@ -335,8 +328,7 @@ public void onPlayersChanged(List updated) { public void onFailure(Exception e) { Gdx.app.postRunnable(() -> { if (game.getLocalSessionId() == null) return; - Leaderboard lb = new Leaderboard(session.getPlayers()); - LeaderboardController lc = new LeaderboardController(game, session, lb); + LeaderboardController lc = new LeaderboardController(game, session); StateManager.getInstance().setState(new LeaderboardState(game, lc)); }); } diff --git a/core/src/main/java/group07/beatbattle/controller/RoundController.java b/core/src/main/java/group07/beatbattle/controller/RoundController.java index d3966ca..3dd1cea 100644 --- a/core/src/main/java/group07/beatbattle/controller/RoundController.java +++ b/core/src/main/java/group07/beatbattle/controller/RoundController.java @@ -12,7 +12,6 @@ import group07.beatbattle.ecs.systems.AudioSystem; import group07.beatbattle.firebase.FirebaseGateway; import group07.beatbattle.model.GameSession; -import group07.beatbattle.model.Leaderboard; import group07.beatbattle.model.Player; import group07.beatbattle.model.Question; import group07.beatbattle.model.score.ScoreCalculator; @@ -332,8 +331,7 @@ public void onFailure(Exception exception) { private void launchLeaderboard() { AudioSystem.getInstance().stop(roundEntity); Engine.getInstance().removeEntity(roundEntity); - Leaderboard leaderboard = new Leaderboard(session.getPlayers()); - LeaderboardController leaderboardController = new LeaderboardController(game, session, leaderboard); + LeaderboardController leaderboardController = new LeaderboardController(game, session); StateManager.getInstance().setState(new LeaderboardState(game, leaderboardController)); } } diff --git a/core/src/main/java/group07/beatbattle/model/GameSession.java b/core/src/main/java/group07/beatbattle/model/GameSession.java index cb65132..f152f41 100644 --- a/core/src/main/java/group07/beatbattle/model/GameSession.java +++ b/core/src/main/java/group07/beatbattle/model/GameSession.java @@ -42,6 +42,23 @@ public void removePlayer(String playerId) { players.removeIf(p -> p.getId().equals(playerId)); } + /** Sync local player list to match Firebase: remove departed players, update scores. */ + public void syncPlayers(List updated) { + players.removeIf(local -> { + for (Player u : updated) { + if (u.getId().equals(local.getId())) return false; + } + return true; + }); + for (Player u : updated) { + for (Player local : players) { + if (local.getId().equals(u.getId())) { + local.setScore(u.getScore()); + } + } + } + } + public void addQuestion(Question question) { questions.add(question); } public Question getCurrentQuestion() { diff --git a/core/src/main/java/group07/beatbattle/view/LeaderboardView.java b/core/src/main/java/group07/beatbattle/view/LeaderboardView.java index 3693575..fda0b8b 100644 --- a/core/src/main/java/group07/beatbattle/view/LeaderboardView.java +++ b/core/src/main/java/group07/beatbattle/view/LeaderboardView.java @@ -35,6 +35,7 @@ public class LeaderboardView extends ScreenAdapter { private float timeLeft = AUTO_ADVANCE_DELAY; private boolean advanced = false; private boolean sessionCancelledHandled = false; + private Table playersTable; public LeaderboardView(BeatBattle game, LeaderboardController controller) { this.game = game; @@ -78,19 +79,10 @@ public LeaderboardView(BeatBattle game, LeaderboardController controller) { header.add(styledLabel(Strings.scoreCol(), Color.LIGHT_GRAY)).width(200f).right(); root.add(header).fillX().padBottom(20f).row(); - // --- Top 3 rows --- - java.util.List entries = controller.getLeaderboard().getEntries(); - int topCount = Math.min(3, entries.size()); - for (int i = 0; i < topCount; i++) { - root.add(buildPlayerRow(entries.get(i))).fillX().padBottom(16f).row(); - } - - // --- Local player row (if outside top 3) --- - LeaderboardEntry local = controller.getLocalPlayerEntry(); - if (local != null && local.getRank() > 3) { - root.add(styledLabel("· · ·", Color.DARK_GRAY)).padTop(8f).padBottom(8f).row(); - root.add(buildPlayerRow(local)).fillX().padBottom(16f).row(); - } + // --- Player rows (rebuilt live when roster changes) --- + playersTable = new Table(); + buildPlayerRows(); + root.add(playersTable).fillX().row(); root.add().expandY().row(); @@ -116,6 +108,7 @@ public void changed(ChangeEvent event, Actor actor) { root.add(leaveButton).width(500f).height(120f).row(); stage.addActor(root); + controller.startPlayerListener(this::refreshLeaderboard); startListeningToSessionState(); } @@ -156,10 +149,29 @@ public void resize(int width, int height) { @Override public void dispose() { + controller.stopPlayerListener(); stage.dispose(); logoTexture.dispose(); } + private void buildPlayerRows() { + playersTable.clearChildren(); + java.util.List entries = controller.getLeaderboard().getEntries(); + int topCount = Math.min(3, entries.size()); + for (int i = 0; i < topCount; i++) { + playersTable.add(buildPlayerRow(entries.get(i))).fillX().padBottom(16f).row(); + } + LeaderboardEntry local = controller.getLocalPlayerEntry(); + if (local != null && local.getRank() > 3) { + playersTable.add(styledLabel("· · ·", Color.DARK_GRAY)).padTop(8f).padBottom(8f).row(); + playersTable.add(buildPlayerRow(local)).fillX().padBottom(16f).row(); + } + } + + private void refreshLeaderboard() { + buildPlayerRows(); + } + private Table buildPlayerRow(LeaderboardEntry entry) { Table row = new Table(); row.add(styledLabel(entry.getRank() + ".", Color.WHITE)).width(80f).left(); From 1a145f48c3e393d6c84d756d4d2aa54774266768 Mon Sep 17 00:00:00 2001 From: Mostafa Date: Thu, 16 Apr 2026 14:37:01 +0200 Subject: [PATCH 6/6] fix --- .../group07/beatbattle/controller/RoundController.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/core/src/main/java/group07/beatbattle/controller/RoundController.java b/core/src/main/java/group07/beatbattle/controller/RoundController.java index 3dd1cea..7403955 100644 --- a/core/src/main/java/group07/beatbattle/controller/RoundController.java +++ b/core/src/main/java/group07/beatbattle/controller/RoundController.java @@ -306,13 +306,7 @@ private void fetchAndLaunch(String sessionId) { @Override public void onPlayersChanged(List updatedPlayers) { Gdx.app.postRunnable(() -> { - for (Player updated : updatedPlayers) { - for (Player local : session.getPlayers()) { - if (local.getId().equals(updated.getId())) { - local.setScore(updated.getScore()); - } - } - } + session.syncPlayers(updatedPlayers); launchLeaderboard(); }); }