Fix: improve round management, betting validation and game engine test coverage #290
@@ -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) {
|
for (String user : users) {
|
||||||
String value = user.split("\\=")[1];
|
if (user == null || !user.contains("=")) {
|
||||||
logger.info(value);
|
continue;
|
||||||
addWhisperUser(value);
|
|
||||||
}
|
}
|
||||||
|
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.
|
||||||
|
|||||||
+2
-1
@@ -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);
|
||||||
|
|||||||
+63
-5
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+112
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -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>
|
||||||
<!-- <!– place below the 'CREATE A LOBBY' button –>-->
|
<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"
|
||||||
|
|||||||
Reference in New Issue
Block a user