Merge branch 'main' into fix/game-turn-and-action-bugs

This commit is contained in:
JK
2026-04-16 13:41:46 +02:00
23 changed files with 869 additions and 167 deletions
@@ -1,8 +1,9 @@
# Protocol Document # Protocol Document
This document describes the protocol for client-server communication in our application. It defines the structure of requests and responses, the supported commands along with their request parameters, response formats, and possible errors. This document describes the protocol for client-server communication in our application. It defines the structure of requests and responses, the supported commands along with their request parameters, response formats, and possible errors.
# Table of Contents # Table of Contents
- [Protocol Document](#protocol-document) - [Protocol Document](#protocol-document)
- [Table of Contents](#table-of-contents) - [Table of Contents](#table-of-contents)
- [General structure of requests](#general-structure-of-requests) - [General structure of requests](#general-structure-of-requests)
@@ -36,20 +37,27 @@ This document describes the protocol for client-server communication in our appl
- [Error Response](#error-response-4) - [Error Response](#error-response-4)
- [Example Request](#example-request-2) - [Example Request](#example-request-2)
- [Example Response](#example-response-2) - [Example Response](#example-response-2)
- [LOGOUT command](#logout-command) - [CHANGE\_USERNAME command](#change_username-command)
- [Required pre-execution checks](#required-pre-execution-checks-3) - [Required pre-execution checks](#required-pre-execution-checks-3)
- [Request Parameters](#request-parameters-3) - [Request Parameters](#request-parameters-3)
- [Success Response](#success-response-3) - [Success Response](#success-response-3)
- [Error Response](#error-response-5) - [Error Response](#error-response-5)
- [Example Request](#example-request-3) - [Example Request](#example-request-3)
- [Example Response](#example-response-3) - [Example Response](#example-response-3)
- [LIST\_USERS command](#list_users-command) - [LOGOUT command](#logout-command)
- [Required pre-execution checks](#required-pre-execution-checks-4) - [Required pre-execution checks](#required-pre-execution-checks-4)
- [Request Parameters](#request-parameters-4) - [Request Parameters](#request-parameters-4)
- [Success Response](#success-response-4) - [Success Response](#success-response-4)
- [Error Response](#error-response-6) - [Error Response](#error-response-6)
- [Example Request](#example-request-4) - [Example Request](#example-request-4)
- [Example Response](#example-response-4) - [Example Response](#example-response-4)
- [LIST\_USERS command](#list_users-command)
- [Required pre-execution checks](#required-pre-execution-checks-5)
- [Request Parameters](#request-parameters-5)
- [Success Response](#success-response-5)
- [Error Response](#error-response-7)
- [Example Request](#example-request-5)
- [Example Response](#example-response-5)
- [GET_LOBBY_LIST command](#get_lobby_list-command) - [GET_LOBBY_LIST command](#get_lobby_list-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)
@@ -91,6 +99,7 @@ This document describes the protocol for client-server communication in our appl
<!-- Please see the comments for copy n' paste ready examples --> <!-- Please see the comments for copy n' paste ready examples -->
# General structure of requests # General structure of requests
As mentioned before, our protocol is based on POP3. As mentioned before, our protocol is based on POP3.
Each command is represented as a single line of text, starting with the command name followed by parameters. The server responds with a status line indicating success or failure, followed by the body. Each command is represented as a single line of text, starting with the command name followed by parameters. The server responds with a status line indicating success or failure, followed by the body.
@@ -99,41 +108,43 @@ Requests can have parameters that provide additional information for the command
Responses are collections of key-value pairs, containing either a value or another collection, allowing for nested structures. Responses are collections of key-value pairs, containing either a value or another collection, allowing for nested structures.
Each collection is ended with the `END` keyword. Each collection is ended with the `END` keyword.
# Preconditions # Preconditions
The serverside pipeline to process incoming requests consists of multiple stages. The serverside pipeline to process incoming requests consists of multiple stages.
Each of these stages can yield an error response if the request does not meet the requirements of that stage. Each of these stages can yield an error response if the request does not meet the requirements of that stage.
## Parsing ## Parsing
One of these stages is the parsing. It is responsible for parsing the raw request into a structured format that can be easily processed by the command handlers. It validates the syntax of the request as well. One of these stages is the parsing. It is responsible for parsing the raw request into a structured format that can be easily processed by the command handlers. It validates the syntax of the request as well.
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :-------------- | :---------------------------------------------------------------------------------------------- | | :-------------- | :---------------------------------------------------------------------------------------------- |
| `PARSING_ERROR` | The body of the request contains syntax errors (see message field of response for more details) | | `PARSING_ERROR` | The body of the request contains syntax errors (see message field of response for more details) |
## Command dispatching ## Command dispatching
After the request has been successfully parsed, the next stage is to dispatch the `PrimitiveRequest` to the appropriate `CommandParser`. This is done by the `CommandDispatcherDispatcher`, which uses the command name to determine which parser to use. After the request has been successfully parsed, the next stage is to dispatch the `PrimitiveRequest` to the appropriate `CommandParser`. This is done by the `CommandDispatcherDispatcher`, which uses the command name to determine which parser to use.
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :---------------- | :---------------------------------------------------------------- | | :---------------- | :---------------------------------------------------------------- |
| `UNKNOWN_COMMAND` | The command is unknown to this server. No parser has been defined | | `UNKNOWN_COMMAND` | The command is unknown to this server. No parser has been defined |
## Command parsing ## Command parsing
Once the `PrimitiveRequest` has been dispatched to the appropriate `CommandParser`, the parser is responsible for parsing the parameters of the request and creating a `Request` that can be executed by the responsible `CommandHandler`. Once the `PrimitiveRequest` has been dispatched to the appropriate `CommandParser`, the parser is responsible for parsing the parameters of the request and creating a `Request` that can be executed by the responsible `CommandHandler`.
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------ | :------------------------------------------------------------------------------------------------ | | :------------------ | :------------------------------------------------------------------------------------------------ |
| `MISSING_PARAMETER` | A required parameter is missing from the request (see message field of response for more details) | | `MISSING_PARAMETER` | A required parameter is missing from the request (see message field of response for more details) |
# Pre-execution checks # Pre-execution checks
Pre-execution checks are reusable validation steps that can be registered on command handlers. Pre-execution checks are reusable validation steps that can be registered on command handlers.
They are implemented as `HandlerCheck` instances and are executed by the `CommandHandlerExecutor` before the handler's main logic is invoked. They are implemented as `HandlerCheck` instances and are executed by the `CommandHandlerExecutor` before the handler's main logic is invoked.
@@ -148,16 +159,17 @@ Description of the check, what it does and when it should be used.
--> -->
## UserLoggedInCheck ## UserLoggedInCheck
The `UserLoggedInCheck` is a common pre-execution check that verifies whether the user is logged in (i.e. has a user associated with his session). The `UserLoggedInCheck` is a common pre-execution check that verifies whether the user is logged in (i.e. has a user associated with his session).
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------- | :------------------------ | | :------------------- | :------------------------ |
| `USER_NOT_LOGGED_IN` | The user is not logged in | | `USER_NOT_LOGGED_IN` | The user is not logged in |
# Commands # Commands
Commands are the core of our protocol, representing the various actions that clients can request from the server. Each command has a unique name and may require specific parameters in addition to pre-execution checks. Commands are the core of our protocol, representing the various actions that clients can request from the server. Each command has a unique name and may require specific parameters in addition to pre-execution checks.
The server processes these commands and responds accordingly. The server processes these commands and responds accordingly.
@@ -212,41 +224,50 @@ END
--> -->
## PING command ## PING command
The `PING` command is a simple command that can be used to check if the server is responsive. The `PING` command is a simple command that can be used to check if the server is responsive.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
No parameters. No parameters.
### Success Response ### Success Response
No response fields. No response fields.
### Example Request ### Example Request
``` ```
PING PING
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
END END
``` ```
## CHECK_USERNAME command ## CHECK_USERNAME command
The `CHECK_USERNAME` command is used to check if a username is already taken by another user. Additional users can still log in with the same username, but their name will be substituted with a suffix. The `CHECK_USERNAME` command is used to check if a username is already taken by another user. Additional users can still log in with the same username, but their name will be substituted with a suffix.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
| Parameter Name | Type | Optional | Description | | Parameter Name | Type | Optional | Description |
| :------------- | :------- | :------- | :------------------------------------- | | :------------- | :------- | :------- | :------------------------------------- |
| `USERNAME` | `String` | no | The username to check for availability | | `USERNAME` | `String` | no | The username to check for availability |
### Success Response ### Success Response
| Field | Type | Description | | Field | Type | Description |
| :------- | :--------------------------- | :---------------------------------------------------------------------- | | :------- | :--------------------------- | :---------------------------------------------------------------------- |
| `STATUS` | `Enum<UsernameAvailability>` | Member of enum indicating if the username is available or already taken | | `STATUS` | `Enum<UsernameAvailability>` | Member of enum indicating if the username is available or already taken |
@@ -257,11 +278,13 @@ None.
| `TAKEN` | Username is already in use | | `TAKEN` | Username is already in use |
### Example Request ### Example Request
``` ```
CHECK_USERNAME USERNAME='Lars' CHECK_USERNAME USERNAME='Lars'
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
STATUS=FREE STATUS=FREE
@@ -269,33 +292,40 @@ END
``` ```
## LOGIN command ## LOGIN command
The `LOGIN` command is used to log in a user with a specified username. If the username is already taken by another user, the server will append a suffix to the username to make it unique. The `LOGIN` command is used to log in a user with a specified username. If the username is already taken by another user, the server will append a suffix to the username to make it unique.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
| Parameter Name | Type | Optional | Description | | Parameter Name | Type | Optional | Description |
| :------------- | :------- | :------- | :----------------------------------- | | :------------- | :------- | :------- | :----------------------------------- |
| `USERNAME` | `String` | no | The username to create the user with | | `USERNAME` | `String` | no | The username to create the user with |
### Success Response ### Success Response
| Field | Type | Description | | Field | Type | Description |
| :--------- | :---------------------------------------------------------------------- | :-------------------------------------------------------------------- | | :--------- | :---------------------------------------------------------------------- | :-------------------------------------------------------------------- |
| `USERNAME` | `String` | Username of the newly created user, can differ from the requested one | | `USERNAME` | `String` | Username of the newly created user, can differ from the requested one |
| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user | | `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user |
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------ | :----------------------------------------------------------------------------- | | :------------------ | :----------------------------------------------------------------------------- |
| `ALREADY_LOGGED_IN` | The session is already associated with a user, logging in again is prohibited. | | `ALREADY_LOGGED_IN` | The session is already associated with a user, logging in again is prohibited. |
### Example Request ### Example Request
``` ```
LOGIN USERNAME='Lars' LOGIN USERNAME='Lars'
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
USERNAME='Lars_1234' USERNAME='Lars_1234'
@@ -303,45 +333,100 @@ LOGIN USERNAME='Lars'
END END
``` ```
## CHANGE_USERNAME command
## LOGOUT command The `CHANGE_USERNAME` command is used to change the username of an already logged-in user. The request is tied to the current session and updates all affected server-side mappings.
Description of the command, what it does, and when it should be used.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
| Parameter Name | Type | Optional | Description |
| :------------- | :------- | :------- | :------------------------------------ |
| `USERNAME` | `String` | no | The new username for the active user |
### Success Response
| Field | Type | Description |
| :--------- | :---------------------------------------------------------------------- | :-------------------------------------- |
| `USERNAME` | `String` | Effective username after rename |
| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the renamed user |
### Error Response
| Code | Description |
| :------------------- | :-------------------------------------------------------------------- |
| `USER_NOT_LOGGED_IN` | No active user is associated with this session. |
| `INVALID_USERNAME` | Username contains disallowed characters or is empty. |
| `USERNAME_TAKEN` | Requested username is already used by another user. |
| `RENAME_CONFLICT` | Rename could not be propagated to all current lobby/game structures. |
### Example Request
```
CHANGE_USERNAME USERNAME='Lars_New'
```
### Example Response
```
+OK
USERNAME='Lars_New'
ID=e47a671e-2b2a-42df-bb82-953fe2ebd307
END
```
## LOGOUT command
Description of the command, what it does, and when it should be used.
### Required pre-execution checks
None.
### Request Parameters
No parameters. No parameters.
### Success Response ### Success Response
No response fields. No response fields.
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------- | :--------------------------------------------------------------- | | :------------------- | :--------------------------------------------------------------- |
| `NO_USER_ASSOCIATED` | The session has no user associated, logging out is not possible. | | `NO_USER_ASSOCIATED` | The session has no user associated, logging out is not possible. |
### Example Request ### Example Request
``` ```
LOGOUT LOGOUT
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
END END
``` ```
## LIST_USERS command ## LIST_USERS command
The `LIST_USERS` command is used to retrieve a list of all currently logged-in users. The `LIST_USERS` command is used to retrieve a list of all currently logged-in users.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
No parameters. No parameters.
### Success Response ### Success Response
| Field | Type | Description | | Field | Type | Description |
| :------ | :----------------- | :--------------------------------------- | | :------ | :----------------- | :--------------------------------------- |
| `USERS` | `Collection<User>` | Collection of all users currently online | | `USERS` | `Collection<User>` | Collection of all users currently online |
@@ -351,16 +436,18 @@ No parameters.
| `USERNAME` | `String` | Username of the newly created user, can differ from the requested one | | `USERNAME` | `String` | Username of the newly created user, can differ from the requested one |
| `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user | | `ID` | [`UUID`](https://docs.oracle.com/javase/8/docs/api/java/util/UUID.html) | The ID of the created user |
### Error Response ### Error Response
None. None.
### Example Request ### Example Request
``` ```
LIST_USERS LIST_USERS
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
USERS USERS
@@ -381,8 +468,11 @@ END
``` ```
## SEND_MESSAGE command ## SEND_MESSAGE command
The `SEND_MESSAGE` command is used to transfer the chat message sent by a user to the server. The `SEND_MESSAGE` command is used to transfer the chat message sent by a user to the server.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
@@ -403,28 +493,36 @@ None.
| `WHISPER` | Message is for the whisper chat | | `WHISPER` | Message is for the whisper chat |
### Success Response ### Success Response
No response fields. No response fields.
### Error Response ### Error Response
None. None.
### Example Request ### Example Request
``` ```
SEND_MESSAGE TYPE=GLOBAL GAME=1 USER=player1 TARGET=null TIME='10:30' TEXT='Hello World' SEND_MESSAGE TYPE=GLOBAL GAME=1 USER=player1 TARGET=null TIME='10:30' TEXT='Hello World'
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
END END
``` ```
## GET_MESSAGE_COUNT command ## GET_MESSAGE_COUNT command
The `GET_MESSAGE_COUNT` is used to get the current number of messages that are stored in the queue for a client. The `GET_MESSAGE_COUNT` is used to get the current number of messages that are stored in the queue for a client.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
No parameters. No parameters.
### Success Response ### Success Response
@@ -434,17 +532,19 @@ No parameters.
| `COUNT` | `int` | The current number of messages | | `COUNT` | `int` | The current number of messages |
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------- |:---------------------------------------------------------------------------------| | :------------------- |:---------------------------------------------------------------------------------|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client | | `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
### Example Request ### Example Request
``` ```
GET_MESSAGE_COUNT GET_MESSAGE_COUNT
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
COUNT=10 COUNT=10
@@ -452,14 +552,19 @@ END
``` ```
## GET_NEXT_MESSAGE command ## GET_NEXT_MESSAGE command
The `GET_NEXT_MESSAGE` command is used to get the next message stored in a queue for the client. The `GET_NEXT_MESSAGE` command is used to get the next message stored in a queue for the client.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
No parameters. No parameters.
### Success Response ### Success Response
| Field | Type | Description | | Field | Type | Description |
|:---------|:----------------|:-------------------------------------------------------------------| |:---------|:----------------|:-------------------------------------------------------------------|
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used | | `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
@@ -476,16 +581,19 @@ No parameters.
| `WHISPER` | Message is for the whisper chat | | `WHISPER` | Message is for the whisper chat |
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :------------------- |:---------------------------------------------------------------------------------| | :------------------- |:---------------------------------------------------------------------------------|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client | | `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
### Example Request ### Example Request
``` ```
GET_NEXT_MESSAGE GET_NEXT_MESSAGE
``` ```
### Example Response ### Example Response
``` ```
+OK +OK
TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag" TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
@@ -613,9 +721,11 @@ END
The `GET_LOBBY_LIST` command requests the server to return a list of currently available lobbies with basic metadata. The `GET_LOBBY_LIST` command requests the server to return a list of currently available lobbies with basic metadata.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
No parameters. No parameters.
### Implementation notes ### Implementation notes
@@ -667,9 +777,11 @@ END
The `GET_GAME_STATE` command returns a snapshot of the current game for a lobby. It includes global fields (`PHASE`, `POT`, `CURRENT_BET`, `DEALER`, `ACTIVE_PLAYER`), repeated `CARD` blocks for community cards and repeated `PLAYER` blocks for per-player information. If the requester is a player in the game, their hole cards are included inside their `PLAYER` block as nested `CARD` blocks. The `GET_GAME_STATE` command returns a snapshot of the current game for a lobby. It includes global fields (`PHASE`, `POT`, `CURRENT_BET`, `DEALER`, `ACTIVE_PLAYER`), repeated `CARD` blocks for community cards and repeated `PLAYER` blocks for per-player information. If the requester is a player in the game, their hole cards are included inside their `PLAYER` block as nested `CARD` blocks.
### Required pre-execution checks ### Required pre-execution checks
- [`UserLoggedInCheck`](#userloggedincheck) - [`UserLoggedInCheck`](#userloggedincheck)
### Request Parameters ### Request Parameters
| Parameter Name | Type | Optional | Description | | Parameter Name | Type | Optional | Description |
| :------------- | :----- | :------: | :---------- | | :------------- | :----- | :------: | :---------- |
| `GAME_ID` | `int` | yes | Numeric id of the lobby/game to query. If omitted the server will resolve the lobby by the requesting session's associated user. | `GAME_ID` | `int` | yes | Numeric id of the lobby/game to query. If omitted the server will resolve the lobby by the requesting session's associated user.
@@ -1114,9 +1226,11 @@ END
The `GET_LOBBY_STATUS` command requests the server to return the current state of a lobby, including the list of players and their ready state. The `GET_LOBBY_STATUS` command requests the server to return the current state of a lobby, including the list of players and their ready state.
### Required pre-execution checks ### Required pre-execution checks
None. None.
### Request Parameters ### Request Parameters
| Parameter Name | Type | Optional | Description | | Parameter Name | Type | Optional | Description |
| :------------- | :--- | :------: | :---------- | | :------------- | :--- | :------: | :---------- |
| `ID` | `int` | no | Numeric id of the target lobby | | `ID` | `int` | no | Numeric id of the target lobby |
@@ -1151,6 +1265,7 @@ END
``` ```
### Error Response ### Error Response
| Code | Description | | Code | Description |
| :--- | :---------- | | :--- | :---------- |
| `LOBBY_NOT_FOUND` | The specified lobby id does not exist | | `LOBBY_NOT_FOUND` | The specified lobby id does not exist |
Binary file not shown.
Binary file not shown.
@@ -39,6 +39,10 @@ public class ClientApp {
return sharedUsername; return sharedUsername;
} }
public static void updateSharedUsername(String username) {
setSharedUsername(username != null && !username.isBlank() ? username.trim() : null);
}
private static void setSharedUsername(String username) { private static void setSharedUsername(String username) {
sharedUsername = username; sharedUsername = username;
LOGGER.info("sharedUsername set to '{}'", getSharedUsername()); LOGGER.info("sharedUsername set to '{}'", getSharedUsername());
@@ -1,15 +1,20 @@
package ch.unibas.dmi.dbis.cs108.casono.client.chat; package ch.unibas.dmi.dbis.cs108.casono.client.chat;
import ch.unibas.dmi.dbis.cs108.casono.client.ClientApp;
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 ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
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.Set;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
@@ -24,7 +29,7 @@ public class ChatController {
private static final Map<ClientService, ChatController> ACTIVE_CONTROLLERS = private static final Map<ClientService, ChatController> ACTIVE_CONTROLLERS =
new WeakHashMap<>(); new WeakHashMap<>();
private final String username; private volatile String username;
private final ClientService clientService; private final ClientService clientService;
@@ -37,6 +42,7 @@ public class ChatController {
private final ChatBoxController chatBoxController; private final ChatBoxController chatBoxController;
private int lobbyId = -1; private int lobbyId = -1;
private final Timer timer; private final Timer timer;
private final Consumer<List<String>> serverEventListener;
public record ChatKey(ChatType type, @Nullable String targetUser) { public record ChatKey(ChatType type, @Nullable String targetUser) {
public ChatKey(ChatType type) { public ChatKey(ChatType type) {
@@ -71,8 +77,10 @@ public class ChatController {
localUserList = new ArrayList<>(); localUserList = new ArrayList<>();
this.chatBoxController = new ChatBoxController(username, this); this.chatBoxController = new ChatBoxController(username, this);
this.logger = LogManager.getLogger(ChatController.class); this.logger = LogManager.getLogger(ChatController.class);
this.serverEventListener = this::handleServerEvent;
registerAsActiveController(clientService); registerAsActiveController(clientService);
clientService.addEventListener(serverEventListener);
this.timer = new Timer(true); this.timer = new Timer(true);
timer.schedule( timer.schedule(
@@ -131,6 +139,7 @@ public class ChatController {
* <p>All Messages get added to a particular {@link ChatModel}, if the checks passed. * <p>All Messages get added to a particular {@link ChatModel}, if the checks passed.
*/ */
public void receiveMessage() { public void receiveMessage() {
String currentUsername = getCurrentUsername();
List<Message> newMessages = chatClient.getMessages(); List<Message> newMessages = chatClient.getMessages();
if (!newMessages.isEmpty()) { if (!newMessages.isEmpty()) {
for (Message msg : newMessages) { for (Message msg : newMessages) {
@@ -146,16 +155,17 @@ public class ChatController {
(_key) -> (_key) ->
new ChatModel( new ChatModel(
ChatType.LOBBY, ChatType.LOBBY,
username, currentUsername,
msg.lobbyId, msg.lobbyId,
null)) null))
.addMessage(msg); .addMessage(msg);
} }
break; break;
case ChatType.WHISPER: case ChatType.WHISPER:
if (msg.target.equals(username) || msg.sender.equals(username)) { if (msg.target.equals(currentUsername)
|| msg.sender.equals(currentUsername)) {
ChatKey key; ChatKey key;
if (msg.target.equals(username)) { if (msg.target.equals(currentUsername)) {
key = new ChatKey(ChatType.WHISPER, msg.sender); key = new ChatKey(ChatType.WHISPER, msg.sender);
} else { } else {
key = new ChatKey(ChatType.WHISPER, msg.target); key = new ChatKey(ChatType.WHISPER, msg.target);
@@ -166,7 +176,7 @@ public class ChatController {
ChatModel chatModel = ChatModel chatModel =
new ChatModel( new ChatModel(
ChatType.WHISPER, ChatType.WHISPER,
username, currentUsername,
lobbyId, lobbyId,
key.targetUser()); key.targetUser());
chatBoxController.addWhisperChat(key.targetUser(), chatModel); chatBoxController.addWhisperChat(key.targetUser(), chatModel);
@@ -203,13 +213,29 @@ public class ChatController {
*/ */
public synchronized void checkWhisperUsers() { public synchronized void checkWhisperUsers() {
List<String> users = chatClient.getUsers(); List<String> users = chatClient.getUsers();
logger.info(users); Set<String> remoteUsers = new HashSet<>();
if (!users.isEmpty()) { String currentUsername = getCurrentUsername();
for (String user : users) {
String value = user.split("\\=")[1]; for (String user : users) {
logger.info(value); if (user == null || !user.contains("=")) {
addWhisperUser(value); continue;
} }
String value = user.split("\\=", 2)[1].trim();
if (value.isBlank() || value.equals(currentUsername)) {
continue;
}
remoteUsers.add(value);
}
for (String known : new ArrayList<>(localUserList)) {
if (!remoteUsers.contains(known)) {
localUserList.remove(known);
chatBoxController.removeWhisperUser(known);
}
}
for (String remote : remoteUsers) {
addWhisperUser(remote);
} }
} }
@@ -222,16 +248,110 @@ public class ChatController {
if (user == null || user.isBlank()) { if (user == null || user.isBlank()) {
return; return;
} }
if (!(localUserList.contains(user) || user.equals(username))) { if (!(localUserList.contains(user) || user.equals(getCurrentUsername()))) {
localUserList.add(user); localUserList.add(user);
logger.info("adding new whisper user"); logger.info("adding new whisper user");
chatBoxController.addWhisperUser(user); chatBoxController.addWhisperUser(user);
} }
} }
public synchronized void updateUsername(String newUsername) {
if (newUsername == null || newUsername.isBlank()) {
return;
}
String oldUsername = this.username;
this.username = newUsername.trim();
chatBoxController.setUsername(this.username);
if (oldUsername != null && !oldUsername.equals(this.username)) {
localUserList.remove(oldUsername);
chatBoxController.removeWhisperUser(oldUsername);
}
}
private void handleServerEvent(List<String> lines) {
if (lines == null || lines.isEmpty()) {
return;
}
List<RequestParameter> params;
try {
params = ClientService.convertToRequestParameters(lines);
} catch (RuntimeException e) {
return;
}
String event = null;
String oldUsername = null;
String newUsername = null;
for (RequestParameter p : params) {
if ("EVENT".equalsIgnoreCase(p.key())) {
event = p.value();
} else if ("OLD_USERNAME".equalsIgnoreCase(p.key())) {
oldUsername = p.value();
} else if ("NEW_USERNAME".equalsIgnoreCase(p.key())) {
newUsername = p.value();
}
}
if (!"USERNAME_CHANGED".equalsIgnoreCase(event)) {
return;
}
applyUsernameMigration(oldUsername, newUsername);
}
private synchronized void applyUsernameMigration(String oldUsername, String newUsername) {
if (oldUsername == null
|| newUsername == null
|| oldUsername.isBlank()
|| newUsername.isBlank()
|| oldUsername.equals(newUsername)) {
return;
}
String currentUsername = getCurrentUsername();
if (oldUsername.equals(currentUsername)) {
updateUsername(newUsername);
}
ChatKey oldKey = new ChatKey(ChatType.WHISPER, oldUsername);
ChatKey newKey = new ChatKey(ChatType.WHISPER, newUsername);
ChatModel oldModel = chatModelMap.remove(oldKey);
ChatModel existingNewModel = chatModelMap.get(newKey);
if (oldModel != null) {
oldModel.setTarget(newUsername);
if (existingNewModel == null) {
chatModelMap.put(newKey, oldModel);
} else {
// Merge possible parallel history into the already existing new-key model.
for (Message msg : oldModel.messages) {
existingNewModel.addMessage(msg);
}
}
chatBoxController.renameWhisperUser(oldUsername, newUsername);
}
localUserList.remove(oldUsername);
chatBoxController.removeWhisperUser(oldUsername);
if (!newUsername.equals(getCurrentUsername())) {
addWhisperUser(newUsername);
}
}
public String getCurrentUsername() {
String shared = ClientApp.getSharedUsername();
if (shared != null && !shared.isBlank()) {
return shared.trim();
}
return username;
}
/** Stops polling background tasks for this chat controller instance. */ /** Stops polling background tasks for this chat controller instance. */
public void shutdown() { public void shutdown() {
timer.cancel(); timer.cancel();
clientService.removeEventListener(serverEventListener);
synchronized (ACTIVE_CONTROLLERS) { synchronized (ACTIVE_CONTROLLERS) {
if (ACTIVE_CONTROLLERS.get(clientService) == this) { if (ACTIVE_CONTROLLERS.get(clientService) == this) {
ACTIVE_CONTROLLERS.remove(clientService); ACTIVE_CONTROLLERS.remove(clientService);
@@ -22,7 +22,7 @@ public class ChatModel {
public final String username; public final String username;
/** The person to send the message to If the chat is a whisper chat */ /** The person to send the message to If the chat is a whisper chat */
private final String target; private String target;
private final IntegerProperty count; private final IntegerProperty count;
@@ -82,4 +82,8 @@ public class ChatModel {
public String getTarget() { public String getTarget() {
return target; return target;
} }
public void setTarget(String target) {
this.target = target;
}
} }
@@ -131,6 +131,29 @@ public class LobbyClient {
return new LoginResult(assigned, id); return new LoginResult(assigned, id);
} }
/**
* Changes the username for the currently logged-in session.
*
* @param newUsername desired new username
* @return a {@link LoginResult} containing assigned username and id as returned by the server
*/
public LoginResult changeUsername(String newUsername) {
List<String> lines = client.processCommand("CHANGE_USERNAME USERNAME=" + newUsername);
List<RequestParameter> params = ClientService.convertToRequestParameters(lines);
String assigned = newUsername;
String id = null;
for (RequestParameter p : params) {
if ("USERNAME".equalsIgnoreCase(p.key())) {
assigned = p.value();
} else if ("ID".equalsIgnoreCase(p.key())) {
id = p.value();
}
}
return new LoginResult(assigned, id);
}
/** /**
* Request the server for the list of available lobbies. * Request the server for the list of available lobbies.
* *
@@ -60,6 +60,12 @@ public class ChatBoxController {
usernameTabMap = new HashMap<>(); usernameTabMap = new HashMap<>();
} }
public void setUsername(String username) {
if (username != null && !username.isBlank()) {
this.username = username.trim();
}
}
/** /**
* Initializes the chat interface by creating the global chat model and adding the corresponding * Initializes the chat interface by creating the global chat model and adding the corresponding
* "GLOBAL" tab to the interface. It also registers the global chat in the {@link * "GLOBAL" tab to the interface. It also registers the global chat in the {@link
@@ -84,6 +90,11 @@ public class ChatBoxController {
public void addWhisperUser(String targetUserName) { public void addWhisperUser(String targetUserName) {
Platform.runLater( Platform.runLater(
() -> { () -> {
for (MenuItem existing : addWhisperChatButton.getItems()) {
if (targetUserName.equals(existing.getText())) {
return;
}
}
MenuItem menuItem = new MenuItem(targetUserName); MenuItem menuItem = new MenuItem(targetUserName);
addWhisperChatButton.getItems().add(menuItem); addWhisperChatButton.getItems().add(menuItem);
menuItem.setOnAction( menuItem.setOnAction(
@@ -98,6 +109,54 @@ public class ChatBoxController {
}); });
} }
public void removeWhisperUser(String targetUserName) {
Platform.runLater(
() ->
addWhisperChatButton
.getItems()
.removeIf(item -> targetUserName.equals(item.getText())));
}
public void renameWhisperUser(String oldUsername, String newUsername) {
if (oldUsername == null
|| newUsername == null
|| oldUsername.isBlank()
|| newUsername.isBlank()
|| oldUsername.equals(newUsername)) {
return;
}
Platform.runLater(
() -> {
for (MenuItem item : addWhisperChatButton.getItems()) {
if (oldUsername.equals(item.getText())) {
item.setText(newUsername);
item.setOnAction(
event ->
addWhisperChat(
newUsername,
new ChatModel(
ChatType.WHISPER,
username,
-1,
newUsername)));
break;
}
}
Tab tab = usernameTabMap.remove(oldUsername);
if (tab != null) {
tab.setText(newUsername);
usernameTabMap.put(newUsername, tab);
}
int idx = activeWhisperChats.indexOf(oldUsername);
if (idx >= 0) {
activeWhisperChats.set(idx, newUsername);
}
});
}
/** /**
* Creates a new private (whisper) chat model for a specific target user, registers it within * Creates a new private (whisper) chat model for a specific target user, registers it within
* the chat system, and opens a new chat tab. * the chat system, and opens a new chat tab.
@@ -68,11 +68,12 @@ public class ChatViewController implements Initializable {
String message = inputField.getText().trim(); String message = inputField.getText().trim();
if (!message.isEmpty()) { if (!message.isEmpty()) {
inputField.clear(); inputField.clear();
String currentUsername = controller.getCurrentUsername();
Message msg = Message msg =
new Message( new Message(
chatModel.getChattype(), chatModel.getChattype(),
chatModel.lobbyId, chatModel.lobbyId,
username, currentUsername,
chatModel.getTarget(), chatModel.getTarget(),
message); message);
controller.onSendToNetwork(msg); controller.onSendToNetwork(msg);
@@ -104,6 +104,13 @@ public class CasinomainuiController {
casinoTable.getChildren().add(gridManager.getGridPane()); casinoTable.getChildren().add(gridManager.getGridPane());
gridManager.renderLobbyButtons(); gridManager.renderLobbyButtons();
String sharedUsername = ClientApp.getSharedUsername();
updateUsernameFieldPresentation(sharedUsername);
if (loginButton != null) {
loginButton.setText(
sharedUsername != null && !sharedUsername.isBlank() ? "Change Name" : "Login");
}
initializeChat(clientService); initializeChat(clientService);
} }
@@ -156,7 +163,7 @@ public class CasinomainuiController {
/** Handles the login button action. Validates input and calls LobbyClient.login(). */ /** Handles the login button action. Validates input and calls LobbyClient.login(). */
@FXML @FXML
public void handleLoginButton() { public void handleLoginButton() {
String username = usernameField.getText(); String username = usernameField == null ? null : usernameField.getText();
if (username == null || username.isBlank()) { if (username == null || username.isBlank()) {
showAlert("Please enter a username."); showAlert("Please enter a username.");
return; return;
@@ -170,12 +177,63 @@ public class CasinomainuiController {
showAlert("Offline mode: cannot send login to server."); showAlert("Offline mode: cannot send login to server.");
return; return;
} }
String trimmed = username.trim();
try { try {
lobbyClient.login(username); var result = lobbyClient.login(trimmed);
showAlert("Login sent: " + username); String assigned = result != null ? result.getUsername() : trimmed;
ClientApp.updateSharedUsername(assigned);
if (chatController != null) {
chatController.updateUsername(assigned);
}
updateUsernameFieldPresentation(assigned);
if (loginButton != null) {
loginButton.setText("Change Name");
}
return;
} catch (RuntimeException loginError) {
if (!containsProtocolError(loginError, "ALREADY_LOGGED_IN")) {
LOGGER.error("Login failed: {}", loginError.getMessage());
showAlert("Login failed: " + loginError.getMessage());
return;
}
LOGGER.info("Session already logged in, trying username change");
}
try {
var result = lobbyClient.changeUsername(trimmed);
String assigned = result != null ? result.getUsername() : trimmed;
ClientApp.updateSharedUsername(assigned);
if (chatController != null) {
chatController.updateUsername(assigned);
}
updateUsernameFieldPresentation(assigned);
if (loginButton != null) {
loginButton.setText("Change Name");
}
} catch (RuntimeException e) { } catch (RuntimeException e) {
LOGGER.error("Login failed: {}", e.getMessage()); LOGGER.error("Change username failed: {}", e.getMessage());
showAlert("Login failed: " + e.getMessage()); showAlert("Change username failed: " + e.getMessage());
}
}
private boolean containsProtocolError(RuntimeException error, String code) {
String msg = error.getMessage();
return msg != null && msg.contains(code);
}
private void updateUsernameFieldPresentation(String username) {
if (usernameField == null) {
return;
}
if (username != null && !username.isBlank()) {
usernameField.setText(username.trim());
usernameField.positionCaret(usernameField.getText().length());
usernameField.setPromptText("Username");
} else {
usernameField.clear();
usernameField.setPromptText("Username");
} }
} }
@@ -1,8 +1,26 @@
package ch.unibas.dmi.dbis.cs108.casono.server; package ch.unibas.dmi.dbis.cs108.casono.server;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username.ChangeUsernameHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username.ChangeUsernameParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username.ChangeUsernameRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.check_nick.CheckUsernameRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state.GetGameStateHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state.GetGameStateParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state.GetGameStateRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.GetMessageCountRequest;
@@ -12,6 +30,21 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetN
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby.CreateLobbyHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby.CreateLobbyParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby.CreateLobbyRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list.GetLobbyListHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list.GetLobbyListParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list.GetLobbyListRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status.GetLobbyStatusHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status.GetLobbyStatusParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status.GetLobbyStatusRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby.JoinLobbyRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game.StartGameRequest;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginHandler; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginParser; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginParser;
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginRequest; import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.login.LoginRequest;
@@ -28,6 +61,7 @@ import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob; import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserCleanupJob;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry; import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager; import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandlerExecutor; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandlerExecutor;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandRouter;
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher; import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParserDispatcher;
@@ -168,6 +202,15 @@ public class ServerApp {
commandRouter.register( commandRouter.register(
LoginRequest.class, new LoginHandler(responseDispatcher, userRegistry)); LoginRequest.class, new LoginHandler(responseDispatcher, userRegistry));
parserDispatcher.register("CHANGE_USERNAME", new ChangeUsernameParser());
commandRouter.register(
ChangeUsernameRequest.class,
new ChangeUsernameHandler(
responseDispatcher,
userRegistry,
context.lobbyManager(),
context.sessionManager()));
parserDispatcher.register("LOGOUT", new LogoutParser()); parserDispatcher.register("LOGOUT", new LogoutParser());
commandRouter.register( commandRouter.register(
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry)); LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
@@ -192,152 +235,84 @@ public class ServerApp {
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry)); ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
// GET_LOBBY_LIST registration // GET_LOBBY_LIST registration
parserDispatcher.register( parserDispatcher.register("GET_LOBBY_LIST", new GetLobbyListParser());
"GET_LOBBY_LIST",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
.GetLobbyListParser());
commandRouter.register( commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list GetLobbyListRequest.class,
.GetLobbyListRequest.class, (CommandHandler<GetLobbyListRequest>)
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< new GetLobbyListHandler(responseDispatcher, context.lobbyManager()));
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
.get_lobby_list.GetLobbyListRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_list
.GetLobbyListHandler(responseDispatcher, context.lobbyManager()));
// GET_GAME_STATE registration // GET_GAME_STATE registration
parserDispatcher.register( parserDispatcher.register("GET_GAME_STATE", new GetGameStateParser());
"GET_GAME_STATE",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
.GetGameStateParser());
commandRouter.register( commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state GetGameStateRequest.class,
.GetGameStateRequest.class, (CommandHandler<GetGameStateRequest>)
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< new GetGameStateHandler(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game
.get_game_state.GetGameStateRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.get_game_state
.GetGameStateHandler(
responseDispatcher, context.lobbyManager(), userRegistry)); responseDispatcher, context.lobbyManager(), userRegistry));
// BET registration // BET registration
parserDispatcher.register( parserDispatcher.register("BET", new PlayerBetParser());
"BET",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetParser());
commandRouter.register( commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet.PlayerBetRequest.class, PlayerBetRequest.class,
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< (CommandHandler<PlayerBetRequest>)
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet new PlayerBetHandler(
.PlayerBetRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.bet
.PlayerBetHandler(
responseDispatcher, userRegistry, context.lobbyManager())); responseDispatcher, userRegistry, context.lobbyManager()));
// RAISE registration // RAISE registration
parserDispatcher.register( parserDispatcher.register("RAISE", new PlayerRaiseParser());
"RAISE",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
.PlayerRaiseParser());
commandRouter.register( commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise.PlayerRaiseRequest PlayerRaiseRequest.class,
.class, (CommandHandler<PlayerRaiseRequest>)
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< new PlayerRaiseHandler(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
.PlayerRaiseRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.raise
.PlayerRaiseHandler(
responseDispatcher, userRegistry, context.lobbyManager())); responseDispatcher, userRegistry, context.lobbyManager()));
// CALL registration // CALL registration
parserDispatcher.register( parserDispatcher.register("CALL", new PlayerCallParser());
"CALL",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
.PlayerCallParser());
commandRouter.register( commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call.PlayerCallRequest PlayerCallRequest.class,
.class, (CommandHandler<PlayerCallRequest>)
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< new PlayerCallHandler(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
.PlayerCallRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.call
.PlayerCallHandler(
responseDispatcher, userRegistry, context.lobbyManager())); responseDispatcher, userRegistry, context.lobbyManager()));
// FOLD registration // FOLD registration
parserDispatcher.register( parserDispatcher.register("FOLD", new PlayerFoldParser());
"FOLD",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
.PlayerFoldParser());
commandRouter.register( commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold.PlayerFoldRequest PlayerFoldRequest.class,
.class, (CommandHandler<PlayerFoldRequest>)
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< new PlayerFoldHandler(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
.PlayerFoldRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.game.fold
.PlayerFoldHandler(
responseDispatcher, userRegistry, context.lobbyManager())); responseDispatcher, userRegistry, context.lobbyManager()));
// GET_LOBBY_STATUS registration // GET_LOBBY_STATUS registration
parserDispatcher.register( parserDispatcher.register("GET_LOBBY_STATUS", new GetLobbyStatusParser());
"GET_LOBBY_STATUS",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status
.GetLobbyStatusParser());
commandRouter.register( commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.get_lobby_status GetLobbyStatusRequest.class,
.GetLobbyStatusRequest.class, (CommandHandler<GetLobbyStatusRequest>)
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< new GetLobbyStatusHandler(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
.get_lobby_status.GetLobbyStatusRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby
.get_lobby_status.GetLobbyStatusHandler(
responseDispatcher, context.lobbyManager(), userRegistry)); responseDispatcher, context.lobbyManager(), userRegistry));
// CREATE_LOBBY registration // CREATE_LOBBY registration
parserDispatcher.register( parserDispatcher.register("CREATE_LOBBY", new CreateLobbyParser());
"CREATE_LOBBY",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby
.CreateLobbyParser());
commandRouter.register( commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.create_lobby CreateLobbyRequest.class,
.CreateLobbyRequest.class, (CommandHandler<CreateLobbyRequest>)
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< new CreateLobbyHandler(
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, responseDispatcher,
context.lobbyManager(), context.lobbyManager(),
context.sessionManager())); context.sessionManager()));
// JOIN_LOBBY registration // JOIN_LOBBY registration
parserDispatcher.register( parserDispatcher.register("JOIN_LOBBY", new JoinLobbyParser());
"JOIN_LOBBY",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
.JoinLobbyParser());
commandRouter.register( commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby JoinLobbyRequest.class,
.JoinLobbyRequest.class, (CommandHandler<JoinLobbyRequest>)
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< new JoinLobbyHandler(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
.JoinLobbyRequest>)
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.join_lobby
.JoinLobbyHandler(
responseDispatcher, context.lobbyManager(), userRegistry)); responseDispatcher, context.lobbyManager(), userRegistry));
// START_GAME registration // START_GAME registration
parserDispatcher.register( parserDispatcher.register("START_GAME", new StartGameParser());
"START_GAME",
new ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game
.StartGameParser());
commandRouter.register( commandRouter.register(
ch.unibas.dmi.dbis.cs108.casono.server.app.commands.lobby.start_game StartGameRequest.class,
.StartGameRequest.class, (CommandHandler<StartGameRequest>)
(ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler< new StartGameHandler(
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, context.lobbyManager(), userRegistry)); responseDispatcher, context.lobbyManager(), userRegistry));
} }
} }
@@ -0,0 +1,112 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.lobby.LobbyManager;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User;
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.request.RequestContext;
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.ErrorResponse;
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;
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.SessionManager;
import java.util.Optional;
import java.util.regex.Pattern;
/** Handles CHANGE_USERNAME requests for already logged-in users. */
public class ChangeUsernameHandler extends CommandHandler<ChangeUsernameRequest> {
private static final Pattern VALID_USERNAME = Pattern.compile("[a-zA-Z0-9_-]+");
private final UserRegistry userRegistry;
private final LobbyManager lobbyManager;
private final SessionManager sessionManager;
/**
* @param responseDispatcher dispatcher used for responses
* @param userRegistry registry containing all users
* @param lobbyManager lobby manager used to keep lobby/game mappings in sync
* @param sessionManager session manager used to broadcast rename events
*/
public ChangeUsernameHandler(
ResponseDispatcher responseDispatcher,
UserRegistry userRegistry,
LobbyManager lobbyManager,
SessionManager sessionManager) {
super(responseDispatcher);
this.userRegistry = userRegistry;
this.lobbyManager = lobbyManager;
this.sessionManager = sessionManager;
}
@Override
public void execute(ChangeUsernameRequest request) {
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
if (user.isEmpty()) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"USER_NOT_LOGGED_IN",
"This session is not associated with an active user."));
return;
}
String newUsername = request.getUsername() == null ? "" : request.getUsername().trim();
if (newUsername.isEmpty() || !VALID_USERNAME.matcher(newUsername).matches()) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"INVALID_USERNAME",
"Only letters, numbers, '_' and '-' are allowed."));
return;
}
User currentUser = user.get();
String oldUsername = currentUser.getName();
boolean changed = userRegistry.changeUsername(currentUser.getId(), newUsername);
if (!changed) {
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"USERNAME_TAKEN",
"The requested username is already taken."));
return;
}
boolean lobbySynced =
lobbyManager == null || lobbyManager.renamePlayer(oldUsername, newUsername);
if (!lobbySynced) {
userRegistry.changeUsername(currentUser.getId(), oldUsername);
responseDispatcher.dispatch(
new ErrorResponse(
request.getContext(),
"RENAME_CONFLICT",
"Could not update username in current lobby/game state."));
return;
}
responseDispatcher.dispatch(
new ChangeUsernameResponse(
request.getContext(), currentUser.getName(), currentUser.getId()));
broadcastUsernameChanged(oldUsername, currentUser.getName());
}
private void broadcastUsernameChanged(String oldUsername, String newUsername) {
if (sessionManager == null) {
return;
}
for (Session session : sessionManager.getAllSessions()) {
RequestContext ctx = new RequestContext(session.getId(), 0);
SuccessResponse ev =
new SuccessResponse(
ctx,
new ResponseBodyBuilder()
.param("EVENT", "USERNAME_CHANGED")
.param("OLD_USERNAME", oldUsername)
.param("NEW_USERNAME", newUsername)
.build()) {};
responseDispatcher.dispatch(ev);
}
}
}
@@ -0,0 +1,15 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
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;
/** Parses CHANGE_USERNAME requests. */
public class ChangeUsernameParser implements CommandParser<ChangeUsernameRequest> {
@Override
public ChangeUsernameRequest parse(PrimitiveRequest primitiveRequest) {
RequestParameterAccessor accessor =
new RequestParameterAccessor(primitiveRequest.parameters());
return new ChangeUsernameRequest(primitiveRequest.context(), accessor.require("USERNAME"));
}
}
@@ -0,0 +1,25 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
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 used to change the username of the current session user. */
public class ChangeUsernameRequest extends Request {
private final String username;
/**
* @param context request context for responses
* @param username desired new username
*/
public ChangeUsernameRequest(RequestContext context, String username) {
super(context);
this.username = username;
}
/**
* @return desired new username
*/
public String getUsername() {
return username;
}
}
@@ -0,0 +1,23 @@
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.change_username;
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserId;
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;
/** Response for successful username changes. */
public class ChangeUsernameResponse extends SuccessResponse {
/**
* @param context request context
* @param username current username after the rename operation
* @param id user id of renamed user
*/
public ChangeUsernameResponse(RequestContext context, String username, UserId id) {
super(
context,
new ResponseBodyBuilder()
.param("USERNAME", username)
.param("ID", id.value())
.build());
}
}
@@ -9,6 +9,7 @@ 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.command.execution.CommandHandler;
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 java.util.Optional;
public class SendMessageHandler extends CommandHandler<SendMessageRequest> { public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
private final UserRegistry userRegistry; private final UserRegistry userRegistry;
@@ -39,6 +40,11 @@ public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
@Override @Override
public void execute(SendMessageRequest request) { public void execute(SendMessageRequest request) {
Message message = request.getMessage(); Message message = request.getMessage();
Optional<ch.unibas.dmi.dbis.cs108.casono.server.domain.user.User> senderUser =
userRegistry.getBySessionId(request.getSessionId());
if (senderUser.isPresent()) {
message.sender = senderUser.get().getName();
}
broadcast(request, message); broadcast(request, message);
OkResponse response = new OkResponse(request.getContext()); OkResponse response = new OkResponse(request.getContext());
responseDispatcher.dispatch(response); responseDispatcher.dispatch(response);
@@ -61,6 +61,35 @@ public class GameController {
engine.getState().addPlayer(name, chips); engine.getState().addPlayer(name, chips);
} }
/**
* Renames a player id in the controller list and underlying game state.
*
* @param oldId old player id
* @param newId new player id
* @return true if rename succeeded
*/
public boolean renamePlayer(PlayerId oldId, PlayerId newId) {
if (oldId == null || newId == null) {
return false;
}
if (oldId.equals(newId)) {
return true;
}
int idx = players.indexOf(oldId);
if (idx < 0 || players.contains(newId)) {
return false;
}
boolean stateRenamed = engine.getState().renamePlayerId(oldId, newId);
if (!stateRenamed) {
return false;
}
players.set(idx, newId);
return true;
}
/** /**
* Initializes a new hand by preparing the deck, setting the phase to PREFLOP, rotating the * Initializes a new hand by preparing the deck, setting the phase to PREFLOP, rotating the
* dealer, dealing hole cards, posting blinds, and setting the first active player. * dealer, dealing hole cards, posting blinds, and setting the first active player.
@@ -49,6 +49,16 @@ public class Player {
return id; return id;
} }
/**
* Updates the player's id. This is used when a username change is propagated into an already
* running game.
*
* @param id new player id
*/
public void setId(PlayerId id) {
this.id = id;
}
/** /**
* Returns the display name of the player. Currently identical to the player ID. * Returns the display name of the player. Currently identical to the player ID.
* *
@@ -174,6 +174,51 @@ public class GameState {
holeCards.computeIfAbsent(id, k -> new ArrayList<>()); holeCards.computeIfAbsent(id, k -> new ArrayList<>());
} }
/**
* Renames a player id across all game-state structures.
*
* @param oldId existing player id
* @param newId new player id
* @return true if the rename was applied, false otherwise
*/
public synchronized boolean renamePlayerId(PlayerId oldId, PlayerId newId) {
if (oldId == null || newId == null) {
return false;
}
if (!players.containsKey(oldId)) {
return false;
}
if (oldId.equals(newId)) {
return true;
}
if (players.containsKey(newId)) {
return false;
}
Player player = players.remove(oldId);
if (player == null) {
return false;
}
player.setId(newId);
players.put(newId, player);
int idx = playerOrder.indexOf(oldId);
if (idx >= 0) {
playerOrder.set(idx, newId);
}
moveMapEntry(currentBets, oldId, newId, 0);
moveMapEntry(playerBetCommitments, oldId, newId, 0);
moveMapEntry(holeCards, oldId, newId, new ArrayList<>());
return true;
}
private <T> void moveMapEntry(
Map<PlayerId, T> map, PlayerId oldId, PlayerId newId, T fallback) {
T value = map.remove(oldId);
map.put(newId, value != null ? value : fallback);
}
// Betting // Betting
public int getCurrentBet(PlayerId playerId) { public int getCurrentBet(PlayerId playerId) {
return currentBets.getOrDefault(playerId, 0); return currentBets.getOrDefault(playerId, 0);
@@ -70,6 +70,30 @@ public class Lobby {
return playerNames.remove(playerName); return playerNames.remove(playerName);
} }
/**
* Renames a player in this lobby's player list.
*
* @param oldName old username
* @param newName new username
* @return true if renamed successfully
*/
public boolean renamePlayer(String oldName, String newName) {
if (oldName == null || newName == null) {
return false;
}
synchronized (playerNames) {
if (oldName.equals(newName)) {
return playerNames.contains(oldName);
}
int idx = playerNames.indexOf(oldName);
if (idx < 0 || playerNames.contains(newName)) {
return false;
}
playerNames.set(idx, newName);
return true;
}
}
public void initGame(GameController controller) { public void initGame(GameController controller) {
this.gameController = controller; this.gameController = controller;
} }
@@ -1,5 +1,6 @@
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.game.player.PlayerId;
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.Duration;
import java.time.Instant; import java.time.Instant;
@@ -170,6 +171,56 @@ public class LobbyManager {
} }
} }
/**
* Renames a player across lobby mapping, lobby player list and running game ids.
*
* @param oldUsername old username
* @param newUsername new username
* @return true if rename was applied
*/
public synchronized boolean renamePlayer(String oldUsername, String newUsername) {
if (oldUsername == null || newUsername == null) {
return false;
}
if (oldUsername.equals(newUsername)) {
return true;
}
LobbyId lobbyId = playerToLobby.get(oldUsername);
if (lobbyId == null) {
return true;
}
if (playerToLobby.containsKey(newUsername)) {
return false;
}
Lobby lobby = activeLobbies.get(lobbyId);
if (lobby == null) {
playerToLobby.remove(oldUsername);
return true;
}
boolean lobbyRenamed = lobby.renamePlayer(oldUsername, newUsername);
if (!lobbyRenamed) {
return false;
}
if (lobby.getGameController() != null) {
boolean gameRenamed =
lobby.getGameController()
.renamePlayer(PlayerId.of(oldUsername), PlayerId.of(newUsername));
if (!gameRenamed) {
// Best-effort rollback to keep structures consistent.
lobby.renamePlayer(newUsername, oldUsername);
return false;
}
}
playerToLobby.remove(oldUsername);
playerToLobby.put(newUsername, lobbyId);
return true;
}
/** /**
* 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
@@ -200,6 +200,10 @@ public class UserRegistry {
return false; return false;
} }
if (user.getName().equals(newName)) {
return true;
}
if (byName.containsKey(newName)) { if (byName.containsKey(newName)) {
return false; return false;
} }
@@ -81,25 +81,24 @@
</GridPane.margin> </GridPane.margin>
</Button> </Button>
<!-- Username input and login button --> <!-- Username input and login/change button -->
<!-- <HBox alignment="CENTER_LEFT" spacing="10"--> <HBox alignment="CENTER_LEFT" spacing="10"
<!-- GridPane.rowIndex="0"--> GridPane.rowIndex="0"
<!-- GridPane.columnIndex="0"--> GridPane.columnIndex="0"
<!-- GridPane.halignment="LEFT"--> GridPane.halignment="LEFT"
<!-- GridPane.valignment="TOP">--> GridPane.valignment="TOP">
<!-- <GridPane.margin>--> <GridPane.margin>
<!-- &lt;!&ndash; place below the 'CREATE A LOBBY' button &ndash;&gt;--> <Insets top="100" left="20" />
<!-- <Insets top="100" left="20" />--> </GridPane.margin>
<!-- </GridPane.margin>--> <TextField fx:id="usernameField"
<!-- <TextField fx:id="usernameField"--> promptText="Username"
<!-- promptText="Username"--> styleClass="gray-input-field"
<!-- styleClass="gray-input-field"--> maxWidth="180" />
<!-- maxWidth="180" />--> <Button text="Apply Name"
<!-- <Button text="Login"--> fx:id="loginButton"
<!-- fx:id="loginButton"--> onAction="#handleLoginButton"
<!-- onAction="#handleLoginButton"--> styleClass="button-create-lobby" />
<!-- styleClass="button-create-lobby" />--> </HBox>
<!-- </HBox>-->
<VBox fx:id="casinoTable" <VBox fx:id="casinoTable"
alignment="CENTER" alignment="CENTER"