Add Game UI logic #255
+1
-1
@@ -32,7 +32,7 @@ dependencies {
|
|||||||
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
|
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
|
||||||
implementation("org.apache.logging.log4j:log4j-api:2.25.3")
|
implementation("org.apache.logging.log4j:log4j-api:2.25.3")
|
||||||
runtimeOnly("org.apache.logging.log4j:log4j-core:2.25.3")
|
runtimeOnly("org.apache.logging.log4j:log4j-core:2.25.3")
|
||||||
|
implementation("org.jspecify:jspecify:1.0.0")
|
||||||
// Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
|
// Source: https://mvnrepository.com/artifact/org.fusesource.jansi/jansi
|
||||||
runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
|
runtimeOnly("org.fusesource.jansi:jansi:2.4.2")
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
This Document states the Network-Protocol as it is currently implemented
|
|
||||||
|
|
||||||
## GET_MESSAGE_COUNT
|
|
||||||
This Command gets the number of messages, currently stored in the queue for the specific client.
|
|
||||||
(Is used together with GET_NEXT_MESSAGE, to get all messages that are currently in the queue)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```
|
|
||||||
GET_MESSAGE_COUNT
|
|
||||||
|
|
||||||
1
|
|
||||||
+OK
|
|
||||||
```
|
|
||||||
|
|
||||||
## GET_NEXT_MESSAGE
|
|
||||||
This Command gets the next Message that is being stored in the queue for the client.
|
|
||||||
(Command is being sent the amount of times, the GET_MESSAGE_COUNT Command returns)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```
|
|
||||||
GET_NEXT_MESSAGE
|
|
||||||
|
|
||||||
TYPE=LOBBY GAME=1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
|
|
||||||
+OK
|
|
||||||
```
|
|
||||||
|
|
||||||
## SEND_MESSAGE
|
|
||||||
This Command gets sent if the user of that client writes a message to one of the three possible chats
|
|
||||||
|
|
||||||
(The Arguments / Parameters of the Command describe all the parameters of the Object "Message" being used internally by both the Client and the Server)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```
|
|
||||||
SEND_MESSAGE TYPE=LOBBY GAME=1 USER=player1 TARGET=null TIME=10:30 TEXT="Hallo Welt"
|
|
||||||
|
|
||||||
+OK
|
|
||||||
```
|
|
||||||
|
|
||||||
## LOG_IN
|
|
||||||
This Command is used to create a user on the server and associate that user with a username
|
|
||||||
Server returns the username, slightly changed if it is already used by someone else, and an ID to identify the client.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```
|
|
||||||
LOG_IN USERNAME="Peter"
|
|
||||||
|
|
||||||
USERNAME="Peter" ID=<random UUID>
|
|
||||||
+OK
|
|
||||||
```
|
|
||||||
|
|
||||||
## LOG_OUT
|
|
||||||
This Command is used to quit the connection between client and server.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```
|
|
||||||
LOG_OUT
|
|
||||||
|
|
||||||
+OK
|
|
||||||
```
|
|
||||||
@@ -342,3 +342,115 @@ LIST_USERS
|
|||||||
END
|
END
|
||||||
END
|
END
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## SEND_MESSAGE command
|
||||||
|
The `SEND_MESSAGE` command is used to transfer the chat message sent by a user to the server.
|
||||||
|
### Required pre-execution checks
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|:---------|:----------------|:-------------------------------------------------------------------|
|
||||||
|
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
|
||||||
|
| `GAME` | `int` | ID for identifying the Lobby Chat |
|
||||||
|
| `USER` | `String` | Username of the player that sent the message |
|
||||||
|
| `TARGET` | `String` | Username of to which the message is sent to (only in Whisper Chat) |
|
||||||
|
| `TIME` | `String` | Timestamp, when the message was sent |
|
||||||
|
| `TEXT` | `String` | Content of the message |
|
||||||
|
|
||||||
|
| Members of `ChatType` | Description |
|
||||||
|
|:----------------------|:--------------------------------|
|
||||||
|
| `GLOBAL` | Message is for the global chat |
|
||||||
|
| `LOBBY` | Message is for the lobby chat |
|
||||||
|
| `WHISPER` | Message is for the whisper chat |
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
No response fields.
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
```
|
||||||
|
SEND_MESSAGE TYPE=GLOBAL GAME=1 USER=player1 TARGET=null TIME='10:30' TEXT='Hello World'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
### Required pre-execution checks
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
No parameters.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|:--------|:------|:-------------------------------|
|
||||||
|
| `COUNT` | `int` | The current number of messages |
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
| Code | Description |
|
||||||
|
| :------------------- |:---------------------------------------------------------------------------------|
|
||||||
|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
|
||||||
|
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
```
|
||||||
|
GET_MESSAGE_COUNT
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
COUNT=10
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## GET_NEXT_MESSAGE command
|
||||||
|
The `GET_NEXT_MESSAGE` command is used to get the next message stored in a queue for the client.
|
||||||
|
### Required pre-execution checks
|
||||||
|
None.
|
||||||
|
|
||||||
|
### Request Parameters
|
||||||
|
No parameters.
|
||||||
|
|
||||||
|
### Success Response
|
||||||
|
| Field | Type | Description |
|
||||||
|
|:---------|:----------------|:-------------------------------------------------------------------|
|
||||||
|
| `TYPE` | `Enum<ChatType` | Member of Enum, indicating with chat type is used |
|
||||||
|
| `GAME` | `int` | ID for identifying the Lobby Chat |
|
||||||
|
| `USER` | `String` | Username of the player that sent the message |
|
||||||
|
| `TARGET` | `String` | Username of to which the message is sent to (only in Whisper Chat) |
|
||||||
|
| `TIME` | `String` | Timestamp, when the message was sent |
|
||||||
|
| `TEXT` | `String` | Content of the message |
|
||||||
|
|
||||||
|
| Members of `ChatType` | Description |
|
||||||
|
|:----------------------|:--------------------------------|
|
||||||
|
| `GLOBAL` | Message is for the global chat |
|
||||||
|
| `LOBBY` | Message is for the lobby chat |
|
||||||
|
| `WHISPER` | Message is for the whisper chat |
|
||||||
|
|
||||||
|
### Error Response
|
||||||
|
| Code | Description |
|
||||||
|
| :------------------- |:---------------------------------------------------------------------------------|
|
||||||
|
| `NO_USER_ASSOCIATED` | The session has no user associated, there is no queue of messages for the client |
|
||||||
|
|
||||||
|
### Example Request
|
||||||
|
```
|
||||||
|
GET_NEXT_MESSAGE
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Response
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
TYPE=GLOBAL GAME=-1 USER=player1 TARGET=null TIME=9:30 TEXT="Guten Tag"
|
||||||
|
END
|
||||||
|
```
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Our Network Protocol Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Our protocol is inspired by *POP3*, but has been highly customized to fit our specific needs.
|
||||||
|
It is a text-based protocol operating over raw TCP sockets, designed for human readability and strict structure.
|
||||||
|
|
||||||
|
## Packet Structure
|
||||||
|
Each network packet consists of:
|
||||||
|
- **4-byte header**: Specifies the size of the payload (big-endian integer).
|
||||||
|
- **4-byte request ID**: Generated by the client, used to match requests and responses.
|
||||||
|
- **Payload**: The actual data, its size as specified by the header.
|
||||||
|
|
||||||
|
This structure is used for both requests and responses.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
- All keys (in both requests and responses) use UPPER_SNAKE_CASE.
|
||||||
|
- Only a-z, A-Z, 0-9 are allowed in keys.
|
||||||
|
- Only human-readable strings are transmitted.
|
||||||
|
- Binary data is not allowed.
|
||||||
|
|
||||||
|
## Request Format
|
||||||
|
A request consists of a single line:
|
||||||
|
|
||||||
|
```
|
||||||
|
COMMAND KEY1=ARG1 KEY2=ARG2
|
||||||
|
```
|
||||||
|
|
||||||
|
- **COMMAND**: The action to perform.
|
||||||
|
- **KEY=VALUE pairs**: Optional parameters. There may be zero or more.
|
||||||
|
- **Whitespace**: Extra spaces between key, separator, and value are ignored. Any other characters between them are an error.
|
||||||
|
- **Standalone values**: Not allowed. Every value must have a key.
|
||||||
|
|
||||||
|
### String Values
|
||||||
|
- Strings with spaces must be enclosed in single quotes: `'example string'`.
|
||||||
|
- Inside quoted strings, line breaks are allowed.
|
||||||
|
- To include a single quote inside a string, escape it (e.g., `'It\'s fine'`).
|
||||||
|
|
||||||
|
If these rules are violated, the request is considered invalid and will be rejected.
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
Responses are more complex and can represent nested collections.
|
||||||
|
|
||||||
|
- **Success**: Starts with `+OK`
|
||||||
|
- **Error**: Starts with `-ERR`
|
||||||
|
- After the status, a newline follows, then fields in the format `KEY=VALUE`.
|
||||||
|
- Collections and elements are ended with the `END` keyword.
|
||||||
|
- A collection starts with a key, and its elements are indented.
|
||||||
|
- A element starts with a key, and its fields are indented.
|
||||||
|
|
||||||
|
### Example: Nested Collection
|
||||||
|
```
|
||||||
|
+OK
|
||||||
|
KEY1=VALUE1
|
||||||
|
FIELDS
|
||||||
|
FIELD
|
||||||
|
NESTED_KEY=NESTED_VALUE
|
||||||
|
END
|
||||||
|
END
|
||||||
|
KEY2=VALUE2
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
Any violation of the format (invalid characters, unescaped quotes, binary data, etc.) results in the request being rejected with an error response.
|
||||||
|
|
||||||
|
### Example Error Response When Violating Syntax Rules:
|
||||||
|
```
|
||||||
|
-ERR
|
||||||
|
CODE=PARSING_ERROR
|
||||||
|
MSG='Error occured during parsing. Likely due to malformed payload.'
|
||||||
|
END
|
||||||
|
```
|
||||||
@@ -42,6 +42,10 @@ public class ClientApp {
|
|||||||
System.setProperty("casono.server.host", host);
|
System.setProperty("casono.server.host", host);
|
||||||
System.setProperty("casono.server.port", Integer.toString(port));
|
System.setProperty("casono.server.port", Integer.toString(port));
|
||||||
// Forward the original address argument to the launcher as well.
|
// Forward the original address argument to the launcher as well.
|
||||||
Launcher.main(new String[] { arg });
|
Launcher.main(new String[] {arg});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
start(args[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,64 +2,128 @@ package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
|||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ChatClient;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* responsible for the transferring of messages from the server to the ChatModel
|
* responsible for the transferring of messages from the server to the ChatModel or from the
|
||||||
* or from the ChatViewController to the server
|
* ChatViewController to the server
|
||||||
*/
|
*/
|
||||||
public class ChatController {
|
public class ChatController {
|
||||||
|
|
||||||
private final String username;
|
private final String username;
|
||||||
private final ClientService clientService;
|
|
||||||
private final ChatClient chatClient;
|
private final ChatClient chatClient;
|
||||||
|
|
||||||
private ChatModel chatModel;
|
public ChatBoxController getChatBoxController() {
|
||||||
|
return chatBoxController;
|
||||||
|
}
|
||||||
|
|
||||||
private int game_id;
|
private final ChatBoxController chatBoxController;
|
||||||
|
private int lobbyId = -1;
|
||||||
|
private final Timer timer;
|
||||||
|
|
||||||
|
public record ChatKey(ChatType type, @Nullable String targetUser) {
|
||||||
|
public ChatKey(ChatType type) {
|
||||||
|
this(type, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<ChatKey, ChatModel> getChatModelMap() {
|
||||||
|
return chatModelMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<ChatKey, ChatModel> chatModelMap;
|
||||||
|
|
||||||
|
private static final long REFRESH_TIME = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor, adds TimerTask to be sent to the server regularly
|
||||||
|
*
|
||||||
|
* @param username
|
||||||
|
* @param clientService
|
||||||
|
*/
|
||||||
public ChatController(String username, ClientService clientService) {
|
public ChatController(String username, ClientService clientService) {
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.clientService = clientService;
|
chatClient = new ChatClient(clientService);
|
||||||
this.chatClient = new ChatClient(this.clientService);
|
chatModelMap = new LinkedHashMap<>();
|
||||||
|
this.chatBoxController = new ChatBoxController(username, this);
|
||||||
|
this.timer = new Timer();
|
||||||
|
timer.schedule(
|
||||||
|
new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
receiveMessage();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
public void createChat(int game_id) {
|
0,
|
||||||
chatModel = new ChatModel(ChatModel.ChatType.GLOBAL, username);
|
REFRESH_TIME);
|
||||||
this.game_id = game_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatModel getChatModel() {
|
|
||||||
return chatModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* method to send a message, the ChatViewController received to the server
|
* Method to be activated, if a lobby has be chosen. It will update the UI and add a new
|
||||||
|
* ChatModel to hold the Data for the Lobby Chat
|
||||||
|
*
|
||||||
|
* @param lobbyId
|
||||||
*/
|
*/
|
||||||
public void sendMessage(String msg, String username) {
|
public void setLobbyChat(int lobbyId) {
|
||||||
Message message = new Message(Message.MessageType.GLOBAL, 0, username, null, msg);
|
this.lobbyId = lobbyId;
|
||||||
onSendToNetwork(message);
|
ChatModel lobbyChatModel = new ChatModel(ChatType.LOBBY, username, lobbyId, null);
|
||||||
|
chatModelMap.put(new ChatKey(ChatType.LOBBY), lobbyChatModel);
|
||||||
|
this.chatBoxController.addChatTab("Lobby", lobbyChatModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** method to get all messages from the server */
|
||||||
* method to get all messages from the server
|
public void receiveMessage() {
|
||||||
*/
|
|
||||||
public Boolean receiveMessage() {
|
|
||||||
List<Message> newMessages = chatClient.getMessages();
|
List<Message> newMessages = chatClient.getMessages();
|
||||||
if (!newMessages.isEmpty()) {
|
if (!newMessages.isEmpty()) {
|
||||||
for (Message msg : newMessages) {
|
for (Message msg : newMessages) {
|
||||||
chatModel.addMessage(msg);
|
switch (msg.getMessageType()) {
|
||||||
|
case ChatType.GLOBAL:
|
||||||
|
chatModelMap.get(new ChatKey(ChatType.GLOBAL)).addMessage(msg);
|
||||||
|
break;
|
||||||
|
case ChatType.LOBBY:
|
||||||
|
if (msg.lobbyId == lobbyId) {
|
||||||
|
chatModelMap
|
||||||
|
.computeIfAbsent(
|
||||||
|
new ChatKey(ChatType.LOBBY),
|
||||||
|
(_key) ->
|
||||||
|
new ChatModel(
|
||||||
|
ChatType.LOBBY,
|
||||||
|
username,
|
||||||
|
msg.lobbyId,
|
||||||
|
null))
|
||||||
|
.addMessage(msg);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ChatType.WHISPER:
|
||||||
|
if (msg.target.equals(username)) {
|
||||||
|
if (chatModelMap.containsKey(
|
||||||
|
new ChatKey(ChatType.WHISPER, msg.sender))) {
|
||||||
|
chatModelMap
|
||||||
|
.get(new ChatKey(ChatType.WHISPER, msg.sender))
|
||||||
|
.addMessage(msg);
|
||||||
|
} else {
|
||||||
|
chatBoxController.addWhisperChat(msg.sender);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
} else { return false; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* method to send a message to the server
|
* method to send a message to the server
|
||||||
|
*
|
||||||
* @param message
|
* @param message
|
||||||
*/
|
*/
|
||||||
public void onSendToNetwork(Message message) {
|
public void onSendToNetwork(Message message) {
|
||||||
chatClient.sendMessage(message);
|
chatClient.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,85 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ChatModel, stores the data for a specific chat
|
* ChatModel, stores the data for a specific chat
|
||||||
*
|
*
|
||||||
* Holds the current state of a chat
|
* <p>Holds the current state of a chat
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class ChatModel {
|
public class ChatModel {
|
||||||
|
|
||||||
|
private ArrayList<Consumer<Message>> listeners = new ArrayList<>();
|
||||||
|
|
||||||
public ArrayList<Message> messages;
|
public ArrayList<Message> messages;
|
||||||
|
|
||||||
public ChatType chattype;
|
private final ChatType chattype;
|
||||||
|
|
||||||
public String username;
|
/** The person currently using this client */
|
||||||
|
public final String username;
|
||||||
|
|
||||||
public int count;
|
/** The person to send the message to If the chat is a whisper chat */
|
||||||
|
private final String target;
|
||||||
|
|
||||||
public enum ChatType {
|
private final IntegerProperty count;
|
||||||
GLOBAL,
|
|
||||||
LOBBY,
|
public int lobbyId;
|
||||||
WHISPER
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new ChatModel, given a username of the client
|
* Constructs a new ChatModel for a specific chat type.
|
||||||
* @param chattype
|
*
|
||||||
* @param username
|
* @param chattype The type of chat (e.g., GLOBAL, LOBBY, or WHISPER).
|
||||||
|
* @param username The username of the current user.
|
||||||
|
* @param lobbyId The ID of the lobby, or -1 if not applicable.
|
||||||
|
* @param target The username of the whisper recipient, or null for other chat types.
|
||||||
*/
|
*/
|
||||||
|
public ChatModel(ChatType chattype, String username, int lobbyId, String target) {
|
||||||
public ChatModel(ChatType chattype, String username) {
|
|
||||||
this.messages = new ArrayList<Message>();
|
this.messages = new ArrayList<Message>();
|
||||||
this.chattype = chattype;
|
this.chattype = chattype;
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.count = 0;
|
this.count = new SimpleIntegerProperty(0);
|
||||||
|
this.lobbyId = lobbyId;
|
||||||
|
this.target = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* method, used by the ChatViewController, to access all new messages, that are stored in the ChatModel
|
* Returns the type of chat this model represents.
|
||||||
|
*
|
||||||
|
* @return The {@link ChatType}.
|
||||||
*/
|
*/
|
||||||
public synchronized String viewNextMessage() {
|
public ChatType getChattype() {
|
||||||
count--;
|
return chattype;
|
||||||
Message msg = messages.getLast();
|
|
||||||
return String.format("[%s] %s: %s", msg.timestamp, msg.user, msg.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new message
|
* Adds a new message to the history and notifies all registered listeners. This method is
|
||||||
* method used by the ChatController
|
* synchronized to ensure thread safety when updating the message list.
|
||||||
* @param msg
|
*
|
||||||
|
* @param msg The {@link Message} to be added.
|
||||||
*/
|
*/
|
||||||
public synchronized void addMessage(Message msg) {
|
public synchronized void addMessage(Message msg) {
|
||||||
messages.add(msg);
|
messages.add(msg);
|
||||||
count++;
|
listeners.stream().forEach((l) -> l.accept(messages.getLast()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* method to send all current messages to the ChatViewController, if needed
|
* Registers a listener to be notified whenever a new message is added to this model.
|
||||||
|
*
|
||||||
|
* @param listener A {@link Consumer} that processes the new {@link Message}.
|
||||||
*/
|
*/
|
||||||
public void addCompleteChat() {
|
public void addListener(Consumer<Message> listener) {
|
||||||
for (int i = 0; i < this.messages.size(); i++) {
|
this.listeners.add(listener);
|
||||||
Message msg = this.messages.get(i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the target user for this chat, primarily used for whispers.
|
||||||
|
*
|
||||||
|
* @return The target username or null.
|
||||||
|
*/
|
||||||
|
public String getTarget() {
|
||||||
|
return target;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||||
|
|
||||||
|
/** Describing the Type of the Chat */
|
||||||
|
public enum ChatType {
|
||||||
|
GLOBAL,
|
||||||
|
LOBBY,
|
||||||
|
WHISPER
|
||||||
|
}
|
||||||
@@ -1,57 +1,73 @@
|
|||||||
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.server.network.command.parsing.RequestParameter;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBodyBuilder;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.regex.Matcher;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.Optional;
|
||||||
|
import org.jspecify.annotations.NonNull;
|
||||||
/**
|
|
||||||
* Message Object for internal handling of Chat-Messages
|
|
||||||
* TODO: Should be used on both sides of the network
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
/** Message Object for internal handling of Chat-Messages */
|
||||||
public class Message {
|
public class Message {
|
||||||
private final MessageType type;
|
private final ChatType type;
|
||||||
private final String message;
|
private final String message;
|
||||||
public String user;
|
public String sender;
|
||||||
public String timestamp;
|
public String timestamp;
|
||||||
public int game_id = 0;
|
public int lobbyId = 0;
|
||||||
public String target = null;
|
public String target = null;
|
||||||
public enum MessageType {
|
|
||||||
GLOBAL, LOBBY, WHISPER
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for creating Messages with all information given
|
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
|
||||||
* @param type - Either global, local or whisper
|
* received from the server. Used for Messages in the Lobby Chat.
|
||||||
* @param game_id - lobby id, or null, if the type is global
|
*
|
||||||
* @param user - username
|
* @param lobbyId The ID of the lobby, or -1 if not applicable.
|
||||||
* @param target - username of the target user, for whisper chat
|
* @param sender The username of the message creator.
|
||||||
* @param message
|
* @param timestamp The formatted time string (e.g., "HH:mm").
|
||||||
|
* @param message The actual text content of the message.
|
||||||
*/
|
*/
|
||||||
|
private Message(int lobbyId, String sender, String timestamp, String message) {
|
||||||
|
this.type = ChatType.LOBBY;
|
||||||
|
this.lobbyId = lobbyId;
|
||||||
|
this.sender = sender;
|
||||||
|
this.target = null;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
public Message(MessageType type, int game_id, String user, String target, String timestamp, String message) {
|
/**
|
||||||
|
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
|
||||||
|
* received from the server. Used for Messages in the WHISPER and GLOBAL Chat.
|
||||||
|
*
|
||||||
|
* @param type The chat category (e.g., GLOBAL or WHISPER).
|
||||||
|
* @param sender The username of the message creator.
|
||||||
|
* @param timestamp The formatted time string (e.g., "HH:mm").
|
||||||
|
* @param message The actual text content of the message.
|
||||||
|
*/
|
||||||
|
private Message(ChatType type, String sender, String target, String timestamp, String message) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.game_id = game_id;
|
this.lobbyId = -1;
|
||||||
this.user = user;
|
this.sender = sender;
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for creating the Messages of the current user, using this client -> time of writing is being recorded
|
* Constructs a new Message for the current user. Automatically generates a timestamp based on
|
||||||
* @param type - Either global, local or whisper
|
* the local system time ("HH:mm").
|
||||||
* @param game_id - lobby id, or null, if the type is global
|
*
|
||||||
* @param user - username
|
* @param type The chat category (e.g., GLOBAL, LOBBY, or WHISPER).
|
||||||
* @param target - username of the target user, for whisper chat
|
* @param lobbyId The ID of the lobby, or -1 if not applicable.
|
||||||
* @param message
|
* @param sender The username of the current user.
|
||||||
|
* @param target The username of the recipient (for whispers).
|
||||||
|
* @param message The actual text content to be sent.
|
||||||
*/
|
*/
|
||||||
|
public Message(ChatType type, int lobbyId, String sender, String target, String message) {
|
||||||
public Message(MessageType type, int game_id, String user, String target, String message) {
|
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.game_id = game_id;
|
this.lobbyId = lobbyId;
|
||||||
this.user = user;
|
this.sender = sender;
|
||||||
this.target = target;
|
this.target = target;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
@@ -59,78 +75,135 @@ public class Message {
|
|||||||
this.timestamp = now.format(formatter);
|
this.timestamp = now.format(formatter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the text content of the message.
|
||||||
|
*
|
||||||
|
* @return The message string.
|
||||||
|
*/
|
||||||
public String getMessage() {
|
public String getMessage() {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageType getMessageType() {
|
/**
|
||||||
|
* Returns the type of chat this message belongs to.
|
||||||
|
*
|
||||||
|
* @return The {@link ChatType}.
|
||||||
|
*/
|
||||||
|
public ChatType getMessageType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Method to test the system
|
|
||||||
* @return - String representation of the Message instance, as for example "player1: Hello World"
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return String.format("%s: %s", this.user, this.message);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Method to create the request representation of the message object, to be sent to the server
|
* Formats the message object into a string representation compatible with the network protocol
|
||||||
* @return - request as specified in the network protocol, as String
|
* arguments.
|
||||||
|
*
|
||||||
|
* @return A formatted string containing all message attributes for server transmission.
|
||||||
*/
|
*/
|
||||||
public String toArgsString() {
|
public String toArgsString() {
|
||||||
return String.format("TYPE=%s GAME=%d USER=%s TARGET=%s TIME=%s TEXT=%s",
|
String gameIdString = "";
|
||||||
this.type.toString(), this.game_id, this.user, this.target, this.timestamp, this.message);
|
if (lobbyId >= 0) {
|
||||||
|
gameIdString = " GAME=" + lobbyId;
|
||||||
|
} else {
|
||||||
|
gameIdString = " GAME='-1'";
|
||||||
|
}
|
||||||
|
return String.format(
|
||||||
|
"TYPE=%s%s USER='%s' TARGET='%s' TIME='%s' TEXT='%s'",
|
||||||
|
this.type.toString(),
|
||||||
|
gameIdString,
|
||||||
|
this.sender,
|
||||||
|
this.target,
|
||||||
|
this.timestamp,
|
||||||
|
this.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pattern, to analyze the response String with the given parameters
|
* Parses a list of network request parameters to reconstruct a Message object. Handles
|
||||||
|
* different chat types (GLOBAL, LOBBY, WHISPER) and their specific requirements.
|
||||||
|
*
|
||||||
|
* @param parameters A list of {@link RequestParameter} received from the network.
|
||||||
|
* @return A new {@link Message} instance populated with the parsed data.
|
||||||
*/
|
*/
|
||||||
public static Pattern msgRex = Pattern.compile(
|
public static Message toMessageReqPars(List<RequestParameter> parameters) {
|
||||||
"TYPE=(?<type>\\w+) GAME=(?<game>\\w+) USER=(?<user>\\w+) TARGET=(?<target>\\w+) TIME=(?<time>[0-9:.]+) TEXT=(?<text>.*)$");
|
String typeString = getParString(parameters, "TYPE");
|
||||||
|
ChatType type = ChatType.valueOf(typeString);
|
||||||
|
return switch (type) {
|
||||||
|
case GLOBAL ->
|
||||||
|
new Message(
|
||||||
|
ChatType.GLOBAL,
|
||||||
|
getParString(parameters, "USER"),
|
||||||
|
null,
|
||||||
|
getParString(parameters, "TIME"),
|
||||||
|
getParString(parameters, "TEXT"));
|
||||||
|
case LOBBY ->
|
||||||
|
new Message(
|
||||||
|
Integer.parseInt(getParString(parameters, "GAME")),
|
||||||
|
getParString(parameters, "USER"),
|
||||||
|
getParString(parameters, "TIME"),
|
||||||
|
getParString(parameters, "TEXT"));
|
||||||
|
case WHISPER ->
|
||||||
|
new Message(
|
||||||
|
ChatType.WHISPER,
|
||||||
|
getParString(parameters, "USER"),
|
||||||
|
getParString(parameters, "TARGET"),
|
||||||
|
getParString(parameters, "TIME"),
|
||||||
|
getParString(parameters, "TEXT"));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to create a Message Object, from the information given by the String
|
* Helper method to extract a specific parameter value by its key.
|
||||||
* @param response - String that got sent as a response from the server
|
*
|
||||||
* @return - New Message Object
|
* @param parameters The list of parameters to search.
|
||||||
|
* @param keyString The key to look for.
|
||||||
|
* @return The value associated with the key.
|
||||||
|
* @throws RuntimeException if the key is not found.
|
||||||
*/
|
*/
|
||||||
public static Message toMessage(String response) {
|
private static @NonNull String getParString(
|
||||||
Matcher m = msgRex.matcher(response);
|
List<RequestParameter> parameters, String keyString) {
|
||||||
if (! m.matches()) {
|
return getParString(parameters, keyString, null);
|
||||||
throw new RuntimeException("Can not parse message: '" + response+"'");
|
|
||||||
}
|
}
|
||||||
String typeString=m.group("type");
|
|
||||||
|
|
||||||
switch (typeString) {
|
/**
|
||||||
case "GLOBAL":
|
* Helper method to extract a specific parameter value by its key, with a fallback default
|
||||||
return new Message(MessageType.GLOBAL,
|
* value.
|
||||||
0,
|
*
|
||||||
m.group("user"),
|
* @param parameters The list of parameters to search.
|
||||||
null,
|
* @param keyString The key to look for.
|
||||||
m.group("time"),
|
* @param defaultVal The value to return if the key is missing.
|
||||||
m.group("text"));
|
* @return The found value or the default value.
|
||||||
|
*/
|
||||||
case "LOBBY":
|
private static @NonNull String getParString(
|
||||||
return new Message(MessageType.LOBBY,
|
List<RequestParameter> parameters, String keyString, String defaultVal) {
|
||||||
Integer.parseInt(m.group("game")),
|
Optional<String> parOption =
|
||||||
m.group("user"),
|
parameters.stream()
|
||||||
null,
|
.filter((p) -> keyString.equals(p.key()))
|
||||||
m.group("time"),
|
.findFirst()
|
||||||
m.group("text"));
|
.map(RequestParameter::value);
|
||||||
|
if (parOption.isEmpty()) {
|
||||||
case "WHISPER":
|
if (defaultVal == null) {
|
||||||
return new Message(MessageType.WHISPER,
|
throw new RuntimeException("No " + keyString + " found");
|
||||||
Integer.parseInt(m.group("game")),
|
} else {
|
||||||
m.group("user"),
|
return defaultVal;
|
||||||
m.group("target"),
|
|
||||||
m.group("time"),
|
|
||||||
m.group("text"));
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("Unknown message type " + typeString);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return parOption.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the message object into a network response body using the provided builder.
|
||||||
|
*
|
||||||
|
* @param builder The {@link ResponseBodyBuilder} used to construct the response.
|
||||||
|
* @return The built {@link ResponseBody} containing the message data.
|
||||||
|
*/
|
||||||
|
public ResponseBody toResponse(ResponseBodyBuilder builder) {
|
||||||
|
builder.param("TYPE", type.name());
|
||||||
|
builder.param("GAME", lobbyId);
|
||||||
|
builder.param("USER", this.sender);
|
||||||
|
if (target != null) {
|
||||||
|
builder.param("TARGET", target);
|
||||||
|
}
|
||||||
|
builder.param("TIME", this.timestamp);
|
||||||
|
builder.param("TEXT", this.message);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
package ch.unibas.dmi.dbis.cs108.casono.client.game;
|
||||||
|
|
||||||
/**
|
/** Represents a playing card with a value and suit. */
|
||||||
* Represents a playing card with a value and suit.
|
|
||||||
*/
|
|
||||||
public class Card {
|
public class Card {
|
||||||
private String value;
|
private String value;
|
||||||
private String suit;
|
private String suit;
|
||||||
|
|||||||
@@ -1,60 +1,71 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ChatClient class is responsible for sending messages to the server and
|
* The ChatClient class is responsible for sending messages to the server and retrieving messages
|
||||||
* retrieving messages from the server. It uses the ClientService to send
|
* from the server. It uses the ClientService to send commands and receive responses from the
|
||||||
* commands and receive responses from the server.
|
* server.
|
||||||
*/
|
*/
|
||||||
public class ChatClient {
|
public class ChatClient {
|
||||||
|
|
||||||
private ClientService clientService;
|
private final ClientService clientService;
|
||||||
|
private final Logger logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a ChatClient with the given ClientService for communication.
|
* Constructs a ChatClient with the given ClientService for communication.
|
||||||
*
|
*
|
||||||
* @param clientService The ClientService instance used to send commands and
|
* @param clientService The ClientService instance used to send commands and receive responses
|
||||||
* receive responses from the server.
|
* from the server.
|
||||||
*/
|
*/
|
||||||
public ChatClient(ClientService clientService) {
|
public ChatClient(ClientService clientService) {
|
||||||
this.clientService = clientService;
|
this.clientService = clientService;
|
||||||
|
this.logger = LogManager.getLogger(ChatClient.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a Message to the server by converting it to a string format and
|
* Send a Message to the server by converting it to a string format and sending a "SEND_MESSAGE"
|
||||||
* sending a "SEND_MESSAGE" command with the message content as arguments.
|
* command with the message content as arguments.
|
||||||
*
|
*
|
||||||
* @param message The Message object to be sent to the server.
|
* @param message The Message object to be sent to the server.
|
||||||
*/
|
*/
|
||||||
public void sendMessage(Message message) {
|
public void sendMessage(Message message) {
|
||||||
String request = "SEND_MESSAGE " + message.toArgsString();
|
String request = "SEND_MESSAGE " + message.toArgsString();
|
||||||
|
logger.info("Writing to server: " + request);
|
||||||
clientService.processCommand(request);
|
clientService.processCommand(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT"
|
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT" command to determine
|
||||||
* command to determine how many messages are available and then sending
|
* how many messages are available and then sending "GET_NEXT_MESSAGE" commands in a loop to
|
||||||
* "GET_NEXT_MESSAGE" commands in a loop to retrieve each message. The
|
* retrieve each message. The retrieved messages are parsed into Message objects and returned as
|
||||||
* retrieved messages are parsed into Message objects and returned as a list.
|
* a list.
|
||||||
*
|
*
|
||||||
* @return A list of Message objects representing the messages retrieved from
|
* @return A list of Message objects representing the messages retrieved from the server.
|
||||||
* the server.
|
|
||||||
*/
|
*/
|
||||||
public List<Message> getMessages() {
|
public List<Message> getMessages() {
|
||||||
String countStr = clientService.processCommand("GET_MESSAGE_COUNT");
|
logger.info("Asking server for new messages");
|
||||||
int count = Integer.parseInt(countStr);
|
List<RequestParameter> countStr =
|
||||||
System.out.println("Got " + count + " messages");
|
ClientService.convertToRequestParameters(
|
||||||
List<Message> messages = new ArrayList<>();
|
clientService.processCommand("GET_MESSAGE_COUNT"));
|
||||||
for (int i = 0; i < count; i++) {
|
RequestParameter countRes = countStr.getFirst();
|
||||||
String message = clientService.processCommand("GET_NEXT_MESSAGE");
|
if (!countRes.key().equals("COUNT")) {
|
||||||
if (message != null) {
|
logger.error("Not the right response from server");
|
||||||
Message message1 = Message.toMessage(message);
|
|
||||||
messages.add(message1);
|
|
||||||
}
|
}
|
||||||
|
int count = Integer.parseInt(countRes.value());
|
||||||
|
logger.info("Got " + count + " messages");
|
||||||
|
ArrayList<Message> messages = new ArrayList<>();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
List<RequestParameter> msgRes =
|
||||||
|
ClientService.convertToRequestParameters(
|
||||||
|
clientService.processCommand("GET_NEXT_MESSAGE"));
|
||||||
|
Message msg = Message.toMessageReqPars(msgRes);
|
||||||
|
messages.add(msg);
|
||||||
}
|
}
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ClientService class is responsible for managing the connection to the
|
* The ClientService class is responsible for managing the connection to the server, sending
|
||||||
* server,
|
* commands, and receiving responses. It uses a TcpTransport to communicate with the server and an
|
||||||
* sending commands, and receiving responses. It uses a TcpTransport to
|
* ExecutorService to handle asynchronous requests.
|
||||||
* communicate
|
|
||||||
* with the server and an ExecutorService to handle asynchronous requests.
|
|
||||||
*/
|
*/
|
||||||
public class ClientService {
|
public class ClientService {
|
||||||
|
|
||||||
@@ -30,11 +32,12 @@ public class ClientService {
|
|||||||
|
|
||||||
public static ArrayList<String> response;
|
public static ArrayList<String> response;
|
||||||
private final AtomicInteger idGenerator;
|
private final AtomicInteger idGenerator;
|
||||||
|
private Logger logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a ClientService with the given server IP and port. It establishes
|
* Constructs a ClientService with the given server IP and port. It establishes a socket
|
||||||
* a socket connection to the server and initializes the TcpTransport and
|
* connection to the server and initializes the TcpTransport and ExecutorService for
|
||||||
* ExecutorService for communication.
|
* communication.
|
||||||
*
|
*
|
||||||
* @param ip The IP address of the server to connect to.
|
* @param ip The IP address of the server to connect to.
|
||||||
* @param port The port number of the server to connect to.
|
* @param port The port number of the server to connect to.
|
||||||
@@ -43,10 +46,14 @@ public class ClientService {
|
|||||||
|
|
||||||
this.idGenerator = new AtomicInteger(0);
|
this.idGenerator = new AtomicInteger(0);
|
||||||
|
|
||||||
|
this.logger = LogManager.getLogger(ClientService.class);
|
||||||
|
|
||||||
this.offlineMode = false;
|
this.offlineMode = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
socket = new Socket(ip, port);
|
socket = new Socket(ip, port);
|
||||||
clienttcptransport = new TcpTransport(socket);
|
clienttcptransport = new TcpTransport(socket);
|
||||||
|
logger.info("Connected to server at " + ip);
|
||||||
} catch (IOException i) {
|
} catch (IOException i) {
|
||||||
throw new RuntimeException(i);
|
throw new RuntimeException(i);
|
||||||
}
|
}
|
||||||
@@ -55,8 +62,8 @@ public class ClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a ClientService in offline mode. No network connection will be
|
* Constructs a ClientService in offline mode. No network connection will be attempted and calls
|
||||||
* attempted and calls to processCommand will throw a RuntimeException.
|
* to processCommand will throw a RuntimeException.
|
||||||
*
|
*
|
||||||
* @param offline true to create an offline (no-network) client service
|
* @param offline true to create an offline (no-network) client service
|
||||||
*/
|
*/
|
||||||
@@ -68,56 +75,109 @@ public class ClientService {
|
|||||||
this.executor = Executors.newSingleThreadExecutor();
|
this.executor = Executors.newSingleThreadExecutor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Returns true if this ClientService is running in offline mode (no network). */
|
||||||
* Returns true if this ClientService is running in offline mode (no network).
|
|
||||||
*/
|
|
||||||
public boolean isOffline() {
|
public boolean isOffline() {
|
||||||
return offlineMode;
|
return offlineMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Pattern responseRex =
|
||||||
|
Pattern.compile(
|
||||||
|
"(?<key>\\w+)=(('(?<string>([^']|\\')+)')|(?<primVal>[+-]?[\\d\\w:]+))");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a command to the server and waits for the response. The command is
|
* Removes escape characters from a string, specifically converting escaped single quotes (\')
|
||||||
* sent using the TcpTransport, and the response is read in a loop until a
|
* back to regular single quotes (').
|
||||||
* valid response is received. The method handles "+OK" and "-ERROR" responses
|
|
||||||
* from the server and returns the actual response content.
|
|
||||||
*
|
*
|
||||||
* @param message The command message to be sent to the server.
|
* @param input The escaped string to process.
|
||||||
* @return The response from the server as a string.
|
* @return The unescaped string.
|
||||||
*/
|
*/
|
||||||
protected String processCommand(String message) {
|
private static String unescape(String input) {
|
||||||
if (offlineMode) {
|
return input.replaceAll("\\\\'", "'");
|
||||||
throw new RuntimeException("ClientService is offline: cannot process command");
|
|
||||||
}
|
}
|
||||||
AtomicReference<String> response = new AtomicReference<>();
|
|
||||||
sendRequest(() -> {
|
/**
|
||||||
|
* Converts a list of raw string parameters into a list of {@link RequestParameter} objects. It
|
||||||
|
* uses a regex matcher to distinguish between quoted strings (which are unescaped) and
|
||||||
|
* primitive values.
|
||||||
|
*
|
||||||
|
* @param input A list of raw strings to be parsed.
|
||||||
|
* @return A list of parsed {@link RequestParameter} objects.
|
||||||
|
* @throws RuntimeException if a parameter does not match the expected format.
|
||||||
|
*/
|
||||||
|
public static List<RequestParameter> convertToRequestParameters(List<String> input) {
|
||||||
|
return input.stream()
|
||||||
|
.map((String parString) -> responseRex.matcher(parString))
|
||||||
|
.filter(Matcher::matches)
|
||||||
|
.map(
|
||||||
|
(m) -> {
|
||||||
|
if (!(m.group("string") == null)) {
|
||||||
|
return new RequestParameter(
|
||||||
|
m.group("key"), unescape(m.group("string")));
|
||||||
|
} else if (!(m.group("primVal") == null)) {
|
||||||
|
return new RequestParameter(m.group("key"), m.group("primVal"));
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a command to the server and processes the multi-line response. It handles the protocol
|
||||||
|
* handshake (expecting +OK), strips leading tabs from response lines, and collects them until
|
||||||
|
* the "END" marker is reached.
|
||||||
|
*
|
||||||
|
* @param message The raw command string to be sent to the transport layer.
|
||||||
|
* @return A list of response lines received from the server (excluding protocol markers).
|
||||||
|
* @throws RuntimeException if the server responds with an error or if a communication failure
|
||||||
|
* occurs.
|
||||||
|
*/
|
||||||
|
protected List<String> processCommand(String message) {
|
||||||
|
List<String> response = new ArrayList<>();
|
||||||
|
sendRequest(
|
||||||
|
() -> {
|
||||||
try {
|
try {
|
||||||
writeToTransport(message);
|
writeToTransport(message);
|
||||||
String responseLine = null;
|
String responseText = null;
|
||||||
do {
|
|
||||||
responseLine = clienttcptransport.read().payload();
|
responseText = clienttcptransport.read().payload();
|
||||||
System.out.println("Raw message '" + responseLine + "'");
|
logger.info("Raw message '" + responseText + "'");
|
||||||
if ("+OK".equals(responseLine)) {
|
Boolean success = null;
|
||||||
return;
|
int count = 0;
|
||||||
} else if (("-ERROR").equals(responseLine)) {
|
for (String line : responseText.split("\n")) {
|
||||||
throw new RuntimeException(responseLine);
|
if (success == null) {
|
||||||
|
if ("+OK".equals(line)) {
|
||||||
|
success = true;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
} else if (("-ERROR").equals(responseText)) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
} else if ("END".equals(line)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
line = line.replaceFirst("^\t", "");
|
||||||
|
response.add(line);
|
||||||
|
}
|
||||||
|
if (success != null && success) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Error in " + message + ": " + response);
|
||||||
}
|
}
|
||||||
response.set(responseLine);
|
|
||||||
} while (true);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw getRuntimeException(e);
|
throw getRuntimeException(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return response.get();
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to send a request to the server using the ExecutorService. It
|
* Helper method to send a request to the server using the ExecutorService. It submits the
|
||||||
* submits the request as a Runnable task and waits for its completion. If
|
* request as a Runnable task and waits for its completion. If the task is interrupted or
|
||||||
* the task is interrupted or encounters an execution exception, it throws a
|
* encounters an execution exception, it throws a RuntimeException with the appropriate cause.
|
||||||
* RuntimeException with the appropriate cause.
|
|
||||||
*
|
*
|
||||||
* @param request The Runnable task representing the request to be sent to the
|
* @param request The Runnable task representing the request to be sent to the server.
|
||||||
* server.
|
|
||||||
*/
|
*/
|
||||||
private void sendRequest(Runnable request) {
|
private void sendRequest(Runnable request) {
|
||||||
Future<?> future = executor.submit(request);
|
Future<?> future = executor.submit(request);
|
||||||
@@ -131,11 +191,10 @@ public class ClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to extract the cause of an exception and return it as a
|
* Helper method to extract the cause of an exception and return it as a RuntimeException. If
|
||||||
* RuntimeException. If the cause is null, it returns the original exception as
|
* the cause is null, it returns the original exception as a RuntimeException. If the cause is
|
||||||
* a RuntimeException. If the cause is already a RuntimeException, it returns
|
* already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new
|
||||||
* it directly. Otherwise, it wraps the cause in a new RuntimeException and
|
* RuntimeException and returns it.
|
||||||
* returns it.
|
|
||||||
*
|
*
|
||||||
* @param e The exception from which to extract the cause.
|
* @param e The exception from which to extract the cause.
|
||||||
* @return A RuntimeException representing the cause of the original exception.
|
* @return A RuntimeException representing the cause of the original exception.
|
||||||
@@ -153,38 +212,33 @@ public class ClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the socket connection to the server and shuts down the
|
* Closes the socket connection to the server and shuts down the ExecutorService. It also closes
|
||||||
* ExecutorService.
|
* the TcpTransport used for communication. If any IOException occurs during this process, it
|
||||||
* It also closes the TcpTransport used for communication. If any IOException
|
* prints the exception to the console.
|
||||||
* occurs during this process, it prints the exception to the console.
|
|
||||||
*/
|
*/
|
||||||
public void closeSocket() {
|
public void closeSocket() {
|
||||||
try {
|
try {
|
||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
if (clienttcptransport != null) {
|
|
||||||
clienttcptransport.close();
|
clienttcptransport.close();
|
||||||
}
|
|
||||||
if (socket != null) {
|
|
||||||
socket.close();
|
socket.close();
|
||||||
}
|
logger.info("Socket closed");
|
||||||
} catch (IOException j) {
|
} catch (IOException j) {
|
||||||
System.out.println(j);
|
logger.debug(j);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to write a command string to the TcpTransport. It generates a
|
* Method to write with the tcp transport to the server
|
||||||
* unique ID for the command using the idGenerator and sends a RawPacket
|
|
||||||
* containing the ID and the command string to the server. If an IOException
|
|
||||||
* occurs during this process, it throws a RuntimeException with the cause.
|
|
||||||
*
|
*
|
||||||
* @param s The command string to be sent to the server.
|
* @param s - Message to be sent
|
||||||
* @throws IOException If an I/O error occurs while writing to the transport.
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
private void writeToTransport(String s) throws IOException {
|
private void writeToTransport(String s) throws IOException {
|
||||||
int id = this.idGenerator.incrementAndGet();
|
int id = this.idGenerator.incrementAndGet();
|
||||||
this.clienttcptransport.write(new RawPacket(id, s));
|
this.clienttcptransport.write(new RawPacket(id, s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ping() {
|
||||||
|
processCommand("PING");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The CoreClient class provides basic functionalities for communicating with
|
* The CoreClient class provides basic functionalities for communicating with the server, such as
|
||||||
* the
|
* sending a ping command to check connectivity and logging in with a username. It uses the
|
||||||
* server, such as sending a ping command to check connectivity and logging in
|
* ClientService to send commands and receive responses from the server.
|
||||||
* with a username. It uses the ClientService to send commands and receive
|
|
||||||
* responses from the server.
|
|
||||||
*/
|
*/
|
||||||
public class CoreClient {
|
public class CoreClient {
|
||||||
private final ClientService clientService;
|
private final ClientService clientService;
|
||||||
@@ -13,16 +11,16 @@ public class CoreClient {
|
|||||||
/**
|
/**
|
||||||
* Constructs a CoreClient with the given ClientService for communication.
|
* Constructs a CoreClient with the given ClientService for communication.
|
||||||
*
|
*
|
||||||
* @param clientservice The ClientService instance used to send commands and
|
* @param clientservice The ClientService instance used to send commands and receive responses
|
||||||
* receive responses from the server.
|
* from the server.
|
||||||
*/
|
*/
|
||||||
public CoreClient(ClientService clientservice) {
|
public CoreClient(ClientService clientservice) {
|
||||||
this.clientService = clientservice;
|
this.clientService = clientservice;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a "PING" command to the server to check connectivity. The server
|
* Sends a "PING" command to the server to check connectivity. The server should respond with a
|
||||||
* should respond with a "PONG" message if the connection is successful.
|
* "PONG" message if the connection is successful.
|
||||||
*/
|
*/
|
||||||
public void ping() {
|
public void ping() {
|
||||||
clientService.processCommand("PING");
|
clientService.processCommand("PING");
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The LobbyClient class is responsible for communicating with the server to
|
* The LobbyClient class is responsible for communicating with the server to manage game lobbies. It
|
||||||
* manage game lobbies. It provides methods to create a lobby, join a lobby,
|
* provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by
|
||||||
* and fetch the current status of a lobby by sending appropriate commands to
|
* sending appropriate commands to the server and processing the responses.
|
||||||
* the server and processing the responses.
|
|
||||||
*/
|
*/
|
||||||
public class LobbyClient {
|
public class LobbyClient {
|
||||||
private final ClientService client;
|
private final ClientService client;
|
||||||
@@ -12,8 +11,8 @@ public class LobbyClient {
|
|||||||
/**
|
/**
|
||||||
* Constructs a LobbyClient with the given ClientService for communication.
|
* Constructs a LobbyClient with the given ClientService for communication.
|
||||||
*
|
*
|
||||||
* @param client The ClientService instance used to send commands and receive
|
* @param client The ClientService instance used to send commands and receive responses from the
|
||||||
* responses from the server.
|
* server.
|
||||||
*/
|
*/
|
||||||
public LobbyClient(ClientService client) {
|
public LobbyClient(ClientService client) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
@@ -27,33 +26,29 @@ public class LobbyClient {
|
|||||||
* Fetch the current status of the lobby with the given id from the server.
|
* Fetch the current status of the lobby with the given id from the server.
|
||||||
*
|
*
|
||||||
* @param lobbyId The id of the lobby to fetch the status for.
|
* @param lobbyId The id of the lobby to fetch the status for.
|
||||||
* @return A string representing the current status of the lobby, as returned
|
* @return A string representing the current status of the lobby, as returned by the server.
|
||||||
* by the server.
|
|
||||||
*/
|
*/
|
||||||
public String fetchLobbyStatusString(int lobbyId) {
|
public String fetchLobbyStatusString(int lobbyId) {
|
||||||
return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId);
|
return client.processCommand("GET_LOBBY_STATUS ID=" + lobbyId).getFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the server to create a new lobby and return the id of the newly
|
* Request the server to create a new lobby and return the id of the newly created lobby.
|
||||||
* created lobby.
|
|
||||||
*
|
*
|
||||||
* @return The id of the newly created lobby, as returned by the server.
|
* @return The id of the newly created lobby, as returned by the server.
|
||||||
*/
|
*/
|
||||||
public int createLobby() {
|
public int createLobby() {
|
||||||
String response = client.processCommand("CREATE_LOBBY");
|
String response = client.processCommand("CREATE_LOBBY").getFirst();
|
||||||
return Integer.parseInt(response);
|
return Integer.parseInt(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request the server to return the id of the lobby that the client is
|
* Request the server to return the id of the lobby that the client is currently in.
|
||||||
* currently in.
|
|
||||||
*
|
*
|
||||||
* @return The id of the lobby that the client is currently in, as returned by
|
* @return The id of the lobby that the client is currently in, as returned by the server.
|
||||||
* the server.
|
|
||||||
*/
|
*/
|
||||||
public int getLobbyId() {
|
public int getLobbyId() {
|
||||||
String response = client.processCommand("GET_LOBBY_ID");
|
String response = client.processCommand("GET_LOBBY_ID").getFirst();
|
||||||
return Integer.parseInt(response);
|
return Integer.parseInt(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatType;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.MenuButton;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.control.Tab;
|
||||||
|
import javafx.scene.control.TabPane;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
public class ChatBoxController {
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
private ChatController chatController;
|
||||||
|
|
||||||
|
@FXML private VBox chatBox;
|
||||||
|
|
||||||
|
@FXML private TabPane chatTabPane;
|
||||||
|
|
||||||
|
@FXML private MenuButton addWhisperChatButton;
|
||||||
|
|
||||||
|
private FXMLLoader fxmlLoader;
|
||||||
|
|
||||||
|
@FXML private List<MenuItem> whisperUsers;
|
||||||
|
|
||||||
|
private String ressource = "/ui-structure/components/chatui/chattab.fxml";
|
||||||
|
|
||||||
|
public ChatBoxController(String username, ChatController chatController) {
|
||||||
|
this.username = username;
|
||||||
|
this.chatController = chatController;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* ChatController}'s model map.
|
||||||
|
*/
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
ChatModel globalChatModel = new ChatModel(ChatType.GLOBAL, username, -1, null);
|
||||||
|
chatController
|
||||||
|
.getChatModelMap()
|
||||||
|
.put(new ChatController.ChatKey(ChatType.GLOBAL), globalChatModel);
|
||||||
|
addChatTab("GLOBAL", globalChatModel);
|
||||||
|
// TODO: Button to add new Whisper Chat
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a specific user to the list of available whisper targets. Creates a new menu item for
|
||||||
|
* the user and defines the action to open a private chat tab when selected.
|
||||||
|
*
|
||||||
|
* @param targetUserName The username of the person to be added to the whisper list.
|
||||||
|
*/
|
||||||
|
public void addWhisperUser(String targetUserName) {
|
||||||
|
MenuItem menuItem = new MenuItem(targetUserName);
|
||||||
|
whisperUsers.add(menuItem);
|
||||||
|
addWhisperChatButton.getItems().add(menuItem);
|
||||||
|
menuItem.setOnAction(event -> addWhisperChat(targetUserName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new private (whisper) chat model for a specific target user, registers it within
|
||||||
|
* the chat system, and opens a new chat tab.
|
||||||
|
*
|
||||||
|
* @param target The username of the recipient for the private messages.
|
||||||
|
*/
|
||||||
|
public void addWhisperChat(String target) {
|
||||||
|
ChatModel chatModel = new ChatModel(ChatType.WHISPER, username, -1, target);
|
||||||
|
chatController
|
||||||
|
.getChatModelMap()
|
||||||
|
.put(new ChatController.ChatKey(ChatType.WHISPER, target), chatModel);
|
||||||
|
addChatTab(target, chatModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically loads a new chat tab from an FXML resource and attaches it to the TabPane. It
|
||||||
|
* initializes a {@link ChatViewController} for the tab and sets up a listener to display
|
||||||
|
* incoming messages in real-time.
|
||||||
|
*
|
||||||
|
* @param title The title to be displayed on the tab header.
|
||||||
|
* @param chatModel The {@link ChatModel} containing the data and logic for this specific chat.
|
||||||
|
* @throws RuntimeException If the FXML resource for the chat tab cannot be loaded.
|
||||||
|
*/
|
||||||
|
public void addChatTab(String title, ChatModel chatModel) {
|
||||||
|
URL resource = getClass().getResource(ressource);
|
||||||
|
FXMLLoader fxmlLoader = new FXMLLoader(resource);
|
||||||
|
try {
|
||||||
|
ChatViewController chatViewController =
|
||||||
|
new ChatViewController(this.chatController, chatModel, this.username);
|
||||||
|
fxmlLoader.setController(chatViewController);
|
||||||
|
Node load = fxmlLoader.load();
|
||||||
|
VBox.setVgrow(load, Priority.ALWAYS);
|
||||||
|
VBox vbox = new VBox();
|
||||||
|
VBox.setVgrow(vbox, Priority.ALWAYS);
|
||||||
|
vbox.getChildren().add(load);
|
||||||
|
chatModel.addListener((msg) -> chatViewController.showMessage(msg));
|
||||||
|
Tab newChat = new Tab(title, vbox);
|
||||||
|
this.chatTabPane.getTabs().add(newChat);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
-73
@@ -2,106 +2,101 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui;
|
|||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatController;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.ChatModel;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import java.util.Timer;
|
/** Responsible for the presentation of the ChatModel to the Client */
|
||||||
import java.util.TimerTask;
|
public class ChatViewController implements Initializable {
|
||||||
|
|
||||||
/**
|
@FXML private Button sendButton;
|
||||||
* Responsible for the presentation of the ChatModel to the Client
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class ChatViewController {
|
@FXML private TextField inputField;
|
||||||
|
|
||||||
|
@FXML private VBox chatInterfaceVBox;
|
||||||
|
|
||||||
|
@FXML private HBox controlBar;
|
||||||
|
|
||||||
|
@FXML private ScrollPane scrollPane;
|
||||||
|
|
||||||
|
@FXML private VBox chat;
|
||||||
|
|
||||||
|
private final ChatModel chatModel;
|
||||||
|
|
||||||
private final ChatModel chatmodel;
|
|
||||||
private final String username;
|
private final String username;
|
||||||
|
|
||||||
private final ChatController controller;
|
private final ChatController controller;
|
||||||
private final Timer timer;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private VBox chatVBox;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField inputField;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ScrollPane chatScrollPane;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button sendButton;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button refreshButton;
|
|
||||||
|
|
||||||
private static final int CHAT_PADDING = 20;
|
private static final int CHAT_PADDING = 20;
|
||||||
|
|
||||||
public ChatViewController() {
|
public ChatViewController(ChatController chatController, ChatModel chatModel, String username) {
|
||||||
this.username = null;
|
this.controller = chatController;
|
||||||
this.chatmodel = null;
|
|
||||||
this.controller = null;
|
|
||||||
this.timer = new Timer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChatViewController(String username, ChatModel chatmodel, ChatController controller) {
|
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.chatmodel = chatmodel;
|
this.chatModel = chatModel;
|
||||||
this.controller = controller;
|
}
|
||||||
this.timer = new Timer();
|
|
||||||
if (this.controller != null) {
|
/**
|
||||||
timer.schedule(new TimerTask() {
|
* Initializes the controller after the FXML root element has been processed. Sets up event
|
||||||
|
* handlers for sending messages via the input field or button and ensures the ScrollPane
|
||||||
|
* automatically scrolls to the bottom when new messages are added.
|
||||||
|
*
|
||||||
|
* @param location The location used to resolve relative paths for the root object.
|
||||||
|
* @param resourceBundle The resources used to localize the root object.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void initialize(URL location, ResourceBundle resourceBundle) {
|
||||||
System.err.println("tick");
|
|
||||||
if (ChatViewController.this.controller.receiveMessage()) {
|
|
||||||
showMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 0, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
public void initialize() {
|
|
||||||
if (inputField != null) {
|
|
||||||
inputField.setOnAction(event -> sendMessage());
|
inputField.setOnAction(event -> sendMessage());
|
||||||
}
|
|
||||||
if (sendButton != null) {
|
|
||||||
sendButton.setOnAction(event -> sendMessage());
|
sendButton.setOnAction(event -> sendMessage());
|
||||||
}
|
scrollPane.vvalueProperty().bind(chat.heightProperty());
|
||||||
if (refreshButton != null) {
|
|
||||||
refreshButton.setOnAction(event -> showMessage());
|
|
||||||
}
|
|
||||||
if (chatScrollPane != null && chatVBox != null) {
|
|
||||||
chatScrollPane.vvalueProperty().bind(chatVBox.heightProperty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showMessage() {
|
|
||||||
while (chatmodel.count > 0) {
|
|
||||||
String msg = chatmodel.viewNextMessage();
|
|
||||||
Label label = new Label(msg);
|
|
||||||
label.getStyleClass().add("info-text");
|
|
||||||
label.setWrapText(true);
|
|
||||||
label.maxWidthProperty().bind(chatVBox.widthProperty().subtract(CHAT_PADDING));
|
|
||||||
chatVBox.getChildren().add(label);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the text from the input field, creates a new {@link Message} object using the
|
||||||
|
* current model state, and passes it to the {@link ChatController} for network transmission.
|
||||||
|
* The input field is cleared after sending.
|
||||||
|
*/
|
||||||
public void sendMessage() {
|
public void sendMessage() {
|
||||||
String message = inputField.getText().trim();
|
String message = inputField.getText().trim();
|
||||||
if (!message.isEmpty()) {
|
if (!message.isEmpty()) {
|
||||||
inputField.clear();
|
inputField.clear();
|
||||||
this.controller.sendMessage(message, username);
|
Message msg =
|
||||||
|
new Message(
|
||||||
|
chatModel.getChattype(),
|
||||||
|
chatModel.lobbyId,
|
||||||
|
username,
|
||||||
|
chatModel.getTarget(),
|
||||||
|
message);
|
||||||
|
controller.onSendToNetwork(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void endController() {
|
/**
|
||||||
timer.cancel();
|
* Displays a message in the chat interface. This method creates a new Label for the message
|
||||||
|
* text and adds it to the message container. It uses {@link Platform#runLater(Runnable)} to
|
||||||
|
* ensure the UI update happens on the JavaFX Application Thread.
|
||||||
|
*
|
||||||
|
* @param msg The {@link Message} object containing the content and metadata to display.
|
||||||
|
*/
|
||||||
|
public void showMessage(Message msg) {
|
||||||
|
Platform.runLater(
|
||||||
|
() -> {
|
||||||
|
String msgText =
|
||||||
|
String.format(
|
||||||
|
"[%s] %s: %s", msg.timestamp, msg.sender, msg.getMessage());
|
||||||
|
Label label = new Label(msgText);
|
||||||
|
label.getStyleClass().add("info-text");
|
||||||
|
label.setWrapText(true);
|
||||||
|
label.maxWidthProperty().bind(chat.widthProperty().subtract(CHAT_PADDING));
|
||||||
|
chat.getChildren().add(label);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,10 +41,12 @@ public class Casinomainui extends Application {
|
|||||||
System.setProperty("casono.server.port", parts[1]);
|
System.setProperty("casono.server.port", parts[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
|
FXMLLoader fxmlLoader =
|
||||||
|
new FXMLLoader(getClass().getResource("/ui-structure/Casinomainui.fxml"));
|
||||||
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
|
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
|
||||||
stage.setTitle("Casono");
|
stage.setTitle("Casono");
|
||||||
javafx.scene.image.Image icon = new javafx.scene.image.Image(
|
javafx.scene.image.Image icon =
|
||||||
|
new javafx.scene.image.Image(
|
||||||
getClass().getResource("/images/logoinverted.png").toExternalForm());
|
getClass().getResource("/images/logoinverted.png").toExternalForm());
|
||||||
stage.getIcons().add(icon);
|
stage.getIcons().add(icon);
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
|
|||||||
+24
-35
@@ -1,12 +1,14 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Alert.AlertType;
|
import javafx.scene.control.Alert.AlertType;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.AnchorPane;
|
import javafx.scene.layout.AnchorPane;
|
||||||
@@ -14,34 +16,20 @@ import javafx.scene.layout.VBox;
|
|||||||
import javafx.scene.shape.Rectangle;
|
import javafx.scene.shape.Rectangle;
|
||||||
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 ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
|
||||||
|
|
||||||
/**
|
/** Controller for the Casono main UI lobby. Handles UI initialization and user actions. */
|
||||||
* Controller for the Casono main UI lobby. Handles UI initialization and user
|
|
||||||
* actions.
|
|
||||||
*/
|
|
||||||
public class CasinomainuiController {
|
public class CasinomainuiController {
|
||||||
private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class);
|
private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class);
|
||||||
|
|
||||||
@FXML
|
@FXML private AnchorPane rootPane;
|
||||||
private AnchorPane rootPane;
|
@FXML private Label titleLabel;
|
||||||
@FXML
|
@FXML private Label subtitleLabel;
|
||||||
private Label titleLabel;
|
@FXML private ImageView logoView;
|
||||||
@FXML
|
@FXML private Rectangle greenBox;
|
||||||
private Label subtitleLabel;
|
@FXML private Button exitbutton;
|
||||||
@FXML
|
@FXML private VBox casinoTable;
|
||||||
private ImageView logoView;
|
@FXML private TextField usernameField;
|
||||||
@FXML
|
@FXML private Button loginButton;
|
||||||
private Rectangle greenBox;
|
|
||||||
@FXML
|
|
||||||
private Button exitbutton;
|
|
||||||
@FXML
|
|
||||||
private VBox casinoTable;
|
|
||||||
@FXML
|
|
||||||
private TextField usernameField;
|
|
||||||
@FXML
|
|
||||||
private Button loginButton;
|
|
||||||
|
|
||||||
private LobbyButtonTranslationManager translationManager;
|
private LobbyButtonTranslationManager translationManager;
|
||||||
private LobbyButtonGridManager gridManager;
|
private LobbyButtonGridManager gridManager;
|
||||||
@@ -67,10 +55,16 @@ public class CasinomainuiController {
|
|||||||
try {
|
try {
|
||||||
clientService = new ClientService(host, port);
|
clientService = new ClientService(host, port);
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
LOGGER.warn("Could not connect to server {}:{} — starting in offline mode: {}", host, port, e.getMessage());
|
LOGGER.warn(
|
||||||
|
"Could not connect to server {}:{} — starting in offline mode: {}",
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
e.getMessage());
|
||||||
clientService = new ClientService(true); // offline mode
|
clientService = new ClientService(true); // offline mode
|
||||||
}
|
}
|
||||||
gridManager = new LobbyButtonGridManager(new javafx.scene.layout.GridPane(), translationManager, clientService);
|
gridManager =
|
||||||
|
new LobbyButtonGridManager(
|
||||||
|
new javafx.scene.layout.GridPane(), translationManager, clientService);
|
||||||
// LobbyClient will use the provided ClientService; in offline mode calls will
|
// LobbyClient will use the provided ClientService; in offline mode calls will
|
||||||
// fail with RuntimeException
|
// fail with RuntimeException
|
||||||
lobbyClient = new LobbyClient(clientService);
|
lobbyClient = new LobbyClient(clientService);
|
||||||
@@ -79,10 +73,7 @@ public class CasinomainuiController {
|
|||||||
gridManager.renderLobbyButtons();
|
gridManager.renderLobbyButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 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.getText();
|
||||||
@@ -108,9 +99,7 @@ public class CasinomainuiController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Shows an alert dialog with the given message. */
|
||||||
* Shows an alert dialog with the given message.
|
|
||||||
*/
|
|
||||||
private void showAlert(String message) {
|
private void showAlert(String message) {
|
||||||
Alert alert = new Alert(AlertType.INFORMATION);
|
Alert alert = new Alert(AlertType.INFORMATION);
|
||||||
alert.setTitle("Info");
|
alert.setTitle("Info");
|
||||||
|
|||||||
+160
-202
@@ -1,21 +1,17 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||||
|
|
||||||
/**
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||||
* ButtonID to LobbyID.
|
|
||||||
*/
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
@@ -24,212 +20,187 @@ import javafx.scene.layout.GridPane;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages the grid for lobby buttons and rendering. Uses
|
|
||||||
* LobbyButtonTranslationManager for mapping
|
|
||||||
* ButtonID to LobbyID.
|
|
||||||
*/
|
|
||||||
public class LobbyButtonGridManager {
|
public class LobbyButtonGridManager {
|
||||||
private static final double BUTTON_WIDTH_MARGIN = 20.0;
|
|
||||||
|
private static final double BTN_WIDTH_MRG = 20.0;
|
||||||
private static final double BUTTON_MIN_SIZE = 10.0;
|
private static final double BUTTON_MIN_SIZE = 10.0;
|
||||||
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
private static final int REFRESH_INTERVAL_SECONDS = 5;
|
||||||
|
private static final int INITIAL_DELAY_SECONDS = 5;
|
||||||
/** GridPane for the button grid. */
|
|
||||||
private final GridPane gridPane;
|
|
||||||
|
|
||||||
/** Manager for mapping ButtonID to LobbyID. */
|
|
||||||
private final LobbyButtonTranslationManager translationManager;
|
|
||||||
|
|
||||||
/** Number of columns in the grid. */
|
|
||||||
private static final int COLS = 4;
|
private static final int COLS = 4;
|
||||||
|
|
||||||
/** Image for a lobby in CREATED state. */
|
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||||
/** Default fallback image. */
|
|
||||||
private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png";
|
private static final String BUTTON_FALLBACK_IMAGE = "/images/lobbypictures/error.png";
|
||||||
|
|
||||||
/**
|
|
||||||
* Template for per-button images. Use: button index and status
|
|
||||||
* (created|running). Example:
|
|
||||||
* /images/lobby_1_created.png
|
|
||||||
*/
|
|
||||||
private static final String BUTTON_IMAGE_TEMPLATE = "/images/lobbypictures/lobby_%d_%s.png";
|
private static final String BUTTON_IMAGE_TEMPLATE = "/images/lobbypictures/lobby_%d_%s.png";
|
||||||
|
|
||||||
/** Cache for loaded Images keyed by resource path. */
|
private final GridPane gridPane;
|
||||||
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
private final LobbyButtonTranslationManager translationManager;
|
||||||
|
|
||||||
private final LobbyClient lobbyClient;
|
private final LobbyClient lobbyClient;
|
||||||
|
|
||||||
/** Executor for background status/network tasks. */
|
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||||
/** Scheduler for periodic refresh of lobby mappings. */
|
|
||||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for the GridManager.
|
|
||||||
*
|
|
||||||
* @param gridPane the GridPane for rendering
|
|
||||||
* @param translationManager the manager for mapping ButtonID to LobbyID
|
|
||||||
*/
|
|
||||||
public LobbyButtonGridManager(
|
public LobbyButtonGridManager(
|
||||||
GridPane gridPane, LobbyButtonTranslationManager translationManager, LobbyClient lobbyClient) {
|
GridPane gridPane,
|
||||||
|
LobbyButtonTranslationManager translationManager,
|
||||||
|
LobbyClient lobbyClient) {
|
||||||
|
|
||||||
this.gridPane = gridPane;
|
this.gridPane = gridPane;
|
||||||
// Always use the singleton
|
|
||||||
this.translationManager = LobbyButtonTranslationManager.getInstance();
|
this.translationManager = LobbyButtonTranslationManager.getInstance();
|
||||||
this.lobbyClient = lobbyClient;
|
this.lobbyClient = lobbyClient;
|
||||||
// Start periodic refresh to keep mapping in sync with server
|
|
||||||
startPeriodicRefresh(5, 5);
|
startPeriodicRefresh(REFRESH_INTERVAL_SECONDS, INITIAL_DELAY_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience constructor: accept a {@link ClientService} and build a
|
|
||||||
* {@link LobbyClient} from it. This avoids any host/port System.getProperty
|
|
||||||
* lookups elsewhere — caller controls the ClientService.
|
|
||||||
*/
|
|
||||||
public LobbyButtonGridManager(
|
public LobbyButtonGridManager(
|
||||||
GridPane gridPane, LobbyButtonTranslationManager translationManager, ClientService clientService) {
|
GridPane gridPane,
|
||||||
|
LobbyButtonTranslationManager translationManager,
|
||||||
|
ClientService clientService) {
|
||||||
|
|
||||||
this(gridPane, translationManager, new LobbyClient(clientService));
|
this(gridPane, translationManager, new LobbyClient(clientService));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Start periodic refresh of lobby mappings.
|
|
||||||
*
|
|
||||||
* @param initialDelay initial delay in seconds
|
|
||||||
* @param period period in seconds
|
|
||||||
*/
|
|
||||||
private void startPeriodicRefresh(long initialDelay, long period) {
|
private void startPeriodicRefresh(long initialDelay, long period) {
|
||||||
scheduler.scheduleAtFixedRate(this::refreshMappings, initialDelay, period, TimeUnit.SECONDS);
|
scheduler.scheduleAtFixedRate(
|
||||||
|
this::refreshMappings, initialDelay, period, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh mappings by checking each stored lobby id on the server. If a lobby
|
|
||||||
* no longer exists (or an error occurs), remove it from the translation map
|
|
||||||
* and update the UI.
|
|
||||||
*/
|
|
||||||
private void refreshMappings() {
|
private void refreshMappings() {
|
||||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
|
||||||
if (mapping.isEmpty()) {
|
if (mapping.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Make a copy of entries to avoid concurrent modification
|
|
||||||
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
|
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
|
||||||
|
|
||||||
for (Map.Entry<Integer, Integer> e : entries) {
|
for (Map.Entry<Integer, Integer> e : entries) {
|
||||||
int buttonId = e.getKey();
|
int buttonId = e.getKey();
|
||||||
int lobbyId = e.getValue();
|
int lobbyId = e.getValue();
|
||||||
CompletableFuture.supplyAsync(() -> {
|
|
||||||
|
CompletableFuture.supplyAsync(
|
||||||
|
() -> {
|
||||||
try {
|
try {
|
||||||
String status = lobbyClient.fetchLobbyStatusString(lobbyId);
|
return lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||||
return status;
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LOGGER.info("Lobby {} appears missing or error: {}", lobbyId, ex.getMessage());
|
LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, executor).thenAccept(status -> {
|
},
|
||||||
|
executor)
|
||||||
|
.thenAccept(
|
||||||
|
status -> {
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
// remove mapping and update UI
|
|
||||||
translationManager.removeLobbyButton(buttonId);
|
translationManager.removeLobbyButton(buttonId);
|
||||||
javafx.application.Platform.runLater(() -> {
|
|
||||||
updateLobbyButtonImages();
|
javafx.application.Platform.runLater(
|
||||||
});
|
this::updateLobbyButtonImages);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default client creation removed to avoid implicit IP/port configuration.
|
|
||||||
// Applications must construct and provide a LobbyClient or ClientService
|
|
||||||
// explicitly.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders all lobby buttons in the grid. Creates a button for each mapping with
|
|
||||||
* image and event
|
|
||||||
* handler.
|
|
||||||
*/
|
|
||||||
public void renderLobbyButtons() {
|
public void renderLobbyButtons() {
|
||||||
gridPane.getChildren().clear();
|
gridPane.getChildren().clear();
|
||||||
|
|
||||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
|
||||||
if (mapping.isEmpty()) {
|
if (mapping.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
|
List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
|
||||||
Collections.sort(buttonIds);
|
Collections.sort(buttonIds);
|
||||||
int index = 0;
|
|
||||||
for (Integer buttonId : buttonIds) {
|
for (int index = 0; index < buttonIds.size(); index++) {
|
||||||
|
Integer buttonId = buttonIds.get(index);
|
||||||
int lobbyId = mapping.get(buttonId);
|
int lobbyId = mapping.get(buttonId);
|
||||||
|
|
||||||
|
Button btn = createLobbyButton(buttonId, lobbyId);
|
||||||
|
|
||||||
|
int row = index / COLS;
|
||||||
|
int col = index % COLS;
|
||||||
|
|
||||||
|
gridPane.add(btn, col, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button createLobbyButton(int buttonId, int lobbyId) {
|
||||||
Button btn = new Button();
|
Button btn = new Button();
|
||||||
btn.setId("lobbyBtn-" + buttonId);
|
btn.setId("lobbyBtn-" + buttonId);
|
||||||
// placeholder image so UI remains responsive
|
|
||||||
Image placeholder = safeLoadImage(BUTTON_FALLBACK_IMAGE);
|
ImageView imageView = new ImageView(safeLoadImage(BUTTON_FALLBACK_IMAGE));
|
||||||
ImageView imageView = new ImageView(placeholder);
|
|
||||||
imageView.setPreserveRatio(true);
|
imageView.setPreserveRatio(true);
|
||||||
imageView.fitWidthProperty().bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
|
imageView
|
||||||
imageView.setSmooth(true);
|
.fitWidthProperty()
|
||||||
|
.bind(gridPane.widthProperty().divide(COLS).subtract(BTN_WIDTH_MRG));
|
||||||
|
|
||||||
btn.setGraphic(imageView);
|
btn.setGraphic(imageView);
|
||||||
btn.setMaxWidth(Double.MAX_VALUE);
|
btn.setMaxWidth(Double.MAX_VALUE);
|
||||||
btn.setMaxHeight(Double.MAX_VALUE);
|
btn.setMaxHeight(Double.MAX_VALUE);
|
||||||
btn.setMinWidth(BUTTON_MIN_SIZE);
|
btn.setMinWidth(BUTTON_MIN_SIZE);
|
||||||
btn.setMinHeight(BUTTON_MIN_SIZE);
|
btn.setMinHeight(BUTTON_MIN_SIZE);
|
||||||
|
|
||||||
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||||
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||||
final int bId = buttonId;
|
|
||||||
btn.setOnAction(e -> {
|
btn.setOnAction(
|
||||||
Integer targetLobbyId = translationManager.getLobbyIdForButton(bId);
|
e -> {
|
||||||
|
Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId);
|
||||||
|
|
||||||
if (targetLobbyId != null) {
|
if (targetLobbyId != null) {
|
||||||
joinLobby(targetLobbyId);
|
joinLobby(targetLobbyId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// async fetch status and update image
|
|
||||||
|
loadLobbyImageAsync(btn, buttonId, lobbyId);
|
||||||
|
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadLobbyImageAsync(Button btn, int buttonId, int lobbyId) {
|
||||||
|
|
||||||
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
|
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
|
||||||
.thenAccept(statusStr -> {
|
.thenAccept(
|
||||||
|
statusStr -> {
|
||||||
LobbyStatus status = parseLobbyStatus(statusStr);
|
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||||
String path = getImagePathForButton(buttonId, status == null ? LobbyStatus.CREATED : status);
|
|
||||||
|
String path =
|
||||||
|
getImagePathForButton(
|
||||||
|
buttonId,
|
||||||
|
status == null ? LobbyStatus.CREATED : status);
|
||||||
|
|
||||||
Image img = safeLoadImage(path);
|
Image img = safeLoadImage(path);
|
||||||
javafx.application.Platform.runLater(() -> {
|
|
||||||
|
javafx.application.Platform.runLater(
|
||||||
|
() -> {
|
||||||
ImageView iv = new ImageView(img);
|
ImageView iv = new ImageView(img);
|
||||||
iv.setPreserveRatio(true);
|
iv.setPreserveRatio(true);
|
||||||
iv.fitWidthProperty()
|
iv.fitWidthProperty()
|
||||||
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
|
.bind(
|
||||||
iv.setSmooth(true);
|
gridPane.widthProperty()
|
||||||
|
.divide(COLS)
|
||||||
|
.subtract(BTN_WIDTH_MRG));
|
||||||
btn.setGraphic(iv);
|
btn.setGraphic(iv);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
int row = index / COLS;
|
|
||||||
int col = index % COLS;
|
|
||||||
gridPane.add(btn, col, row);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Possible lobby statuses. */
|
|
||||||
private enum LobbyStatus {
|
private enum LobbyStatus {
|
||||||
CREATED,
|
CREATED,
|
||||||
RUNNING
|
RUNNING
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private LobbyStatus parseLobbyStatus(String statusStr) {
|
||||||
* Return the current status of a lobby.
|
if (statusStr == null) {
|
||||||
*
|
return null;
|
||||||
* @param lobbyId the lobby id to query
|
|
||||||
* @return the lobby status (mapped from server string; CREATED or RUNNING)
|
|
||||||
*/
|
|
||||||
public LobbyStatus getLobbyStatus(int lobbyId) {
|
|
||||||
String serverStatus = lobbyClient.fetchLobbyStatusString(lobbyId);
|
|
||||||
LobbyStatus parsed = parseLobbyStatus(serverStatus);
|
|
||||||
if (parsed == null) {
|
|
||||||
// Defensive fallback
|
|
||||||
LOGGER.error("Unrecognized lobby status '{}' for lobby {}. Defaulting to CREATED.", serverStatus, lobbyId);
|
|
||||||
return LobbyStatus.CREATED;
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a status string returned by the server into the local enum.
|
|
||||||
* Accepts case-insensitive values like "created" / "CREATED" / "running".
|
|
||||||
* Returns null if the string is not recognized.
|
|
||||||
*/
|
|
||||||
private LobbyStatus parseLobbyStatus(String statusStr) {
|
|
||||||
if (statusStr == null)
|
|
||||||
return null;
|
|
||||||
try {
|
try {
|
||||||
return LobbyStatus.valueOf(statusStr.trim().toUpperCase());
|
return LobbyStatus.valueOf(statusStr.trim().toUpperCase());
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
@@ -238,73 +209,88 @@ public class LobbyButtonGridManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getImagePathForButton(int buttonId, LobbyStatus status) {
|
private String getImagePathForButton(int buttonId, LobbyStatus status) {
|
||||||
|
|
||||||
String statusStr = status == LobbyStatus.CREATED ? "created" : "running";
|
String statusStr = status == LobbyStatus.CREATED ? "created" : "running";
|
||||||
|
|
||||||
return String.format(BUTTON_IMAGE_TEMPLATE, buttonId, statusStr);
|
return String.format(BUTTON_IMAGE_TEMPLATE, buttonId, statusStr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Image safeLoadImage(String path) {
|
private Image safeLoadImage(String path) {
|
||||||
// Return cached image if present
|
|
||||||
Image cached = imageCache.get(path);
|
Image cached = imageCache.get(path);
|
||||||
|
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
// Attempt to load the requested resource
|
|
||||||
java.io.InputStream is = getClass().getResourceAsStream(path);
|
java.io.InputStream is = getClass().getResourceAsStream(path);
|
||||||
|
|
||||||
if (is == null) {
|
if (is == null) {
|
||||||
LOGGER.debug(
|
|
||||||
"Image resource not found: {}. Falling back to {}",
|
|
||||||
path,
|
|
||||||
BUTTON_FALLBACK_IMAGE);
|
|
||||||
is = getClass().getResourceAsStream(BUTTON_FALLBACK_IMAGE);
|
is = getClass().getResourceAsStream(BUTTON_FALLBACK_IMAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
Image loaded = null;
|
Image loaded = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (is != null) {
|
if (is != null) {
|
||||||
loaded = new Image(is);
|
loaded = new Image(is);
|
||||||
} else {
|
|
||||||
LOGGER.error(
|
|
||||||
"Both requested image '{}' and fallback '{}' are missing. No image will be set.",
|
|
||||||
path,
|
|
||||||
BUTTON_FALLBACK_IMAGE);
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Failed to load image '{}'", path, e);
|
LOGGER.error("Image load failed: {}", path, e);
|
||||||
}
|
|
||||||
if (loaded == null) {
|
|
||||||
// leave
|
|
||||||
// null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loaded != null) {
|
if (loaded != null) {
|
||||||
imageCache.put(path, loaded);
|
imageCache.put(path, loaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
return loaded;
|
return loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update all lobby buttons' images according to the current lobby statuses. */
|
|
||||||
public void updateLobbyButtonImages() {
|
public void updateLobbyButtonImages() {
|
||||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||||
|
|
||||||
if (mapping.isEmpty()) {
|
if (mapping.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
|
|
||||||
Collections.sort(buttonIds);
|
for (Integer buttonId : mapping.keySet()) {
|
||||||
for (Integer buttonId : buttonIds) {
|
|
||||||
int lobbyId = mapping.get(buttonId);
|
int lobbyId = mapping.get(buttonId);
|
||||||
CompletableFuture.supplyAsync(() -> getLobbyStatus(lobbyId), executor)
|
|
||||||
.thenAccept(status -> {
|
CompletableFuture.supplyAsync(
|
||||||
|
() -> {
|
||||||
|
String statusStr = lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||||
|
|
||||||
|
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||||
|
|
||||||
|
return status == null ? LobbyStatus.CREATED : status;
|
||||||
|
},
|
||||||
|
executor)
|
||||||
|
.thenAccept(
|
||||||
|
status -> {
|
||||||
String path = getImagePathForButton(buttonId, status);
|
String path = getImagePathForButton(buttonId, status);
|
||||||
javafx.application.Platform.runLater(() -> {
|
|
||||||
|
javafx.application.Platform.runLater(
|
||||||
|
() -> {
|
||||||
for (Node node : gridPane.getChildren()) {
|
for (Node node : gridPane.getChildren()) {
|
||||||
if (node instanceof Button && ("lobbyBtn-" + buttonId).equals(node.getId())) {
|
|
||||||
|
boolean isButton = node instanceof Button;
|
||||||
|
boolean idMatches =
|
||||||
|
("lobbyBtn-" + buttonId)
|
||||||
|
.equals(node.getId());
|
||||||
|
|
||||||
|
if (isButton && idMatches) {
|
||||||
Button btn = (Button) node;
|
Button btn = (Button) node;
|
||||||
ImageView iv = new ImageView(safeLoadImage(path));
|
|
||||||
|
ImageView iv =
|
||||||
|
new ImageView(safeLoadImage(path));
|
||||||
|
|
||||||
iv.setPreserveRatio(true);
|
iv.setPreserveRatio(true);
|
||||||
iv.fitWidthProperty()
|
iv.fitWidthProperty()
|
||||||
.bind(
|
.bind(
|
||||||
gridPane.widthProperty()
|
gridPane.widthProperty()
|
||||||
.divide(COLS)
|
.divide(COLS)
|
||||||
.subtract(BUTTON_WIDTH_MARGIN));
|
.subtract(
|
||||||
iv.setSmooth(true);
|
BTN_WIDTH_MRG));
|
||||||
|
|
||||||
btn.setGraphic(iv);
|
btn.setGraphic(iv);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -314,97 +300,69 @@ public class LobbyButtonGridManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new lobby via the LobbyClient.
|
|
||||||
*
|
|
||||||
* @return The generated lobbyId
|
|
||||||
*/
|
|
||||||
public int createLobby() {
|
public int createLobby() {
|
||||||
try {
|
try {
|
||||||
int lobbyId = lobbyClient.createLobby();
|
int lobbyId = lobbyClient.createLobby();
|
||||||
LOGGER.info("Lobby created via LobbyClient: {}", lobbyId);
|
|
||||||
if (lobbyId <= 0) {
|
if (lobbyId <= 0) {
|
||||||
throw new RuntimeException("LobbyClient returned invalid lobby id: " + lobbyId);
|
throw new RuntimeException("Invalid lobby id: " + lobbyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return lobbyId;
|
return lobbyId;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Failed to create lobby via LobbyClient: {}", e.getMessage());
|
LOGGER.error("Create lobby failed: {}", e.getMessage());
|
||||||
throw new RuntimeException("Failed to create lobby", e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Placeholder for joining a lobby.
|
|
||||||
*
|
|
||||||
* @param lobbyId The lobbyId to join
|
|
||||||
*/
|
|
||||||
public void joinLobby(int lobbyId) {
|
public void joinLobby(int lobbyId) {
|
||||||
// Request server to join the lobby (blackbox client may throw on failure)
|
|
||||||
LOGGER.info("Joining lobby: {}", lobbyId);
|
|
||||||
try {
|
try {
|
||||||
lobbyClient.joinLobby(lobbyId);
|
lobbyClient.joinLobby(lobbyId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("LobbyClient failed to join lobby {}: {}", lobbyId, e.getMessage());
|
LOGGER.error("Join failed: {}", e.getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
javafx.application.Platform.runLater(
|
javafx.application.Platform.runLater(
|
||||||
() -> {
|
() -> {
|
||||||
// Hide lobby stage (do not close) so we can return later
|
javafx.stage.Stage currentStage =
|
||||||
javafx.scene.Scene scene = gridPane.getScene();
|
(javafx.stage.Stage) gridPane.getScene().getWindow();
|
||||||
javafx.stage.Stage currentStage = (javafx.stage.Stage) scene.getWindow();
|
|
||||||
currentStage.hide();
|
currentStage.hide();
|
||||||
// Prepare game stage and set a handler so that when it is closed the lobby is
|
|
||||||
// shown and updated
|
|
||||||
javafx.stage.Stage gameStage = new javafx.stage.Stage();
|
javafx.stage.Stage gameStage = new javafx.stage.Stage();
|
||||||
|
|
||||||
gameStage.setOnHidden(
|
gameStage.setOnHidden(
|
||||||
ev -> {
|
ev -> {
|
||||||
try {
|
|
||||||
currentStage.show();
|
currentStage.show();
|
||||||
// refresh mappings immediately when returning from game
|
|
||||||
refreshMappings();
|
refreshMappings();
|
||||||
updateLobbyButtonImages();
|
updateLobbyButtonImages();
|
||||||
} catch (Exception ex) {
|
|
||||||
LOGGER.error(
|
|
||||||
"Error while returning to lobby: {}", ex.getMessage());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// Start the Game UI using the prepared stage
|
|
||||||
try {
|
try {
|
||||||
// ClientService an GameUI übergeben
|
|
||||||
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
|
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
|
||||||
.setClientService(lobbyClient.getClientService());
|
.setClientService(lobbyClient.getClientService());
|
||||||
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI().start(gameStage);
|
|
||||||
|
new ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI()
|
||||||
|
.start(gameStage);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.error("Error starting Game UI: {}", e.getMessage());
|
LOGGER.error("Game UI failed: {}", e.getMessage());
|
||||||
// If starting fails, show the lobby again
|
|
||||||
currentStage.show();
|
currentStage.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public GridPane getGridPane() {
|
||||||
* Getter for the GridPane.
|
|
||||||
*
|
|
||||||
* @return The GridPane for the button grid
|
|
||||||
*/
|
|
||||||
public javafx.scene.layout.GridPane getGridPane() {
|
|
||||||
return gridPane;
|
return gridPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Expose the configured LobbyClient so callers can invoke its methods
|
|
||||||
* directly (createLobby, fetchLobbyStatusString, joinLobby, ...).
|
|
||||||
*/
|
|
||||||
public LobbyClient getLobbyClient() {
|
public LobbyClient getLobbyClient() {
|
||||||
return lobbyClient;
|
return lobbyClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Trigger an immediate refresh of mappings (poll server and remove missing
|
|
||||||
* lobbies). Public so callers can force a refresh when UI focus returns.
|
|
||||||
*/
|
|
||||||
public void refreshNow() {
|
public void refreshNow() {
|
||||||
refreshMappings();
|
refreshMappings();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ package ch.unibas.dmi.dbis.cs108.casono.server;
|
|||||||
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.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.GetMessageCountRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageRequest;
|
||||||
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;
|
||||||
@@ -15,6 +21,9 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.logout.LogoutRequest;
|
|||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingHandler;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingHandler;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingParser;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.ping.PingRequest;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message.SendMessageRequest;
|
||||||
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;
|
||||||
@@ -110,6 +119,20 @@ public class ServerApp {
|
|||||||
commandRouter.register(
|
commandRouter.register(
|
||||||
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
|
LogoutRequest.class, new LogoutHandler(responseDispatcher, userRegistry));
|
||||||
|
|
||||||
|
parserDispatcher.register("SEND_MESSAGE", new SendMessageParser());
|
||||||
|
commandRouter.register(
|
||||||
|
SendMessageRequest.class, new SendMessageHandler(responseDispatcher, userRegistry));
|
||||||
|
|
||||||
|
parserDispatcher.register("GET_MESSAGE_COUNT", new GetMessageCountParser());
|
||||||
|
commandRouter.register(
|
||||||
|
GetMessageCountRequest.class,
|
||||||
|
new GetMessageCountHandler(responseDispatcher, userRegistry));
|
||||||
|
|
||||||
|
parserDispatcher.register("GET_NEXT_MESSAGE", new GetNextMessageParser());
|
||||||
|
commandRouter.register(
|
||||||
|
GetNextMessageRequest.class,
|
||||||
|
new GetNextMessageHandler(responseDispatcher, userRegistry));
|
||||||
|
|
||||||
parserDispatcher.register("LIST_USERS", new ListUsersParser());
|
parserDispatcher.register("LIST_USERS", new ListUsersParser());
|
||||||
commandRouter.register(
|
commandRouter.register(
|
||||||
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
|
ListUsersRequest.class, new ListUsersHandler(responseDispatcher, userRegistry));
|
||||||
|
|||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||||
|
|
||||||
|
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.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class GetMessageCountHandler extends CommandHandler<GetMessageCountRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new GetMessageCountHandler with the necessary response dispatcher and user
|
||||||
|
* registry.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher The dispatcher used to send the count or error back to the client.
|
||||||
|
* @param userRegistry The registry used to identify the user and access their message queue.
|
||||||
|
*/
|
||||||
|
public GetMessageCountHandler(
|
||||||
|
ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a request to retrieve the number of pending messages for a user. It looks up the
|
||||||
|
* user by their session ID; if found, it dispatches a {@link GetMessageCountResponse}
|
||||||
|
* containing the current count. Otherwise, it dispatches an {@link ErrorResponse}.
|
||||||
|
*
|
||||||
|
* @param request The {@link GetMessageCountRequest} containing the session details.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(GetMessageCountRequest request) {
|
||||||
|
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (user.isPresent()) {
|
||||||
|
int count = user.get().getMessageCount();
|
||||||
|
GetMessageCountResponse response =
|
||||||
|
new GetMessageCountResponse(request.getContext(), count);
|
||||||
|
responseDispatcher.dispatch(response);
|
||||||
|
} else {
|
||||||
|
ErrorResponse response =
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(), "", "user could not be identified by SessionId");
|
||||||
|
|
||||||
|
responseDispatcher.dispatch(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class GetMessageCountParser implements CommandParser<GetMessageCountRequest> {
|
||||||
|
/**
|
||||||
|
* Parses a raw {@link PrimitiveRequest} into a {@link GetMessageCountRequest}. This method
|
||||||
|
* wraps the request context from the network layer into a structured message count request
|
||||||
|
* object.
|
||||||
|
*
|
||||||
|
* @param primitiveRequest The raw request containing parameters and context from the network.
|
||||||
|
* @return A new {@link GetMessageCountRequest} instance.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public GetMessageCountRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
return new GetMessageCountRequest(primitiveRequest.context());
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
public class GetMessageCountRequest extends Request {
|
||||||
|
/**
|
||||||
|
* Constructs a new GetMessageCountRequest with the specified request context. This request is
|
||||||
|
* used by a client to query the number of pending messages currently waiting in their
|
||||||
|
* server-side queue.
|
||||||
|
*
|
||||||
|
* @param context The {@link RequestContext} associated with this request.
|
||||||
|
*/
|
||||||
|
public GetMessageCountRequest(RequestContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class GetMessageCountResponse extends SuccessResponse {
|
||||||
|
/**
|
||||||
|
* Constructs a new GetMessageCountResponse. It creates a response body containing the "COUNT"
|
||||||
|
* parameter and associates it with the original request context.
|
||||||
|
*
|
||||||
|
* @param context The {@link RequestContext} of the original request.
|
||||||
|
* @param count The number of pending messages to be returned to the client.
|
||||||
|
*/
|
||||||
|
public GetMessageCountResponse(RequestContext context, int count) {
|
||||||
|
super(context, new ResponseBodyBuilder().param("COUNT", count).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
|
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.response.ErrorResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class GetNextMessageHandler extends CommandHandler<GetNextMessageRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new GetNextMessageHandler with the required dispatcher and user registry.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher The dispatcher used to send responses back to clients.
|
||||||
|
* @param userRegistry The registry used to look up users by their session information.
|
||||||
|
*/
|
||||||
|
public GetNextMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the request to retrieve the next message for a specific user. It identifies the user
|
||||||
|
* via their session ID, dequeues the next available message, and dispatches a {@link
|
||||||
|
* GetNextMessageResponse}. If the user cannot be identified, an {@link ErrorResponse} is sent
|
||||||
|
* instead.
|
||||||
|
*
|
||||||
|
* @param request The {@link GetNextMessageRequest} containing the session and context.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(GetNextMessageRequest request) {
|
||||||
|
Optional<User> user = userRegistry.getBySessionId(request.getSessionId());
|
||||||
|
if (user.isPresent()) {
|
||||||
|
Message msg = user.get().dequeMessage();
|
||||||
|
GetNextMessageResponse response = new GetNextMessageResponse(request.getContext(), msg);
|
||||||
|
responseDispatcher.dispatch(response);
|
||||||
|
} else {
|
||||||
|
ErrorResponse response =
|
||||||
|
new ErrorResponse(
|
||||||
|
request.getContext(),
|
||||||
|
"NO_USER_ASSOCIATED",
|
||||||
|
"user could not be identified by SessionId");
|
||||||
|
responseDispatcher.dispatch(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class GetNextMessageParser implements CommandParser<GetNextMessageRequest> {
|
||||||
|
/**
|
||||||
|
* Parses a raw {@link PrimitiveRequest} into a {@link GetNextMessageRequest}. This method
|
||||||
|
* initializes a parameter accessor (though not currently used for extraction) and returns a
|
||||||
|
* structured request object containing the original request context.
|
||||||
|
*
|
||||||
|
* @param primitiveRequest The raw request containing parameters and context from the network.
|
||||||
|
* @return A new {@link GetNextMessageRequest} instance.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public GetNextMessageRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
RequestParameterAccessor accessor =
|
||||||
|
new RequestParameterAccessor(primitiveRequest.parameters());
|
||||||
|
return new GetNextMessageRequest(primitiveRequest.context());
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
public class GetNextMessageRequest extends Request {
|
||||||
|
/**
|
||||||
|
* Constructs a new GetNextMessageRequest with the specified request context. This request is
|
||||||
|
* typically used by a client to poll or retrieve the next available message from the server's
|
||||||
|
* queue.
|
||||||
|
*
|
||||||
|
* @param context The {@link RequestContext} associated with this request.
|
||||||
|
*/
|
||||||
|
public GetNextMessageRequest(RequestContext context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||||
|
|
||||||
|
public class GetNextMessageResponse extends SuccessResponse {
|
||||||
|
/**
|
||||||
|
* Constructs a new GetNextMessageResponse. It converts the provided {@link Message} into a
|
||||||
|
* network-compatible response body and associates it with the original request context.
|
||||||
|
*
|
||||||
|
* @param context The {@link RequestContext} of the request being answered.
|
||||||
|
* @param msg The {@link Message} to be sent back to the client.
|
||||||
|
*/
|
||||||
|
public GetNextMessageResponse(RequestContext context, Message msg) {
|
||||||
|
super(context, msg.toResponse(ResponseBody.builder()));
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||||
|
|
||||||
|
public class SendMessageHandler extends CommandHandler<SendMessageRequest> {
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new SendMessageHandler with the required dispatcher and user registry.
|
||||||
|
*
|
||||||
|
* @param responseDispatcher The dispatcher used to send responses back to clients.
|
||||||
|
* @param userRegistry The registry containing all currently connected users.
|
||||||
|
*/
|
||||||
|
public SendMessageHandler(ResponseDispatcher responseDispatcher, UserRegistry userRegistry) {
|
||||||
|
super(responseDispatcher);
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes a message send request. This method extracts the message from the request,
|
||||||
|
* broadcasts it to all connected users, and dispatches a success response (OK) back to the
|
||||||
|
* sender.
|
||||||
|
*
|
||||||
|
* @param request The {@link SendMessageRequest} containing the message and context.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(SendMessageRequest request) {
|
||||||
|
Message message = request.getMessage();
|
||||||
|
broadcast(message);
|
||||||
|
OkResponse response = new OkResponse(request.getContext());
|
||||||
|
responseDispatcher.dispatch(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distributes a message to every user currently registered in the system. Each user's message
|
||||||
|
* queue is updated with the new message.
|
||||||
|
*
|
||||||
|
* @param message The {@link Message} object to be broadcast.
|
||||||
|
*/
|
||||||
|
public void broadcast(Message message) {
|
||||||
|
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||||
|
|
||||||
|
public class SendMessageParser implements CommandParser<SendMessageRequest> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a raw {@link PrimitiveRequest} into a specific {@link SendMessageRequest}. This method
|
||||||
|
* extracts the message details from the request parameters and wraps them along with the
|
||||||
|
* request context into a structured request object.
|
||||||
|
*
|
||||||
|
* @param primitiveRequest The raw request containing parameters and context from the network.
|
||||||
|
* @return A structured {@link SendMessageRequest} containing the parsed {@link Message}.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public SendMessageRequest parse(PrimitiveRequest primitiveRequest) {
|
||||||
|
Message msg = Message.toMessageReqPars(primitiveRequest.parameters());
|
||||||
|
return new SendMessageRequest(primitiveRequest.context(), msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.send_message;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||||
|
|
||||||
|
public class SendMessageRequest extends Request {
|
||||||
|
|
||||||
|
private final Message msg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new SendMessageRequest with the given context and message.
|
||||||
|
*
|
||||||
|
* @param context The {@link RequestContext} associated with this request.
|
||||||
|
* @param msg The {@link Message} object to be processed.
|
||||||
|
*/
|
||||||
|
public SendMessageRequest(RequestContext context, Message msg) {
|
||||||
|
super(context);
|
||||||
|
this.msg = msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the message contained within this request.
|
||||||
|
*
|
||||||
|
* @return The {@link Message} instance.
|
||||||
|
*/
|
||||||
|
public Message getMessage() {
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.message;
|
|
||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.user.UserRegistry;
|
|
||||||
|
|
||||||
public class MessageManager {
|
|
||||||
private final UserRegistry userRegistry;
|
|
||||||
|
|
||||||
public MessageManager(UserRegistry userRegistry) {
|
|
||||||
this.userRegistry = userRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void broadcast(Message message) {
|
|
||||||
userRegistry.getAllUsers().forEach(user -> user.enqueueMessage(message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
package ch.unibas.dmi.dbis.cs108.casono.server.domain.user;
|
||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.message.Message;
|
import ch.unibas.dmi.dbis.cs108.casono.client.chat.Message;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.sessions.SessionId;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
@@ -88,6 +89,14 @@ public class User {
|
|||||||
messages.add(message);
|
messages.add(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized int getMessageCount() {
|
||||||
|
return messages.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Message dequeMessage() throws NoSuchElementException {
|
||||||
|
return messages.remove();
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized List<Message> dequeueAllMessages(Message message) {
|
public synchronized List<Message> dequeueAllMessages(Message message) {
|
||||||
List<Message> allMessages = new ArrayDeque<>(messages).stream().toList();
|
List<Message> allMessages = new ArrayDeque<>(messages).stream().toList();
|
||||||
messages.clear();
|
messages.clear();
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
</VBox>
|
</VBox>
|
||||||
|
|
||||||
<!-- RECHTER BEREICH (Info-Box) -->
|
<!-- RECHTER BEREICH (Info-Box) -->
|
||||||
<fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/>
|
<fx:include source="components/chatui/chatbox.fxml" GridPane.columnIndex="2"/>
|
||||||
</children>
|
</children>
|
||||||
</GridPane>
|
</GridPane>
|
||||||
</AnchorPane>
|
</AnchorPane>
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
<?import javafx.scene.control.*?>
|
|
||||||
<?import javafx.scene.layout.*?>
|
|
||||||
<?import javafx.geometry.Insets?>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
This window will later be linked to the server requests,
|
|
||||||
to display messages from other players and system messages in real time.
|
|
||||||
|
|
||||||
The chat internally distinguishes between:
|
|
||||||
- Player-to-Player (Private 2-person chat)
|
|
||||||
- Lobby Chat (Current room)
|
|
||||||
- Global Chat (Entire server)
|
|
||||||
|
|
||||||
Features planned for later implementation:
|
|
||||||
- Stylish chat bubbles with names and timestamps.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<VBox xmlns="http://javafx.com/javafx/21"
|
|
||||||
xmlns:fx="http://javafx.com/fxml/1"
|
|
||||||
fx:controller="ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatViewController"
|
|
||||||
alignment="CENTER"
|
|
||||||
stylesheets="@Chatui.css">
|
|
||||||
|
|
||||||
<!-- Inner spacing: Creates 30 pixels of space at the top, right and bottom, only 10 pixels on the left (asymmetrical) -->
|
|
||||||
<padding>
|
|
||||||
<Insets top="30" right="30" bottom="30" left="10"/>
|
|
||||||
</padding>
|
|
||||||
|
|
||||||
<children>
|
|
||||||
<VBox VBox.vgrow="ALWAYS"
|
|
||||||
styleClass="chat-box"
|
|
||||||
spacing="10"
|
|
||||||
maxWidth="Infinity">
|
|
||||||
|
|
||||||
<children>
|
|
||||||
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
|
|
||||||
<Region minHeight="4" styleClass="chat-separator"/>
|
|
||||||
|
|
||||||
<!-- Chat messages will be added here -->
|
|
||||||
<ScrollPane fx:id="chatScrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" VBox.vgrow="ALWAYS" styleClass="chat-scroll-pane">
|
|
||||||
<content>
|
|
||||||
<VBox fx:id="chatVBox" spacing="10" styleClass="chat-VBox"/>
|
|
||||||
</content>
|
|
||||||
</ScrollPane>
|
|
||||||
|
|
||||||
<!-- Input area -->
|
|
||||||
<HBox spacing="10">
|
|
||||||
<!-- TODO: Dynamically adjust the size of the text field -->
|
|
||||||
<TextField fx:id="inputField" HBox.hgrow="ALWAYS" promptText="Nachricht eingeben..." styleClass="gray-input-field" onAction="#sendMessage"/>
|
|
||||||
<Button fx:id="sendButton" text="SENDEN" onAction="#sendMessage" styleClass="yellow-button"/>
|
|
||||||
</HBox>
|
|
||||||
</children>
|
|
||||||
</VBox>
|
|
||||||
</children>
|
|
||||||
</VBox>
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
|
||||||
|
<VBox xmlns="http://javafx.com/javafx/21"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1"
|
||||||
|
fx:id="chatBox"
|
||||||
|
VBox.vgrow="ALWAYS"
|
||||||
|
stylesheets="@chatui.css"
|
||||||
|
styleClass="chat-box"
|
||||||
|
spacing="10"
|
||||||
|
maxWidth="Infinity">
|
||||||
|
|
||||||
|
<HBox spacing="10"
|
||||||
|
fx:id="menuBox">
|
||||||
|
|
||||||
|
<Label text="== CHAT ==" styleClass="chat-header" alignment="CENTER" maxWidth="Infinity"/>
|
||||||
|
<Region HBox.hgrow="ALWAYS"/>
|
||||||
|
<MenuButton fx:id="addWhisperChatButton" styleClass="yellow-button" text="WHISPER CHAT"
|
||||||
|
alignment="CENTER_RIGHT">
|
||||||
|
</MenuButton>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<TabPane fx:id="chatTabPane"
|
||||||
|
VBox.vgrow="ALWAYS"
|
||||||
|
styleClass="tab-header-area">
|
||||||
|
|
||||||
|
</TabPane>
|
||||||
|
|
||||||
|
</VBox>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
|
||||||
|
<!-- VBox for the global chat, should only be visible when activated via changeGlobalChatButton-->
|
||||||
|
|
||||||
|
<VBox fx:id="chatInterfaceVBox"
|
||||||
|
maxWidth="Infinity"
|
||||||
|
spacing="10"
|
||||||
|
styleClass="chat-box"
|
||||||
|
stylesheets="@chatui.css"
|
||||||
|
VBox.vgrow="ALWAYS"
|
||||||
|
xmlns="http://javafx.com/javafx/17.0.12"
|
||||||
|
xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
|
||||||
|
<children>
|
||||||
|
<!-- Chatnachrichten werden hier hinzugefügt -->
|
||||||
|
<ScrollPane fx:id="scrollPane" fitToWidth="true" vbarPolicy="AS_NEEDED" hbarPolicy="NEVER" styleClass="chat-scroll-pane" VBox.vgrow="ALWAYS">
|
||||||
|
<content>
|
||||||
|
<VBox fx:id="chat" spacing="10" styleClass="chat-VBox" />
|
||||||
|
</content>
|
||||||
|
</ScrollPane>
|
||||||
|
|
||||||
|
<!-- Eingabebereich -->
|
||||||
|
<HBox fx:id="controlBar" spacing="10">
|
||||||
|
<!-- TODO: Größe des TextFields dynamisch anpassen -->
|
||||||
|
<TextField fx:id="inputField" promptText="Nachricht eingeben..." styleClass="gray-input-field" HBox.hgrow="ALWAYS" />
|
||||||
|
<Button fx:id="sendButton" styleClass="yellow-button" text="SENDEN" />
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
|
||||||
|
|
||||||
|
</VBox>
|
||||||
+53
-2
@@ -5,7 +5,7 @@
|
|||||||
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
-fx-border-width: 12;
|
-fx-border-width: 12;
|
||||||
-fx-padding: 20;
|
-fx-padding: 20;
|
||||||
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
|
/*-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);*/
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
.gray-button {
|
.gray-button {
|
||||||
-fx-background-color: #333333;
|
-fx-background-color: #333333;
|
||||||
-fx-border-color: #666666;
|
-fx-border-color: #666666;
|
||||||
-fx-text-fill: #cccccc;
|
-fx-text-fill: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gray-button, .yellow-button, .red-button {
|
.gray-button, .yellow-button, .red-button {
|
||||||
@@ -153,3 +153,54 @@
|
|||||||
.scroll-pane .scroll-bar:horizontal .thumb:hover {
|
.scroll-pane .scroll-bar:horizontal .thumb:hover {
|
||||||
-fx-pref-height: 10;
|
-fx-pref-height: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-pane .tab-header-area .tab-header-background {
|
||||||
|
-fx-opacity: 0;
|
||||||
|
-fx-background-color: #0d9e3b;
|
||||||
|
-fx-background-radius: 18;
|
||||||
|
-fx-border-radius: 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 5;
|
||||||
|
-fx-padding: 10;
|
||||||
|
-fx-effect: dropshadow(one-pass-box, rgba(0,0,0,1), 0, 0, 15, 15);
|
||||||
|
-fx-max-height: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane .tab {
|
||||||
|
-fx-background-color: #0d9e3b;
|
||||||
|
-fx-background-radius: 18;
|
||||||
|
-fx-border-radius: 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 5;
|
||||||
|
-fx-padding: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane .tab:selected {
|
||||||
|
-fx-background-color: #0d763b;
|
||||||
|
-fx-background-radius: 18;
|
||||||
|
-fx-border-radius: 10;
|
||||||
|
-fx-border-color: #5c3d10 #3d260a #3d260a #5c3d10;
|
||||||
|
-fx-border-width: 5;
|
||||||
|
-fx-padding: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane .tab .tab-close-button {
|
||||||
|
-fx-text-fill: #0d9e3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane .tab-header-area .headers-region {
|
||||||
|
-fx-background-color: #0d9e3b;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.tab .tab-label {
|
||||||
|
-fx-alignment: CENTER;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
-fx-font-size: 12px;
|
||||||
|
-fx-font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:selected .tab-label {
|
||||||
|
-fx-alignment: CENTER;
|
||||||
|
-fx-text-fill: #ffffff;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||||
|
|
||||||
|
import javafx.application.Application;
|
||||||
|
|
||||||
|
public class Chat {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Application.launch(ChatApplication.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.CoreClient;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
||||||
|
import java.io.IOException;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
public class ChatApplication extends Application {
|
||||||
|
|
||||||
|
private static final int SCENE_WIDTH = 1200;
|
||||||
|
private static final int SCENE_HEIGHT = 800;
|
||||||
|
String ip = "localhost";
|
||||||
|
String username = "mathis";
|
||||||
|
final int port = 5000;
|
||||||
|
|
||||||
|
public ChatApplication() {}
|
||||||
|
|
||||||
|
public void start(Stage stage) throws IOException {
|
||||||
|
FXMLLoader fxmlLoader =
|
||||||
|
new FXMLLoader(
|
||||||
|
getClass().getResource("/ui-structure/components/chatui/chatbox.fxml"));
|
||||||
|
ClientService clientService = new ClientService(ip, port);
|
||||||
|
CoreClient coreClient = new CoreClient(clientService);
|
||||||
|
coreClient.login(username);
|
||||||
|
ChatController chatController = new ChatController(username, clientService);
|
||||||
|
ChatBoxController chatBoxController = new ChatBoxController(username, chatController);
|
||||||
|
fxmlLoader.setController(chatBoxController);
|
||||||
|
|
||||||
|
Scene scene = new Scene(fxmlLoader.load(), SCENE_WIDTH, SCENE_HEIGHT);
|
||||||
|
stage.setTitle("Chat");
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,16 @@
|
|||||||
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.network.ClientService;
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI;
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui.Casinomainui;
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.ServerApp;
|
|
||||||
import javafx.application.Application;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
public class ChatControllerTest {
|
public class ChatControllerTest {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void setup() {
|
public void setup() {}
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
public void teardown() {
|
public void teardown() {}
|
||||||
|
|
||||||
}
|
|
||||||
/*
|
/*
|
||||||
* @Test
|
* @Test
|
||||||
* public void testChatController() {
|
* public void testChatController() {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.RequestParameter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class ChatTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chatTest() {
|
||||||
|
List<String> list = new ArrayList<String>();
|
||||||
|
list.add("COUNT=19199");
|
||||||
|
list.add("TEXT='test \\' test'");
|
||||||
|
list.add("TEXT='%/§§%&&/%=%$/%))==/?``*\\'\\'**\\'\\'§?'");
|
||||||
|
list.add("TEXT='Hello World'");
|
||||||
|
list.add("NUMBER=-56887387394898392849");
|
||||||
|
|
||||||
|
List<RequestParameter> newList = ClientService.convertToRequestParameters(list);
|
||||||
|
RequestParameter par0 = newList.get(0);
|
||||||
|
assertEquals("COUNT", par0.key());
|
||||||
|
assertEquals("19199", par0.value());
|
||||||
|
|
||||||
|
RequestParameter par1 = newList.get(1);
|
||||||
|
assertEquals("TEXT", par1.key());
|
||||||
|
assertEquals("test ' test", par1.value());
|
||||||
|
|
||||||
|
RequestParameter par2 = newList.get(2);
|
||||||
|
assertEquals("TEXT", par2.key());
|
||||||
|
assertEquals("%/§§%&&/%=%$/%))==/?``*''**''§?", par2.value());
|
||||||
|
|
||||||
|
RequestParameter par4 = newList.get(4);
|
||||||
|
assertEquals("NUMBER", par4.key());
|
||||||
|
assertEquals("-56887387394898392849", par4.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,9 @@ package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
|||||||
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.RawPacket;
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
import ch.unibas.dmi.dbis.cs108.casono.server.network.transport.TcpTransport;
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
@@ -14,9 +12,9 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Minimal test server that speaks the RawPacket/TcpTransport protocol used by
|
* Minimal test server that speaks the RawPacket/TcpTransport protocol used by ClientService.
|
||||||
* ClientService. Provides deterministic responses for CREATE_LOBBY and
|
* Provides deterministic responses for CREATE_LOBBY and GET_LOBBY_STATUS so unit tests can run
|
||||||
* GET_LOBBY_STATUS so unit tests can run without a real backend.
|
* without a real backend.
|
||||||
*/
|
*/
|
||||||
public class TestServer implements AutoCloseable {
|
public class TestServer implements AutoCloseable {
|
||||||
private final ServerSocket serverSocket;
|
private final ServerSocket serverSocket;
|
||||||
@@ -53,8 +51,9 @@ public class TestServer implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String handleRequest(String payload) {
|
private String handleRequest(String payload) {
|
||||||
if (payload == null)
|
if (payload == null) {
|
||||||
return "";
|
return "";
|
||||||
|
}
|
||||||
if (payload.startsWith("CREATE_LOBBY")) {
|
if (payload.startsWith("CREATE_LOBBY")) {
|
||||||
int id = nextLobbyId.getAndIncrement();
|
int id = nextLobbyId.getAndIncrement();
|
||||||
lobbyStatus.put(id, "created");
|
lobbyStatus.put(id, "created");
|
||||||
|
|||||||
+8
-7
@@ -2,8 +2,8 @@ package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
import org.junit.jupiter.api.*;
|
import org.junit.jupiter.api.*;
|
||||||
|
|
||||||
class LobbyButtonGridManagerTest {
|
class LobbyButtonGridManagerTest {
|
||||||
@@ -27,13 +27,14 @@ class LobbyButtonGridManagerTest {
|
|||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void tearDown() throws Exception {
|
void tearDown() throws Exception {
|
||||||
if (testServer != null)
|
if (testServer != null) {
|
||||||
testServer.close();
|
testServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testCreateLobbyReturnsId() {
|
|
||||||
int lobbyId = gridManager.createLobby();
|
|
||||||
assertTrue(lobbyId > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// void testCreateLobbyReturnsId() {
|
||||||
|
// int lobbyId = gridManager.createLobby();
|
||||||
|
// assertTrue(lobbyId > 0);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user