Merge branch 'main' into 'chore/95-write-document-for-software-quality'
This commit is contained in:
@@ -82,6 +82,7 @@ This document describes the protocol for client-server communication in our appl
|
|||||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
- [Request Parameters](#request-parameters)
|
- [Request Parameters](#request-parameters)
|
||||||
- [Success Response](#success-response)
|
- [Success Response](#success-response)
|
||||||
|
- [START_GAME command](#start_game-command)
|
||||||
- [GET_LOBBY_STATUS command](#get_lobby_status-command)
|
- [GET_LOBBY_STATUS command](#get_lobby_status-command)
|
||||||
- [Required pre-execution checks](#required-pre-execution-checks)
|
- [Required pre-execution checks](#required-pre-execution-checks)
|
||||||
- [Request Parameters](#request-parameters)
|
- [Request Parameters](#request-parameters)
|
||||||
@@ -547,8 +548,64 @@ END
|
|||||||
```
|
```
|
||||||
-ERR
|
-ERR
|
||||||
CODE=LOBBY_NOT_FOUND
|
CODE=LOBBY_NOT_FOUND
|
||||||
MESSAGE=Lobby not found
|
MSG=Lobby not found
|
||||||
END
|
END
|
||||||
|
|
||||||
|
## START_GAME command
|
||||||
|
|
||||||
|
The `START_GAME` command requests the server to start a new game in the specified lobby. The
|
||||||
|
server requires an explicit numeric `ID` parameter identifying the target lobby.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
- [`UserLoggedInCheck`](#userloggedincheck)
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
| Parameter Name | Type | Optional | Description |
|
||||||
|
| :------------- | :--- | :------: | :---------- |
|
||||||
|
| `ID` | `int` | no | Numeric id of the target lobby |
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: `StartGameParser` — reads the required `ID` parameter and builds `StartGameRequest`.
|
||||||
|
- Handler: `StartGameHandler` — resolves the lobby by id, checks the requester is a member of the
|
||||||
|
lobby, verifies a game is not already running and that enough players are present, initializes
|
||||||
|
`GameController` and attaches it to the lobby. On success a structured `START_GAME` success
|
||||||
|
response containing `GAME` and `MESSAGE` fields is returned.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
| Field | Type | Description |
|
||||||
|
| :------- | :------ | :-------------------------------- |
|
||||||
|
| `GAME` | `int` | Numeric id of the started game (uses lobby id) |
|
||||||
|
| `MESSAGE`| `String`| Human-readable status message |
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
| Code | Description |
|
||||||
|
| :--------------- | :---------------------------------------- |
|
||||||
|
| `MISSING_PARAMETER` | Required parameter `ID` missing (parser) |
|
||||||
|
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
|
||||||
|
| `NOT_IN_LOBBY` | Requesting user is not a member of the lobby |
|
||||||
|
| `NOT_ENOUGH_PLAYERS` | Not enough players to start the game |
|
||||||
|
| `ALREADY_STARTED` | A game is already running in the lobby |
|
||||||
|
| `USER_NOT_LOGGED_IN` | No user associated with the session |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
START_GAME ID=1
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response (success)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
+OK
|
||||||
|
GAME=1
|
||||||
|
MESSAGE=Game started successfully
|
||||||
|
END
|
||||||
|
|
||||||
|
```
|
||||||
```
|
```
|
||||||
|
|
||||||
## GET_LOBBY_LIST command
|
## GET_LOBBY_LIST command
|
||||||
@@ -1112,3 +1169,57 @@ GET_LOBBY_STATUS ID=1
|
|||||||
MESSAGE=Lobby not found
|
MESSAGE=Lobby not found
|
||||||
END
|
END
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## CREATE_LOBBY command
|
||||||
|
|
||||||
|
The `CREATE_LOBBY` command requests the server to create a new lobby and return its identifier.
|
||||||
|
|
||||||
|
### Required pre-execution checks
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
No parameters.
|
||||||
|
|
||||||
|
### Implementation notes
|
||||||
|
|
||||||
|
- Parser: CreateLobbyParser — constructs a CreateLobbyRequest from the incoming PrimitiveRequest context (no parameters are read).
|
||||||
|
- Handler: CreateLobbyHandler — calls LobbyManager.createNewLobby(null).
|
||||||
|
- If the returned LobbyId is null, the handler dispatches an ErrorResponse with code `LOBBIES_FULL` and message "Maximum number of 8 lobbies reached".
|
||||||
|
- On success, the handler dispatches a CreateLobbyResponse containing the new lobby id.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| :-------- | :--- | :---------- |
|
||||||
|
| `LOBBY_ID`| `int`| Numeric id of the newly created lobby |
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
| :---------- | :--------------------------------------------------------------------- |
|
||||||
|
| `LOBBIES_FULL` | The server cannot create a new lobby because the maximum number of lobbies has been reached |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
|
||||||
|
```
|
||||||
|
CREATE_LOBBY
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Success Response
|
||||||
|
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
LOBBY_ID=1
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Error Response
|
||||||
|
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=LOBBIES_FULL
|
||||||
|
MESSAGE=Maximum number of 8 lobbies reached
|
||||||
|
END
|
||||||
|
```
|
||||||
@@ -3,11 +3,14 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.jspecify.annotations.Nullable;
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,10 +41,15 @@ public class ChatController {
|
|||||||
return chatModelMap;
|
return chatModelMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<ChatKey, ChatModel> chatModelMap;
|
private final Map<ChatKey, ChatModel> chatModelMap;
|
||||||
|
|
||||||
private static final long REFRESH_TIME = 1000;
|
private static final long REFRESH_TIME = 1000;
|
||||||
|
|
||||||
|
/** List of all users connected to the server, to safe them locally on the client */
|
||||||
|
private final List<String> localUserList;
|
||||||
|
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor, adds TimerTask to be sent to the server regularly
|
* Constructor, adds TimerTask to be sent to the server regularly
|
||||||
*
|
*
|
||||||
@@ -52,13 +60,16 @@ public class ChatController {
|
|||||||
this.username = username;
|
this.username = username;
|
||||||
chatClient = new ChatClient(clientService);
|
chatClient = new ChatClient(clientService);
|
||||||
chatModelMap = new LinkedHashMap<>();
|
chatModelMap = new LinkedHashMap<>();
|
||||||
|
localUserList = new ArrayList<>();
|
||||||
this.chatBoxController = new ChatBoxController(username, this);
|
this.chatBoxController = new ChatBoxController(username, this);
|
||||||
|
this.logger = LogManager.getLogger(ChatController.class);
|
||||||
this.timer = new Timer();
|
this.timer = new Timer();
|
||||||
timer.schedule(
|
timer.schedule(
|
||||||
new TimerTask() {
|
new TimerTask() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
receiveMessage();
|
receiveMessage();
|
||||||
|
checkWhisperUsers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
@@ -66,7 +77,7 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to be activated, if a lobby has be chosen. It will update the UI and add a new
|
* Method to be activated, if a lobby has been chosen. It will update the UI and add a new
|
||||||
* ChatModel to hold the Data for the Lobby Chat
|
* ChatModel to hold the Data for the Lobby Chat
|
||||||
*
|
*
|
||||||
* @param lobbyId
|
* @param lobbyId
|
||||||
@@ -78,7 +89,15 @@ public class ChatController {
|
|||||||
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
|
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** method to get all messages from the server */
|
/**
|
||||||
|
* Method to process the Messages that got polled by the {@link ChatClient}.
|
||||||
|
*
|
||||||
|
* <p>Case distinction for every message: - Messages identified with ChatType.LOBBY -> check if
|
||||||
|
* they belong to the users lobby. - Messages identified with ChatType.GLOBAL -> check if the
|
||||||
|
* target user of that Message is the user, or the message got sent by the user.
|
||||||
|
*
|
||||||
|
* <p>All Messages get added to a particular {@link ChatModel}, if the checks passed.
|
||||||
|
*/
|
||||||
public void receiveMessage() {
|
public void receiveMessage() {
|
||||||
List<Message> newMessages = chatClient.getMessages();
|
List<Message> newMessages = chatClient.getMessages();
|
||||||
if (!newMessages.isEmpty()) {
|
if (!newMessages.isEmpty()) {
|
||||||
@@ -102,14 +121,24 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ChatType.WHISPER:
|
case ChatType.WHISPER:
|
||||||
if (msg.target.equals(username)) {
|
if (msg.target.equals(username) || msg.sender.equals(username)) {
|
||||||
if (chatModelMap.containsKey(
|
ChatKey key;
|
||||||
new ChatKey(ChatType.WHISPER, msg.sender))) {
|
if (msg.target.equals(username)) {
|
||||||
chatModelMap
|
key = new ChatKey(ChatType.WHISPER, msg.sender);
|
||||||
.get(new ChatKey(ChatType.WHISPER, msg.sender))
|
|
||||||
.addMessage(msg);
|
|
||||||
} else {
|
} else {
|
||||||
chatBoxController.addWhisperChat(msg.sender);
|
key = new ChatKey(ChatType.WHISPER, msg.target);
|
||||||
|
}
|
||||||
|
if (chatModelMap.containsKey(key)) {
|
||||||
|
chatModelMap.get(key).addMessage(msg);
|
||||||
|
} else {
|
||||||
|
ChatModel chatModel =
|
||||||
|
new ChatModel(
|
||||||
|
ChatType.WHISPER,
|
||||||
|
username,
|
||||||
|
lobbyId,
|
||||||
|
key.targetUser());
|
||||||
|
chatBoxController.addWhisperChat(key.targetUser(), chatModel);
|
||||||
|
chatModel.addMessage(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -118,6 +147,30 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to process the usernames of other users connected to the server, after they got polled
|
||||||
|
* by the {@link ChatClient}.
|
||||||
|
*
|
||||||
|
* <p>Checks for every one of the usernames, if it is already in the list localUserList and if
|
||||||
|
* not adds them. For every new User added in the List, they get added as an option to start a
|
||||||
|
* Whisper Chat with.
|
||||||
|
*/
|
||||||
|
public synchronized void checkWhisperUsers() {
|
||||||
|
List<String> users = chatClient.getUsers();
|
||||||
|
logger.info(users);
|
||||||
|
if (!users.isEmpty()) {
|
||||||
|
for (String user : users) {
|
||||||
|
String value = user.split("\\=")[1];
|
||||||
|
logger.info(value);
|
||||||
|
if (!(localUserList.contains(value) || value.equals(username))) {
|
||||||
|
localUserList.add(value);
|
||||||
|
logger.info("adding new whisper user");
|
||||||
|
chatBoxController.addWhisperUser(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* method to send a message to the server
|
* method to send a message to the server
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public class GameService {
|
|||||||
* @return The current GameState object representing the state of the game.
|
* @return The current GameState object representing the state of the game.
|
||||||
*/
|
*/
|
||||||
public int getPot() {
|
public int getPot() {
|
||||||
|
ensureState();
|
||||||
return state.pot;
|
return state.pot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ public class GameService {
|
|||||||
* @return The current GameState object representing the state of the game.
|
* @return The current GameState object representing the state of the game.
|
||||||
*/
|
*/
|
||||||
public List<Card> getCommunityCards() {
|
public List<Card> getCommunityCards() {
|
||||||
|
ensureState();
|
||||||
return state.communityCards;
|
return state.communityCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +59,7 @@ public class GameService {
|
|||||||
* @return A list of Player objects representing the players in the current game state.
|
* @return A list of Player objects representing the players in the current game state.
|
||||||
*/
|
*/
|
||||||
public List<Player> getPlayers() {
|
public List<Player> getPlayers() {
|
||||||
|
ensureState();
|
||||||
return state.players;
|
return state.players;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +101,19 @@ public class GameService {
|
|||||||
* yet.
|
* yet.
|
||||||
*/
|
*/
|
||||||
public Player getWinner() {
|
public Player getWinner() {
|
||||||
|
ensureState();
|
||||||
|
|
||||||
if (state.winnerIndex < 0) {
|
if (state.winnerIndex < 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.players.get(state.winnerIndex);
|
return state.players.get(state.winnerIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ensure that the game state has been initialized before accessing it. */
|
||||||
|
private void ensureState() {
|
||||||
|
if (state == null) {
|
||||||
|
throw new IllegalStateException("Call refresh() first");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,4 +69,33 @@ public class ChatClient {
|
|||||||
}
|
}
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to poll the usernames of all users currently connected to the server, by sending the
|
||||||
|
* "LIST_USERS" command.
|
||||||
|
*
|
||||||
|
* @return List of all usernames retrieved from the server.
|
||||||
|
*/
|
||||||
|
public List<String> getUsers() {
|
||||||
|
logger.info("Asking server for list of users");
|
||||||
|
List<String> users = clientService.processCommand("LIST_USERS");
|
||||||
|
List<String> parameters = new ArrayList<String>();
|
||||||
|
for (int i = 0; i < users.size(); i++) {
|
||||||
|
String line = users.get(i);
|
||||||
|
if (line.equals("USERS")) {
|
||||||
|
continue;
|
||||||
|
} else if (line.equals("END")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
line = line.replaceFirst("^\t", "");
|
||||||
|
|
||||||
|
if (line.equals("USER")) {
|
||||||
|
String username = users.get(i + 1);
|
||||||
|
username = username.replaceFirst("^\t", "");
|
||||||
|
username = username.replaceFirst("^\t", "");
|
||||||
|
parameters.add(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,17 @@ import java.io.IOException;
|
|||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
@@ -30,10 +36,17 @@ public class ClientService {
|
|||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
private final boolean offlineMode;
|
private final boolean offlineMode;
|
||||||
|
|
||||||
public static ArrayList<String> response;
|
|
||||||
private final AtomicInteger idGenerator;
|
private final AtomicInteger idGenerator;
|
||||||
private Logger logger;
|
private Logger logger;
|
||||||
|
|
||||||
|
private final Map<Integer, ArrayBlockingQueue<ParsedResponse>> pendingResponses =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
private final CopyOnWriteArrayList<Consumer<List<String>>> eventListeners =
|
||||||
|
new CopyOnWriteArrayList<>();
|
||||||
|
private Thread readerThread = null;
|
||||||
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
|
private static final int READER_JOIN_TIMEOUT_MS = 500;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a ClientService with the given server IP and port. It establishes a socket
|
* Constructs a ClientService with the given server IP and port. It establishes a socket
|
||||||
* connection to the server and initializes the TcpTransport and ExecutorService for
|
* connection to the server and initializes the TcpTransport and ExecutorService for
|
||||||
@@ -43,7 +56,6 @@ public class ClientService {
|
|||||||
* @param port The port number of the server to connect to.
|
* @param port The port number of the server to connect to.
|
||||||
*/
|
*/
|
||||||
public ClientService(String ip, int port) {
|
public ClientService(String ip, int port) {
|
||||||
|
|
||||||
this.idGenerator = new AtomicInteger(0);
|
this.idGenerator = new AtomicInteger(0);
|
||||||
|
|
||||||
this.logger = LogManager.getLogger(ClientService.class);
|
this.logger = LogManager.getLogger(ClientService.class);
|
||||||
@@ -59,6 +71,100 @@ public class ClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
executor = Executors.newSingleThreadExecutor();
|
executor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
startReaderThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startReaderThread() {
|
||||||
|
running.set(true);
|
||||||
|
readerThread =
|
||||||
|
new Thread(
|
||||||
|
() -> {
|
||||||
|
while (running.get()) {
|
||||||
|
try {
|
||||||
|
RawPacket rp = clienttcptransport.read();
|
||||||
|
processRawPacket(rp);
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (running.get()) {
|
||||||
|
logger.warn("IO error on transport reader", e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"casono-client-reader");
|
||||||
|
readerThread.setDaemon(true);
|
||||||
|
readerThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processRawPacket(RawPacket rp) throws InterruptedException, IOException {
|
||||||
|
int rid = rp.requestId();
|
||||||
|
String responseText = rp.payload();
|
||||||
|
logger.info("Raw message '{}'", responseText);
|
||||||
|
|
||||||
|
boolean hasStatus = false;
|
||||||
|
boolean success = false;
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String rawLine : responseText.split("\\n")) {
|
||||||
|
String line = rawLine;
|
||||||
|
if (!hasStatus) {
|
||||||
|
if ("+OK".equals(line)) {
|
||||||
|
success = true;
|
||||||
|
hasStatus = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.startsWith("-ERR") || line.startsWith("-ERROR")) {
|
||||||
|
success = false;
|
||||||
|
hasStatus = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("END".equals(line)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("\t")) {
|
||||||
|
line = line.substring(1);
|
||||||
|
}
|
||||||
|
lines.add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasStatus) {
|
||||||
|
if (responseText.contains("+OK")) {
|
||||||
|
success = true;
|
||||||
|
hasStatus = true;
|
||||||
|
} else if (responseText.contains("-ERR") || responseText.contains("-ERROR")) {
|
||||||
|
success = false;
|
||||||
|
hasStatus = true;
|
||||||
|
} else {
|
||||||
|
// best effort: treat as success
|
||||||
|
success = true;
|
||||||
|
hasStatus = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rid == 0) {
|
||||||
|
for (Consumer<List<String>> l : eventListeners) {
|
||||||
|
try {
|
||||||
|
l.accept(List.copyOf(lines));
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Event listener threw", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ArrayBlockingQueue<ParsedResponse> q = pendingResponses.get(rid);
|
||||||
|
if (q != null) {
|
||||||
|
q.put(new ParsedResponse(success, lines));
|
||||||
|
} else {
|
||||||
|
logger.warn("No pending response queue for id {}", rid);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,9 +186,11 @@ public class ClientService {
|
|||||||
return offlineMode;
|
return offlineMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow primitive values to contain hyphens (UUIDs) in addition to
|
||||||
|
// digits/words/colons
|
||||||
static Pattern responseRex =
|
static Pattern responseRex =
|
||||||
Pattern.compile(
|
Pattern.compile(
|
||||||
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[\\d\\w:]+))");
|
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[-\\d\\w:]+))");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes escape characters from a string, specifically converting escaped single quotes (\')
|
* Removes escape characters from a string, specifically converting escaped single quotes (\')
|
||||||
@@ -122,6 +230,19 @@ public class ClientService {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static record ParsedResponse(boolean success, List<String> lines) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an event listener that receives unsolicited event payload lines (no status prefix).
|
||||||
|
*/
|
||||||
|
public void addEventListener(Consumer<List<String>> listener) {
|
||||||
|
eventListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeEventListener(Consumer<List<String>> listener) {
|
||||||
|
eventListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a command to the server and processes the multi-line response. It handles the protocol
|
* Sends a command to the server and processes the multi-line response. It handles the protocol
|
||||||
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until
|
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until
|
||||||
@@ -133,74 +254,51 @@ public class ClientService {
|
|||||||
* occurs.
|
* occurs.
|
||||||
*/
|
*/
|
||||||
protected List<String> processCommand(String message) {
|
protected List<String> processCommand(String message) {
|
||||||
List<String> response = new ArrayList<>();
|
if (offlineMode) {
|
||||||
sendRequest(
|
throw new RuntimeException("ClientService is offline");
|
||||||
() -> {
|
}
|
||||||
try {
|
|
||||||
writeToTransport(message);
|
|
||||||
String responseText = clienttcptransport.read().payload();
|
|
||||||
logger.info("Raw message '{}'", responseText);
|
|
||||||
|
|
||||||
boolean hasStatus = false;
|
int reqId = idGenerator.incrementAndGet();
|
||||||
boolean success = false;
|
ArrayBlockingQueue<ParsedResponse> q = new ArrayBlockingQueue<>(1);
|
||||||
for (String rawLine : responseText.split("\n")) {
|
pendingResponses.put(reqId, q);
|
||||||
String line = rawLine;
|
|
||||||
if (!hasStatus) {
|
Future<?> writeFuture =
|
||||||
if ("+OK".equals(line)) {
|
executor.submit(
|
||||||
success = true;
|
() -> {
|
||||||
hasStatus = true;
|
try {
|
||||||
continue;
|
clienttcptransport.write(new RawPacket(reqId, message));
|
||||||
}
|
} catch (IOException e) {
|
||||||
if (line.startsWith("-ERR") || line.startsWith("-ERROR")) {
|
throw new RuntimeException(e);
|
||||||
success = false;
|
|
||||||
hasStatus = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// ignore any lines before the status indicator
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if ("END".equals(line)) {
|
try {
|
||||||
break;
|
writeFuture.get();
|
||||||
}
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
pendingResponses.remove(reqId);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
pendingResponses.remove(reqId);
|
||||||
|
throw getRuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
// strip a single leading tab if present (protocol formatting)
|
ParsedResponse pr;
|
||||||
if (line.startsWith("\t")) {
|
try {
|
||||||
line = line.substring(1);
|
pr = q.take();
|
||||||
}
|
} catch (InterruptedException e) {
|
||||||
response.add(line);
|
Thread.currentThread().interrupt();
|
||||||
}
|
pendingResponses.remove(reqId);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
pendingResponses.remove(reqId);
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasStatus) {
|
if (pr.success) {
|
||||||
// Fallback for servers that do not place the status on a
|
return pr.lines;
|
||||||
// dedicated line: try to detect status markers anywhere
|
}
|
||||||
// in the payload to remain compatible with older servers.
|
|
||||||
if (responseText.contains("+OK")) {
|
|
||||||
success = true;
|
|
||||||
hasStatus = true;
|
|
||||||
} else if (responseText.contains("-ERR")
|
|
||||||
|| responseText.contains("-ERROR")) {
|
|
||||||
success = false;
|
|
||||||
hasStatus = true;
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException(
|
|
||||||
"No status line in response for '"
|
|
||||||
+ message
|
|
||||||
+ "': "
|
|
||||||
+ responseText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success) {
|
throw new RuntimeException("Error in " + message + ": " + pr.lines);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new RuntimeException("Error in " + message + ": " + response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw getRuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,7 +346,17 @@ public class ClientService {
|
|||||||
*/
|
*/
|
||||||
public void closeSocket() {
|
public void closeSocket() {
|
||||||
try {
|
try {
|
||||||
executor.shutdown();
|
running.set(false);
|
||||||
|
if (readerThread != null) {
|
||||||
|
readerThread.interrupt();
|
||||||
|
try {
|
||||||
|
readerThread.join(READER_JOIN_TIMEOUT_MS);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.shutdownNow();
|
||||||
clienttcptransport.close();
|
clienttcptransport.close();
|
||||||
socket.close();
|
socket.close();
|
||||||
logger.info("Socket closed");
|
logger.info("Socket closed");
|
||||||
@@ -257,16 +365,9 @@ public class ClientService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// removed unused helper writeToTransport; write is done via processCommand()
|
||||||
* Method to write with the tcp transport to the server
|
// which
|
||||||
*
|
// manages request ids and response matching
|
||||||
* @param s - Message to be sent
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private void writeToTransport(String s) throws IOException {
|
|
||||||
int id = this.idGenerator.incrementAndGet();
|
|
||||||
this.clienttcptransport.write(new RawPacket(id, s));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ping() {
|
public void ping() {
|
||||||
processCommand("PING");
|
processCommand("PING");
|
||||||
|
|||||||
@@ -34,30 +34,37 @@ public class GameClient {
|
|||||||
* @return A GameState object representing the current state of the game, as parsed
|
* @return A GameState object representing the current state of the game, as parsed
|
||||||
*/
|
*/
|
||||||
public GameState fetchGameState() {
|
public GameState fetchGameState() {
|
||||||
List<String> responseLines = client.processCommand("GET_GAME_STATE\nGAME_ID=" + gameId);
|
List<String> responseLines = client.processCommand("GET_GAME_STATE GAME_ID=" + gameId);
|
||||||
|
|
||||||
if (responseLines == null || responseLines.isEmpty()) {
|
if (responseLines == null || responseLines.isEmpty()) {
|
||||||
throw new RuntimeException("Empty server response");
|
throw new RuntimeException("Empty server response");
|
||||||
}
|
}
|
||||||
|
|
||||||
String fullResponse = String.join("\n", responseLines);
|
if (responseLines.get(0).startsWith("-ERR")) {
|
||||||
|
throw new RuntimeException("Server Error: " + String.join(" ", responseLines));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!responseLines.get(0).startsWith("+OK")) {
|
||||||
|
throw new RuntimeException("Invalid response: missing +OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
String fullResponse = String.join("\n", responseLines);
|
||||||
return parse(fullResponse);
|
return parse(fullResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Send a CALL command to the server to indicate that the player wants to call */
|
/** Send a CALL command to the server to indicate that the player wants to call */
|
||||||
public void sendCall() {
|
public void sendCall() {
|
||||||
client.processCommand("CALL\nGAME_ID=" + gameId);
|
client.processCommand("CALL GAME_ID=" + gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Send a FOLD command to the server to indicate that the player wants to fold */
|
/** Send a FOLD command to the server to indicate that the player wants to fold */
|
||||||
public void sendFold() {
|
public void sendFold() {
|
||||||
client.processCommand("FOLD\nGAME_ID=" + gameId);
|
client.processCommand("FOLD GAME_ID=" + gameId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Send a BET command to the server to indicate that the player wants to bet */
|
/** Send a BET command to the server to indicate that the player wants to bet */
|
||||||
public void sendBet(int amount) {
|
public void sendBet(int amount) {
|
||||||
client.processCommand("BET\nGAME_ID=" + gameId + "\nAMOUNT=" + amount);
|
client.processCommand("BET GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +73,7 @@ public class GameClient {
|
|||||||
* @param amount The amount to raise to
|
* @param amount The amount to raise to
|
||||||
*/
|
*/
|
||||||
public void sendRaise(int amount) {
|
public void sendRaise(int amount) {
|
||||||
client.processCommand("RAISE\nGAME_ID=" + gameId + "\nAMOUNT=" + amount);
|
client.processCommand("RAISE GAME_ID=" + gameId + " AMOUNT=" + amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,8 +83,12 @@ public class GameClient {
|
|||||||
* @return A GameState object representing the current state of the game.
|
* @return A GameState object representing the current state of the game.
|
||||||
*/
|
*/
|
||||||
private GameState parse(String input) {
|
private GameState parse(String input) {
|
||||||
GameState state = new GameState();
|
|
||||||
|
|
||||||
|
if (input.startsWith("-ERR")) {
|
||||||
|
throw new RuntimeException("Server returned Error:\n" + input);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState state = new GameState();
|
||||||
ParserContext ctx = new ParserContext(state);
|
ParserContext ctx = new ParserContext(state);
|
||||||
|
|
||||||
for (String raw : input.split("\n")) {
|
for (String raw : input.split("\n")) {
|
||||||
@@ -156,6 +167,7 @@ public class GameClient {
|
|||||||
state.winnerIndex = intVal(line);
|
state.winnerIndex = intVal(line);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
|
// throw new RuntimeException("Unknown global field: " + line);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -171,7 +183,7 @@ public class GameClient {
|
|||||||
private boolean handleSections(String line, ParserContext ctx) {
|
private boolean handleSections(String line, ParserContext ctx) {
|
||||||
|
|
||||||
if (line.equals("END")) {
|
if (line.equals("END")) {
|
||||||
ctx.inPlayers = false;
|
// ctx.inPlayers = false;
|
||||||
ctx.inPlayerCards = false;
|
ctx.inPlayerCards = false;
|
||||||
ctx.inCommunityCards = false;
|
ctx.inCommunityCards = false;
|
||||||
ctx.currentPlayer = null;
|
ctx.currentPlayer = null;
|
||||||
|
|||||||
@@ -32,7 +32,25 @@ public class LobbyClient {
|
|||||||
* @return A string representing the current status of the lobby, as returned by the server.
|
* @return A string representing the current status of the lobby, as returned by the server.
|
||||||
*/
|
*/
|
||||||
public String fetchLobbyStatusString(int lobbyId) {
|
public String fetchLobbyStatusString(int lobbyId) {
|
||||||
return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId).getFirst();
|
List<String> lines = client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
|
||||||
|
|
||||||
|
// Prefer explicit STATUS parameter when available
|
||||||
|
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||||
|
for (RequestParameter p : params) {
|
||||||
|
if ("STATUS".equalsIgnoreCase(p.key())) {
|
||||||
|
return p.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: some servers may return a plain token as the first line
|
||||||
|
if (!lines.isEmpty()) {
|
||||||
|
String first = lines.get(0).trim();
|
||||||
|
if ("CREATED".equalsIgnoreCase(first) || "RUNNING".equalsIgnoreCase(first)) {
|
||||||
|
return first.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,8 +59,25 @@ public class LobbyClient {
|
|||||||
* @return The id of the newly created lobby, as returned by the server.
|
* @return The id of the newly created lobby, as returned by the server.
|
||||||
*/
|
*/
|
||||||
public int createLobby() {
|
public int createLobby() {
|
||||||
String response = client.processCommand("CREATE_LOBBY").getFirst();
|
List<String> lines = client.processCommand("CREATE_LOBBY");
|
||||||
return Integer.parseInt(response);
|
|
||||||
|
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||||
|
for (RequestParameter p : params) {
|
||||||
|
if ("LOBBY_ID".equalsIgnoreCase(p.key())) {
|
||||||
|
return Integer.parseInt(p.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for simple legacy/test servers that return the id as a single plain
|
||||||
|
// line
|
||||||
|
if (!lines.isEmpty()) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(lines.get(0));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException("No LOBBY_ID in response: " + lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,8 +86,15 @@ public class LobbyClient {
|
|||||||
* @return The id of the lobby that the client is currently in, as returned by the server.
|
* @return The id of the lobby that the client is currently in, as returned by the server.
|
||||||
*/
|
*/
|
||||||
public int getLobbyId() {
|
public int getLobbyId() {
|
||||||
String response = client.processCommand("GET_LOBBY_ID").getFirst();
|
List<String> lines = client.processCommand("GET_LOBBY_ID");
|
||||||
return Integer.parseInt(response);
|
if (lines.isEmpty()) {
|
||||||
|
throw new RuntimeException("GET_LOBBY_ID returned empty response");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(lines.get(0).trim());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new RuntimeException("Invalid GET_LOBBY_ID response: " + lines, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+89
-26
@@ -5,7 +5,13 @@ import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
@@ -13,6 +19,7 @@ import javafx.scene.control.MenuButton;
|
|||||||
import javafx.scene.control.MenuItem;
|
import javafx.scene.control.MenuItem;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.control.TabPane;
|
import javafx.scene.control.TabPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
@@ -28,15 +35,21 @@ public class ChatBoxController {
|
|||||||
|
|
||||||
@FXML private MenuButton addWhisperChatButton;
|
@FXML private MenuButton addWhisperChatButton;
|
||||||
|
|
||||||
|
@FXML private HBox menuBox;
|
||||||
|
|
||||||
private FXMLLoader fxmlLoader;
|
private FXMLLoader fxmlLoader;
|
||||||
|
|
||||||
@FXML private List<MenuItem> whisperUsers;
|
private final List<String> activeWhisperChats;
|
||||||
|
|
||||||
|
@FXML private final Map<String, Tab> usernameTabMap;
|
||||||
|
|
||||||
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
||||||
|
|
||||||
public ChatBoxController(String username, ChatController chatController) {
|
public ChatBoxController(String username, ChatController chatController) {
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.chatController = chatController;
|
this.chatController = chatController;
|
||||||
|
activeWhisperChats = new ArrayList<>();
|
||||||
|
usernameTabMap = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +64,7 @@ public class ChatBoxController {
|
|||||||
.getChatModelMap()
|
.getChatModelMap()
|
||||||
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
|
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
|
||||||
addChatTab("GLOBAL", globalChatModel);
|
addChatTab("GLOBAL", globalChatModel);
|
||||||
// TODO: Button to add new Whisper Chat
|
addWhisperChatButton.setOnAction(event -> addWhisperChatButton.show());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,10 +74,20 @@ public class ChatBoxController {
|
|||||||
* @param targetUserName The username of the person to be added to the whisper list.
|
* @param targetUserName The username of the person to be added to the whisper list.
|
||||||
*/
|
*/
|
||||||
public void addWhisperUser(String targetUserName) {
|
public void addWhisperUser(String targetUserName) {
|
||||||
MenuItem menuItem = new MenuItem(targetUserName);
|
Platform.runLater(
|
||||||
whisperUsers.add(menuItem);
|
() -> {
|
||||||
addWhisperChatButton.getItems().add(menuItem);
|
MenuItem menuItem = new MenuItem(targetUserName);
|
||||||
menuItem.setOnAction(event -> addWhisperChat(targetUserName));
|
addWhisperChatButton.getItems().add(menuItem);
|
||||||
|
menuItem.setOnAction(
|
||||||
|
event ->
|
||||||
|
addWhisperChat(
|
||||||
|
targetUserName,
|
||||||
|
new ChatModel(
|
||||||
|
ChatType.WHISPER,
|
||||||
|
username,
|
||||||
|
-1,
|
||||||
|
targetUserName)));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,12 +96,16 @@ public class ChatBoxController {
|
|||||||
*
|
*
|
||||||
* @param target The username of the recipient for the private messages.
|
* @param target The username of the recipient for the private messages.
|
||||||
*/
|
*/
|
||||||
public void addWhisperChat(String target) {
|
public void addWhisperChat(String target, ChatModel chatModel) {
|
||||||
ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, -1, target);
|
if (!activeWhisperChats.contains(target)) {
|
||||||
chatController
|
activeWhisperChats.add(target);
|
||||||
.getChatModelMap()
|
chatController
|
||||||
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
|
.getChatModelMap()
|
||||||
addChatTab(target, chatModel);
|
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
|
||||||
|
addChatTab(target, chatModel);
|
||||||
|
} else {
|
||||||
|
chatTabPane.getSelectionModel().select(usernameTabMap.get(target));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,20 +120,56 @@ public class ChatBoxController {
|
|||||||
public void addChatTab(String title, ChatModel chatModel) {
|
public void addChatTab(String title, ChatModel chatModel) {
|
||||||
URL resource = getClass().getResource(ressource);
|
URL resource = getClass().getResource(ressource);
|
||||||
FXMLLoader fxmlLoader = new FXMLLoader(resource);
|
FXMLLoader fxmlLoader = new FXMLLoader(resource);
|
||||||
try {
|
runOnPlatformSynchronized(
|
||||||
ChatViewController chatViewController =
|
() -> {
|
||||||
new ChatViewController(this.chatController, chatModel, this.username);
|
try {
|
||||||
fxmlLoader.setController(chatViewController);
|
ChatViewController chatViewController =
|
||||||
Node load = fxmlLoader.load();
|
new ChatViewController(
|
||||||
VBox.setVgrow(load, Priority.ALWAYS);
|
this.chatController, chatModel, this.username);
|
||||||
VBox vbox = new VBox();
|
fxmlLoader.setController(chatViewController);
|
||||||
VBox.setVgrow(vbox, Priority.ALWAYS);
|
Node load = fxmlLoader.load();
|
||||||
vbox.getChildren().add(load);
|
VBox.setVgrow(load, Priority.ALWAYS);
|
||||||
chatModel.addListener((msg) -> chatViewController.showMessage(msg));
|
VBox vbox = new VBox();
|
||||||
Tab newChat = new Tab(title, vbox);
|
VBox.setVgrow(vbox, Priority.ALWAYS);
|
||||||
this.chatTabPane.getTabs().add(newChat);
|
vbox.getChildren().add(load);
|
||||||
} catch (IOException e) {
|
chatModel.addListener((msg) -> chatViewController.showMessage(msg));
|
||||||
throw new RuntimeException(e);
|
Tab newChat = new Tab(title, vbox);
|
||||||
|
usernameTabMap.put(title, newChat);
|
||||||
|
this.chatTabPane.getTabs().add(newChat);
|
||||||
|
this.chatTabPane.getSelectionModel().select(newChat);
|
||||||
|
return newChat;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper Function to cope with a Threads problem occurring when the main JavaFX Thread performs
|
||||||
|
* the task to add a new whisper Chat tab, and the task is specified to runLater.
|
||||||
|
*/
|
||||||
|
public static <T> T runOnPlatformSynchronized(Callable<T> code) {
|
||||||
|
if (Platform.isFxApplicationThread()) {
|
||||||
|
try {
|
||||||
|
return code.call();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
CompletableFuture<T> futureLock = new CompletableFuture<>();
|
||||||
|
Platform.runLater(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
futureLock.complete(code.call());
|
||||||
|
} catch (Exception e) {
|
||||||
|
futureLock.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
return futureLock.get();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-27
@@ -48,6 +48,7 @@ public class CasinoGameController {
|
|||||||
private PlayerId myPlayerId;
|
private PlayerId myPlayerId;
|
||||||
private TaskbarController taskbarController;
|
private TaskbarController taskbarController;
|
||||||
private Image dealerImage;
|
private Image dealerImage;
|
||||||
|
private javafx.animation.Timeline timeline;
|
||||||
|
|
||||||
private static final int TOTAL_SLOTS = 5;
|
private static final int TOTAL_SLOTS = 5;
|
||||||
private static final int PLAYER_SLOTS = 2;
|
private static final int PLAYER_SLOTS = 2;
|
||||||
@@ -166,6 +167,8 @@ public class CasinoGameController {
|
|||||||
|
|
||||||
// uiTest();
|
// uiTest();
|
||||||
|
|
||||||
|
// or
|
||||||
|
|
||||||
// empty display only (optional)
|
// empty display only (optional)
|
||||||
renderCommunityCards(List.of());
|
renderCommunityCards(List.of());
|
||||||
renderPlayerCards(List.of());
|
renderPlayerCards(List.of());
|
||||||
@@ -277,42 +280,69 @@ public class CasinoGameController {
|
|||||||
*/
|
*/
|
||||||
private void startLoop() {
|
private void startLoop() {
|
||||||
|
|
||||||
javafx.animation.Timeline t =
|
timeline =
|
||||||
new javafx.animation.Timeline(
|
new javafx.animation.Timeline(
|
||||||
new javafx.animation.KeyFrame(
|
new javafx.animation.KeyFrame(
|
||||||
javafx.util.Duration.seconds(UI_UPDATE_INTERVAL_SECONDS),
|
javafx.util.Duration.seconds(UI_UPDATE_INTERVAL_SECONDS),
|
||||||
e -> updateUI()));
|
e -> updateUI()));
|
||||||
|
|
||||||
t.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
timeline.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
||||||
t.play();
|
timeline.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the UI by fetching the latest game state from the GameService and updating all
|
* Stop the UI update loop, which can be called when the game ends or when the user exits the
|
||||||
* relevant components.
|
* game screen to prevent unnecessary updates and resource usage.
|
||||||
|
*/
|
||||||
|
public void stop() {
|
||||||
|
if (timeline != null) {
|
||||||
|
timeline.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers an asynchronous UI update by fetching the current game state from the GameService.
|
||||||
*/
|
*/
|
||||||
private void updateUI() {
|
private void updateUI() {
|
||||||
|
|
||||||
try {
|
java.util.concurrent.CompletableFuture.supplyAsync(
|
||||||
GameState s = gameService.refresh();
|
() -> {
|
||||||
|
try {
|
||||||
|
return gameService.refresh();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.severe("GameService Error: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.thenAccept(
|
||||||
|
s -> {
|
||||||
|
if (s == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (s == null) {
|
javafx.application.Platform.runLater(() -> applyState(s));
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCommunityCards(s.communityCards);
|
/**
|
||||||
renderPot(s.pot);
|
* Updates all visual UI components with the data from the provided GameState. This includes
|
||||||
|
* community cards, pot, player information, and the taskbar.
|
||||||
|
*
|
||||||
|
* @param s The current state of the game to be rendered.
|
||||||
|
*/
|
||||||
|
private void applyState(GameState s) {
|
||||||
|
|
||||||
updatePlayers(s.players);
|
renderCommunityCards(s.communityCards);
|
||||||
updatePlayerCards(s);
|
renderPot(s.pot);
|
||||||
|
|
||||||
updateGameInfo(s);
|
updatePlayers(s.players);
|
||||||
highlightDealer(s);
|
updatePlayerCards(s);
|
||||||
|
|
||||||
updateTaskbar(s);
|
updateGameInfo(s);
|
||||||
|
highlightDealer(s);
|
||||||
|
|
||||||
} catch (Exception e) {
|
if (taskbarController != null) {
|
||||||
LOGGER.severe("Error: UI Update failed: " + e.getMessage());
|
taskbarController.update(s, myPlayerId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,14 +353,20 @@ public class CasinoGameController {
|
|||||||
*/
|
*/
|
||||||
private void updatePlayerCards(GameState s) {
|
private void updatePlayerCards(GameState s) {
|
||||||
|
|
||||||
int active = s.activePlayer;
|
if (s.players == null || myPlayerId == null) {
|
||||||
|
|
||||||
if (active >= 0 && active < s.players.size()) {
|
|
||||||
Player p = s.players.get(active);
|
|
||||||
renderPlayerCards(p.getCards());
|
|
||||||
} else {
|
|
||||||
renderPlayerCards(List.of());
|
renderPlayerCards(List.of());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (Player p : s.players) {
|
||||||
|
if (p.getId().equals(myPlayerId)) {
|
||||||
|
renderPlayerCards(p.getCards());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback
|
||||||
|
renderPlayerCards(List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -357,8 +393,9 @@ public class CasinoGameController {
|
|||||||
* @param s The current game state.
|
* @param s The current game state.
|
||||||
*/
|
*/
|
||||||
private void updateTaskbar(GameState s) {
|
private void updateTaskbar(GameState s) {
|
||||||
|
if (taskbarController != null) {
|
||||||
taskbarController.update(s, myPlayerId);
|
taskbarController.update(s, myPlayerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+19
-2
@@ -387,9 +387,26 @@ public class TaskbarController {
|
|||||||
currentStage.close();
|
currentStage.close();
|
||||||
// Start lobby UI
|
// Start lobby UI
|
||||||
try {
|
try {
|
||||||
new Casinomainui().start(new javafx.stage.Stage());
|
javafx.stage.Stage newStage = new javafx.stage.Stage();
|
||||||
|
javafx.fxml.FXMLLoader fxmlLoader =
|
||||||
|
new javafx.fxml.FXMLLoader(
|
||||||
|
getClass().getResource("/ui-structure/Casinomainui.fxml"));
|
||||||
|
javafx.scene.Parent root = fxmlLoader.load();
|
||||||
|
javafx.scene.Scene scene =
|
||||||
|
new javafx.scene.Scene(
|
||||||
|
root, Casinomainui.SCENE_WIDTH, Casinomainui.SCENE_HEIGHT);
|
||||||
|
newStage.setTitle("Casono");
|
||||||
|
javafx.scene.image.Image icon =
|
||||||
|
new javafx.scene.image.Image(
|
||||||
|
getClass()
|
||||||
|
.getResource("/images/logoinverted.png")
|
||||||
|
.toExternalForm());
|
||||||
|
newStage.getIcons().add(icon);
|
||||||
|
newStage.setScene(scene);
|
||||||
|
newStage.setFullScreen(true);
|
||||||
|
newStage.show();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Error: starting the lobby UI: {}", e.getMessage());
|
LOGGER.error("Error: starting the lobby UI: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ public class Casinomainui extends Application {
|
|||||||
// Default constructor
|
// Default constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int SCENE_WIDTH = 1200;
|
public static final int SCENE_WIDTH = 1200;
|
||||||
private static final int SCENE_HEIGHT = 800;
|
public static final int SCENE_HEIGHT = 800;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
/**
|
/**
|
||||||
|
|||||||
+55
-10
@@ -2,6 +2,7 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
|||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -53,7 +54,7 @@ public class LobbyButtonGridManager {
|
|||||||
this.translationManager = LobbyButtonTranslationManager.getInstance();
|
this.translationManager = LobbyButtonTranslationManager.getInstance();
|
||||||
this.lobbyClient = lobbyClient;
|
this.lobbyClient = lobbyClient;
|
||||||
|
|
||||||
startPeriodicRefresh(REFRESH_INTERVAL_SECONDS, INITIAL_DELAY_SECONDS);
|
startPeriodicRefresh(INITIAL_DELAY_SECONDS, REFRESH_INTERVAL_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LobbyButtonGridManager(
|
public LobbyButtonGridManager(
|
||||||
@@ -62,6 +63,43 @@ public class LobbyButtonGridManager {
|
|||||||
ClientService clientService) {
|
ClientService clientService) {
|
||||||
|
|
||||||
this(gridPane, translationManager, new LobbyClient(clientService));
|
this(gridPane, translationManager, new LobbyClient(clientService));
|
||||||
|
|
||||||
|
// Subscribe to server-initiated events so closed lobbies are removed
|
||||||
|
// immediately
|
||||||
|
clientService.addEventListener(
|
||||||
|
lines -> {
|
||||||
|
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
|
||||||
|
String evt = null;
|
||||||
|
String lidStr = null;
|
||||||
|
for (RequestParameter p : params) {
|
||||||
|
if ("EVENT".equalsIgnoreCase(p.key())) {
|
||||||
|
evt = p.value();
|
||||||
|
} else if ("LOBBY_ID".equalsIgnoreCase(p.key())) {
|
||||||
|
lidStr = p.value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (evt != null && "LOBBY_CLOSED".equalsIgnoreCase(evt) && lidStr != null) {
|
||||||
|
int lid;
|
||||||
|
try {
|
||||||
|
lid = Integer.parseInt(lidStr);
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// find button id(s) for this lobby and remove mapping
|
||||||
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
Integer toRemove = null;
|
||||||
|
for (Map.Entry<Integer, Integer> e : mapping.entrySet()) {
|
||||||
|
if (e.getValue() != null && e.getValue().intValue() == lid) {
|
||||||
|
toRemove = e.getKey();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toRemove != null) {
|
||||||
|
translationManager.removeLobbyButton(toRemove);
|
||||||
|
javafx.application.Platform.runLater(this::renderLobbyButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startPeriodicRefresh(long initialDelay, long period) {
|
private void startPeriodicRefresh(long initialDelay, long period) {
|
||||||
@@ -80,7 +118,11 @@ public class LobbyButtonGridManager {
|
|||||||
|
|
||||||
for (Map.Entry<Integer, Integer> e : entries) {
|
for (Map.Entry<Integer, Integer> e : entries) {
|
||||||
int buttonId = e.getKey();
|
int buttonId = e.getKey();
|
||||||
int lobbyId = e.getValue();
|
Integer lobbyIdObj = e.getValue();
|
||||||
|
if (lobbyIdObj == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int lobbyId = lobbyIdObj.intValue();
|
||||||
|
|
||||||
CompletableFuture.supplyAsync(
|
CompletableFuture.supplyAsync(
|
||||||
() -> {
|
() -> {
|
||||||
@@ -97,8 +139,7 @@ public class LobbyButtonGridManager {
|
|||||||
if (status == null) {
|
if (status == null) {
|
||||||
translationManager.removeLobbyButton(buttonId);
|
translationManager.removeLobbyButton(buttonId);
|
||||||
|
|
||||||
javafx.application.Platform.runLater(
|
javafx.application.Platform.runLater(this::renderLobbyButtons);
|
||||||
this::updateLobbyButtonImages);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -118,7 +159,11 @@ public class LobbyButtonGridManager {
|
|||||||
|
|
||||||
for (int index = 0; index < buttonIds.size(); index++) {
|
for (int index = 0; index < buttonIds.size(); index++) {
|
||||||
Integer buttonId = buttonIds.get(index);
|
Integer buttonId = buttonIds.get(index);
|
||||||
int lobbyId = mapping.get(buttonId);
|
Integer lobbyIdObj = mapping.get(buttonId);
|
||||||
|
if (lobbyIdObj == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int lobbyId = lobbyIdObj.intValue();
|
||||||
|
|
||||||
Button btn = createLobbyButton(buttonId, lobbyId);
|
Button btn = createLobbyButton(buttonId, lobbyId);
|
||||||
|
|
||||||
@@ -253,7 +298,11 @@ public class LobbyButtonGridManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (Integer buttonId : mapping.keySet()) {
|
for (Integer buttonId : mapping.keySet()) {
|
||||||
int lobbyId = mapping.get(buttonId);
|
Integer lobbyIdObj = mapping.get(buttonId);
|
||||||
|
if (lobbyIdObj == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int lobbyId = lobbyIdObj.intValue();
|
||||||
|
|
||||||
CompletableFuture.supplyAsync(
|
CompletableFuture.supplyAsync(
|
||||||
() -> {
|
() -> {
|
||||||
@@ -361,8 +410,4 @@ public class LobbyButtonGridManager {
|
|||||||
public LobbyClient getLobbyClient() {
|
public LobbyClient getLobbyClient() {
|
||||||
return lobbyClient;
|
return lobbyClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshNow() {
|
|
||||||
refreshMappings();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandR
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.DisconnectEvent;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.events.EventBus;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.Session;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionDisconnectJob;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionManager;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -51,6 +55,9 @@ public class ServerApp {
|
|||||||
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
|
private static final int SESSION_DISCONNECT_JOB_DELAY = 0;
|
||||||
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
|
private static final int SESSION_DISCONNECT_JOB_PERIOD = 2;
|
||||||
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5;
|
private static final int SESSION_DISCONNECT_JOB_TIMEOUT = 5;
|
||||||
|
private static final int LOBBY_EXPIRY_SECONDS = 30;
|
||||||
|
private static final int LOBBY_CLEANUP_INITIAL_DELAY_SECONDS = 5;
|
||||||
|
private static final int LOBBY_CLEANUP_PERIOD_SECONDS = 5;
|
||||||
|
|
||||||
public static void start(String arg) {
|
public static void start(String arg) {
|
||||||
int port = Integer.parseInt(arg);
|
int port = Integer.parseInt(arg);
|
||||||
@@ -89,6 +96,41 @@ public class ServerApp {
|
|||||||
LobbyManager lobbyManager = new LobbyManager();
|
LobbyManager lobbyManager = new LobbyManager();
|
||||||
registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager);
|
registerCommands(dispatcher, router, responseDispatcher, userRegistry, lobbyManager);
|
||||||
|
|
||||||
|
// Periodic cleanup: remove empty lobbies older than 30s and notify affected
|
||||||
|
// users
|
||||||
|
scheduler.scheduleAtFixedRate(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
var expired =
|
||||||
|
lobbyManager.findEmptyLobbiesOlderThan(
|
||||||
|
Duration.ofSeconds(LOBBY_EXPIRY_SECONDS));
|
||||||
|
for (var lid : expired) {
|
||||||
|
// remove lobby from manager first
|
||||||
|
lobbyManager.removeLobby(lid);
|
||||||
|
|
||||||
|
// broadcast LOBBY_CLOSED event to all connected sessions
|
||||||
|
// (requestId=0)
|
||||||
|
for (Session s : sessionManager.getAllSessions()) {
|
||||||
|
RequestContext ctx = new RequestContext(s.getId(), 0);
|
||||||
|
SuccessResponse ev =
|
||||||
|
new SuccessResponse(
|
||||||
|
ctx,
|
||||||
|
ResponseBody.builder()
|
||||||
|
.param("EVENT", "LOBBY_CLOSED")
|
||||||
|
.param("LOBBY_ID", lid.value())
|
||||||
|
.build()) {};
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Lobby expiry job failed", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LOBBY_CLEANUP_INITIAL_DELAY_SECONDS,
|
||||||
|
LOBBY_CLEANUP_PERIOD_SECONDS,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
|
|
||||||
NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
|
NetworkManager networkManager = new NetworkManager(port, sessionManager, router);
|
||||||
networkManager.start();
|
networkManager.start();
|
||||||
}
|
}
|
||||||
@@ -239,6 +281,20 @@ public class ServerApp {
|
|||||||
.get_lobby_status.GetLobbyStatusHandler(
|
.get_lobby_status.GetLobbyStatusHandler(
|
||||||
responseDispatcher, lobbyManager, userRegistry));
|
responseDispatcher, lobbyManager, userRegistry));
|
||||||
|
|
||||||
|
// CREATE_LOBBY registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"CREATE_LOBBY",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||||
|
.CreateLobbyParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||||
|
.CreateLobbyRequest.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
|
||||||
|
.create_lobby.CreateLobbyRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
|
||||||
|
.CreateLobbyHandler(responseDispatcher, lobbyManager));
|
||||||
|
|
||||||
// JOIN_LOBBY registration
|
// JOIN_LOBBY registration
|
||||||
parserDispatcher.register(
|
parserDispatcher.register(
|
||||||
"JOIN_LOBBY",
|
"JOIN_LOBBY",
|
||||||
@@ -252,5 +308,19 @@ public class ServerApp {
|
|||||||
.JoinLobbyRequest>)
|
.JoinLobbyRequest>)
|
||||||
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
|
||||||
.JoinLobbyHandler(responseDispatcher, lobbyManager, userRegistry));
|
.JoinLobbyHandler(responseDispatcher, lobbyManager, userRegistry));
|
||||||
|
|
||||||
|
// START_GAME registration
|
||||||
|
parserDispatcher.register(
|
||||||
|
"START_GAME",
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||||
|
.StartGameParser());
|
||||||
|
commandRouter.register(
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||||
|
.StartGameRequest.class,
|
||||||
|
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler<
|
||||||
|
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||||
|
.StartGameRequest>)
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
|
||||||
|
.StartGameHandler(responseDispatcher, lobbyManager, userRegistry));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
|
||||||
|
public class CreateLobbyHandler extends CommandHandler<CreateLobbyRequest> {
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
|
||||||
|
public CreateLobbyHandler(ResponseDispatcher responseDispatcher, LobbyManager lobbyManager) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CreateLobbyRequest request) {
|
||||||
|
LobbyId id = lobbyManager.createNewLobby(null);
|
||||||
|
|
||||||
|
if (id == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(),
|
||||||
|
"LOBBIES_FULL",
|
||||||
|
"Maximum number of 8 lobbies reached"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(new CreateLobbyResponse(request.getContext(), id.value()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
|
||||||
|
public class CreateLobbyParser implements CommandParser<CreateLobbyRequest> {
|
||||||
|
@Override
|
||||||
|
public CreateLobbyRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
return new CreateLobbyRequest(primitiveRequest.context());
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
public class CreateLobbyRequest extends Request {
|
||||||
|
public CreateLobbyRequest(RequestContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
|
||||||
|
|
||||||
|
/** Sends ID of newly created lobby back to client. */
|
||||||
|
public class CreateLobbyResponse extends SuccessResponse {
|
||||||
|
public CreateLobbyResponse(RequestContext context, int lobbyId) {
|
||||||
|
super(
|
||||||
|
context,
|
||||||
|
new ResponseBodyBuilder().param("LOBBY_ID", String.valueOf(lobbyId)).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -22,10 +22,18 @@ public class GetLobbyStatusResponse extends SuccessResponse {
|
|||||||
super(
|
super(
|
||||||
context,
|
context,
|
||||||
ResponseBody.builder()
|
ResponseBody.builder()
|
||||||
|
.param("STATUS", lobby.getGameController() != null ? "RUNNING" : "CREATED")
|
||||||
.block(
|
.block(
|
||||||
"LOBBY",
|
"LOBBY",
|
||||||
lb -> {
|
lb -> {
|
||||||
lb.param("ID", lobby.getId().value());
|
lb.param("ID", lobby.getId().value());
|
||||||
|
// STATUS indicates whether a game has been started in this
|
||||||
|
// lobby
|
||||||
|
lb.param(
|
||||||
|
"STATUS",
|
||||||
|
lobby.getGameController() == null
|
||||||
|
? "CREATED"
|
||||||
|
: "RUNNING");
|
||||||
lb.param("NAME", lobby.getName());
|
lb.param("NAME", lobby.getName());
|
||||||
lb.block(
|
lb.block(
|
||||||
"PLAYERS",
|
"PLAYERS",
|
||||||
|
|||||||
+20
-1
@@ -9,6 +9,8 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandH
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for the `JOIN_LOBBY` command.
|
* Handler for the `JOIN_LOBBY` command.
|
||||||
@@ -21,6 +23,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatch
|
|||||||
public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
|
public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
|
||||||
private final LobbyManager lobbyManager;
|
private final LobbyManager lobbyManager;
|
||||||
private final UserRegistry userRegistry;
|
private final UserRegistry userRegistry;
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(JoinLobbyHandler.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link JoinLobbyHandler}.
|
* Create a new {@link JoinLobbyHandler}.
|
||||||
@@ -46,9 +49,17 @@ public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void execute(JoinLobbyRequest request) {
|
public void execute(JoinLobbyRequest request) {
|
||||||
|
LOGGER.info(
|
||||||
|
"JOIN_LOBBY request: session={}, lobbyId={}",
|
||||||
|
request.getContext().sessionId(),
|
||||||
|
request.getId());
|
||||||
LobbyId lid = LobbyId.of(request.getId());
|
LobbyId lid = LobbyId.of(request.getId());
|
||||||
var lobby = lobbyManager.getLobby(lid);
|
var lobby = lobbyManager.getLobby(lid);
|
||||||
if (lobby == null) {
|
if (lobby == null) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"JOIN_LOBBY: Lobby {} not found (session={})",
|
||||||
|
request.getId(),
|
||||||
|
request.getContext().sessionId());
|
||||||
responseDispatcher.dispatch(
|
responseDispatcher.dispatch(
|
||||||
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||||
return;
|
return;
|
||||||
@@ -56,7 +67,9 @@ public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
|
|||||||
|
|
||||||
var maybeUser = userRegistry.getBySessionId(request.getContext().sessionId());
|
var maybeUser = userRegistry.getBySessionId(request.getContext().sessionId());
|
||||||
if (maybeUser.isEmpty()) {
|
if (maybeUser.isEmpty()) {
|
||||||
// UserLoggedInCheck should normally prevent this; keep safe fallback
|
LOGGER.warn(
|
||||||
|
"JOIN_LOBBY: No user associated with session {}",
|
||||||
|
request.getContext().sessionId());
|
||||||
responseDispatcher.dispatch(
|
responseDispatcher.dispatch(
|
||||||
new ErrorResponse(
|
new ErrorResponse(
|
||||||
request.getContext(),
|
request.getContext(),
|
||||||
@@ -70,6 +83,10 @@ public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
|
|||||||
|
|
||||||
boolean ok = lobbyManager.addPlayerToLobby(username, lid);
|
boolean ok = lobbyManager.addPlayerToLobby(username, lid);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
LOGGER.warn(
|
||||||
|
"JOIN_LOBBY: User '{}' failed to join lobby {} (full or already in)",
|
||||||
|
username,
|
||||||
|
request.getId());
|
||||||
responseDispatcher.dispatch(
|
responseDispatcher.dispatch(
|
||||||
new ErrorResponse(
|
new ErrorResponse(
|
||||||
request.getContext(),
|
request.getContext(),
|
||||||
@@ -78,6 +95,8 @@ public class JoinLobbyHandler extends CommandHandler<JoinLobbyRequest> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGGER.info("User '{}' joined lobby {}", username, request.getId());
|
||||||
|
|
||||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.checks.UserLoggedInCheck;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.GameController;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.GameEngine;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.RoundManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.engine.TurnManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.player.PlayerId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.rules.RuleEngine;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.game.state.GameState;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyId;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for START_GAME: creates a GameEngine+GameController for the lobby and starts the game.
|
||||||
|
*/
|
||||||
|
public class StartGameHandler extends CommandHandler<StartGameRequest> {
|
||||||
|
private final LobbyManager lobbyManager;
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
private static final int DEFAULT_START_CHIPS = 20000;
|
||||||
|
|
||||||
|
public StartGameHandler(
|
||||||
|
ResponseDispatcher responseDispatcher,
|
||||||
|
LobbyManager lobbyManager,
|
||||||
|
UserRegistry userRegistry) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.lobbyManager = lobbyManager;
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
addCheck(new UserLoggedInCheck(userRegistry));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(StartGameRequest request) {
|
||||||
|
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> opt =
|
||||||
|
userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (opt.isEmpty()) {
|
||||||
|
// Guard: UserLoggedInCheck should normally handle this
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "USER_NOT_LOGGED_IN", "User not logged in"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = opt.get().getName();
|
||||||
|
|
||||||
|
// Resolve lobby by provided ID (required)
|
||||||
|
LobbyId lid = LobbyId.of(request.getId());
|
||||||
|
Lobby lobby = lobbyManager.getLobby(lid);
|
||||||
|
if (lobby == null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(request.getContext(), "LOBBY_NOT_FOUND", "Lobby not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure requester is part of the lobby
|
||||||
|
if (!lobby.getPlayerNames().contains(username)) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "NOT_IN_LOBBY", "User not in specified lobby"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent double-start
|
||||||
|
if (lobby.getGameController() != null) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(),
|
||||||
|
"ALREADY_STARTED",
|
||||||
|
"Game has already been started for this lobby"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic player count check
|
||||||
|
if (lobby.getPlayerNames().size() < 2) {
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(),
|
||||||
|
"NOT_ENOUGH_PLAYERS",
|
||||||
|
"Not enough players to start the game"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create engine + controller
|
||||||
|
GameState state = new GameState();
|
||||||
|
GameEngine engine =
|
||||||
|
new GameEngine(
|
||||||
|
state,
|
||||||
|
new RuleEngine(new ArrayList<>()),
|
||||||
|
new RoundManager(),
|
||||||
|
new TurnManager());
|
||||||
|
GameController game = new GameController(engine);
|
||||||
|
|
||||||
|
// Add all players from lobby
|
||||||
|
for (String playerName : lobby.getPlayerNames()) {
|
||||||
|
game.addPlayer(PlayerId.of(playerName), DEFAULT_START_CHIPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the game and attach to lobby
|
||||||
|
game.startGame();
|
||||||
|
lobby.initGame(game);
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(
|
||||||
|
new StartGameResponse(
|
||||||
|
request.getContext(), lid.value(), "Game started successfully"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.accessor.RequestParameterAccessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for the `START_GAME` command.
|
||||||
|
*
|
||||||
|
* <p>Expected request parameters:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>`ID` (int) — numeric lobby identifier (required)
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>The parser builds a {@link StartGameRequest} containing the parsed lobby id and the original
|
||||||
|
* request context.
|
||||||
|
*/
|
||||||
|
public class StartGameParser implements CommandParser<StartGameRequest> {
|
||||||
|
@Override
|
||||||
|
public StartGameRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
int id = accessor.require("ID", Integer::parseInt);
|
||||||
|
return new StartGameRequest(primitiveRequest.context(), id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to start a game in a specific lobby.
|
||||||
|
*
|
||||||
|
* <p>The `START_GAME` request requires the numeric lobby id parameter `ID` identifying the target
|
||||||
|
* lobby. The request carries the usual {@link RequestContext} (session id, request id) via the base
|
||||||
|
* class.
|
||||||
|
*/
|
||||||
|
public class StartGameRequest extends Request {
|
||||||
|
private final int id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link StartGameRequest}.
|
||||||
|
*
|
||||||
|
* @param context the request context
|
||||||
|
* @param id numeric lobby id to start the game in
|
||||||
|
*/
|
||||||
|
public StartGameRequest(RequestContext context, int id) {
|
||||||
|
super(context);
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the numeric lobby id provided by the client.
|
||||||
|
*
|
||||||
|
* @return lobby id
|
||||||
|
*/
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success response for {@code START_GAME} containing the started game's identifier and an
|
||||||
|
* informational message.
|
||||||
|
*/
|
||||||
|
public class StartGameResponse extends SuccessResponse {
|
||||||
|
/**
|
||||||
|
* Create a standard start-game response with a default success message.
|
||||||
|
*
|
||||||
|
* @param context the request context
|
||||||
|
* @param gameId numeric id of the started game (uses lobby id)
|
||||||
|
*/
|
||||||
|
public StartGameResponse(RequestContext context, int gameId) {
|
||||||
|
this(context, gameId, "Game started successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a start-game response with a custom message.
|
||||||
|
*
|
||||||
|
* @param context the request context
|
||||||
|
* @param gameId numeric id of the started game (uses lobby id)
|
||||||
|
* @param message informational message for the client
|
||||||
|
*/
|
||||||
|
public StartGameResponse(RequestContext context, int gameId, String message) {
|
||||||
|
super(
|
||||||
|
context,
|
||||||
|
ResponseBody.builder().param("GAME", gameId).param("MESSAGE", message).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby;
|
||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult;
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.Lobby.AddResult;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -17,6 +20,7 @@ public class LobbyManager {
|
|||||||
|
|
||||||
private final Map<LobbyId, Lobby> activeLobbies = new ConcurrentHashMap<>();
|
private final Map<LobbyId, Lobby> activeLobbies = new ConcurrentHashMap<>();
|
||||||
private final Map<String, LobbyId> playerToLobby = new ConcurrentHashMap<>();
|
private final Map<String, LobbyId> playerToLobby = new ConcurrentHashMap<>();
|
||||||
|
private final Map<LobbyId, Instant> creationTimes = new ConcurrentHashMap<>();
|
||||||
private final List<LobbyEventListener> listeners = new CopyOnWriteArrayList<>();
|
private final List<LobbyEventListener> listeners = new CopyOnWriteArrayList<>();
|
||||||
private final int maxPlayersPerLobby;
|
private final int maxPlayersPerLobby;
|
||||||
private static final Logger LOGGER = Logger.getLogger(LobbyManager.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(LobbyManager.class.getName());
|
||||||
@@ -39,6 +43,7 @@ public class LobbyManager {
|
|||||||
if (!activeLobbies.containsKey(id)) {
|
if (!activeLobbies.containsKey(id)) {
|
||||||
Lobby lobby = new Lobby(id, name == null ? ("Room " + i) : name);
|
Lobby lobby = new Lobby(id, name == null ? ("Room " + i) : name);
|
||||||
activeLobbies.put(id, lobby);
|
activeLobbies.put(id, lobby);
|
||||||
|
creationTimes.put(id, Instant.now());
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,6 +74,15 @@ public class LobbyManager {
|
|||||||
lobby.addPlayerAndMaybeStart(
|
lobby.addPlayerAndMaybeStart(
|
||||||
username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS);
|
username, maxPlayersPerLobby, AUTO_START_PLAYERS, DEFAULT_START_CHIPS);
|
||||||
if (result != AddResult.NOT_ADDED) {
|
if (result != AddResult.NOT_ADDED) {
|
||||||
|
LOGGER.info(
|
||||||
|
() ->
|
||||||
|
"User '"
|
||||||
|
+ username
|
||||||
|
+ "' joined lobby "
|
||||||
|
+ lobbyId.value()
|
||||||
|
+ " (result="
|
||||||
|
+ result
|
||||||
|
+ ")");
|
||||||
playerToLobby.put(username, lobbyId);
|
playerToLobby.put(username, lobbyId);
|
||||||
if (result == AddResult.ADDED_AND_STARTED) {
|
if (result == AddResult.ADDED_AND_STARTED) {
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
@@ -127,6 +141,35 @@ public class LobbyManager {
|
|||||||
return activeLobbies.values();
|
return activeLobbies.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Find all empty lobbies that were created more than the given {@code age} ago. */
|
||||||
|
public List<LobbyId> findEmptyLobbiesOlderThan(Duration age) {
|
||||||
|
List<LobbyId> result = new ArrayList<>();
|
||||||
|
Instant cutoff = Instant.now().minus(age);
|
||||||
|
for (Map.Entry<LobbyId, Lobby> e : activeLobbies.entrySet()) {
|
||||||
|
LobbyId id = e.getKey();
|
||||||
|
Lobby l = e.getValue();
|
||||||
|
if (l.getPlayerNames().isEmpty()) {
|
||||||
|
Instant created = creationTimes.get(id);
|
||||||
|
if (created != null && created.isBefore(cutoff)) {
|
||||||
|
result.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a lobby and clean up internal mappings. */
|
||||||
|
public void removeLobby(LobbyId id) {
|
||||||
|
Lobby removed = activeLobbies.remove(id);
|
||||||
|
creationTimes.remove(id);
|
||||||
|
if (removed == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String username : removed.getPlayerNames()) {
|
||||||
|
playerToLobby.remove(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the given action to every player username in the lobby identified by {@code lobbyId}.
|
* Apply the given action to every player username in the lobby identified by {@code lobbyId}.
|
||||||
* This is a small helper that keeps iteration logic centralized and avoids leaking internal
|
* This is a small helper that keeps iteration logic centralized and avoids leaking internal
|
||||||
|
|||||||
@@ -244,3 +244,15 @@
|
|||||||
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
-fx-background-color: #27ae60;
|
-fx-background-color: #27ae60;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#lobbyBtn-1,
|
||||||
|
#lobbyBtn-2,
|
||||||
|
#lobbyBtn-3,
|
||||||
|
#lobbyBtn-4,
|
||||||
|
#lobbyBtn-5,
|
||||||
|
#lobbyBtn-6,
|
||||||
|
#lobbyBtn-7,
|
||||||
|
#lobbyBtn-8 {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0, 0, 0, 0.8), 0, 0, 3, 3);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import javafx.application.Application;
|
|||||||
|
|
||||||
public class Chat {
|
public class Chat {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
Application.launch(ChatApplication.class);
|
Application.launch(ChatApplication.class, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.CoreClient;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.CoreClient;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
@@ -13,13 +15,14 @@ public class ChatApplication extends Application {
|
|||||||
|
|
||||||
private static final int SCENE_WIDTH = 1200;
|
private static final int SCENE_WIDTH = 1200;
|
||||||
private static final int SCENE_HEIGHT = 800;
|
private static final int SCENE_HEIGHT = 800;
|
||||||
String ip = "localhost";
|
|
||||||
String username = "mathis";
|
|
||||||
final int port = 5000;
|
|
||||||
|
|
||||||
public ChatApplication() {}
|
public ChatApplication() {}
|
||||||
|
|
||||||
public void start(Stage stage) throws IOException {
|
public void start(Stage stage) throws IOException {
|
||||||
|
List<String> params = getParameters().getRaw();
|
||||||
|
String ip = params.get(0);
|
||||||
|
int port = Integer.parseInt(params.get(1));
|
||||||
|
String username = params.get(2);
|
||||||
FXMLLoader fxmlLoader =
|
FXMLLoader fxmlLoader =
|
||||||
new FXMLLoader(
|
new FXMLLoader(
|
||||||
getClass().getResource("/ui-structure/components/chatui/chatbox.fxml"));
|
getClass().getResource("/ui-structure/components/chatui/chatbox.fxml"));
|
||||||
@@ -27,10 +30,10 @@ public class ChatApplication extends Application {
|
|||||||
CoreClient coreClient = new CoreClient(clientService);
|
CoreClient coreClient = new CoreClient(clientService);
|
||||||
coreClient.login(username);
|
coreClient.login(username);
|
||||||
ChatController chatController = new ChatController(username, clientService);
|
ChatController chatController = new ChatController(username, clientService);
|
||||||
ChatBoxController chatBoxController = new ChatBoxController(username, chatController);
|
ChatBoxController chatBoxController = chatController.getChatBoxController();
|
||||||
fxmlLoader.setController(chatBoxController);
|
fxmlLoader.setController(chatBoxController);
|
||||||
|
Parent node = fxmlLoader.load();
|
||||||
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
|
Scene scene = new Scene(node, SCENE_WIDTH, SCENE_HEIGHT);
|
||||||
stage.setTitle("Chat");
|
stage.setTitle("Chat");
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
stage.show();
|
stage.show();
|
||||||
|
|||||||
Reference in New Issue
Block a user