Merge branch 'main' into feat/game-ui
# Conflicts: # src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/network/GameClient.java # src/main/java/ch/unibas/dmi/dbis/cs108/casono/client/ui/gameui/CasinoGameUI.java # src/main/resources/ui-structure/Casinogameui.fxml
This commit is contained in:
+1
-1
@@ -32,7 +32,7 @@ dependencies {
|
||||
// Source: https://mvnrepository.com/artifact/org.apache.logging.log4j
|
||||
implementation("org.apache.logging.log4j:log4j-api: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
|
||||
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
|
||||
```
|
||||
@@ -341,4 +341,116 @@ LIST_USERS
|
||||
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.port", Integer.toString(port));
|
||||
// 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.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.ui.chatui.ChatBoxController;
|
||||
import java.util.LinkedHashMap;
|
||||
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
|
||||
* or from the ChatViewController to the server
|
||||
* responsible for the transferring of messages from the server to the ChatModel or from the
|
||||
* ChatViewController to the server
|
||||
*/
|
||||
public class ChatController {
|
||||
|
||||
private final String username;
|
||||
private final ClientService clientService;
|
||||
|
||||
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) {
|
||||
this.username = username;
|
||||
this.clientService = clientService;
|
||||
this.chatClient = new ChatClient(this.clientService);
|
||||
}
|
||||
|
||||
public void createChat(int game_id) {
|
||||
chatModel = new ChatModel(ChatModel.ChatType.GLOBAL, username);
|
||||
this.game_id = game_id;
|
||||
}
|
||||
|
||||
public ChatModel getChatModel() {
|
||||
return chatModel;
|
||||
chatClient = new ChatClient(clientService);
|
||||
chatModelMap = new LinkedHashMap<>();
|
||||
this.chatBoxController = new ChatBoxController(username, this);
|
||||
this.timer = new Timer();
|
||||
timer.schedule(
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
receiveMessage();
|
||||
}
|
||||
},
|
||||
0,
|
||||
REFRESH_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
Message message = new Message(Message.MessageType.GLOBAL, 0, username, null, msg);
|
||||
onSendToNetwork(message);
|
||||
public void setLobbyChat(int lobbyId) {
|
||||
this.lobbyId = lobbyId;
|
||||
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
|
||||
*/
|
||||
public Boolean receiveMessage() {
|
||||
/** method to get all messages from the server */
|
||||
public void receiveMessage() {
|
||||
List<Message> newMessages = chatClient.getMessages();
|
||||
if (!newMessages.isEmpty()) {
|
||||
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
|
||||
*
|
||||
* @param message
|
||||
*/
|
||||
public void onSendToNetwork(Message message) {
|
||||
chatClient.sendMessage(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,67 +1,85 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.chat;
|
||||
|
||||
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
|
||||
*
|
||||
* Holds the current state of a chat
|
||||
* <p>Holds the current state of a chat
|
||||
*/
|
||||
|
||||
public class ChatModel {
|
||||
|
||||
private ArrayList<Consumer<Message>> listeners = new ArrayList<>();
|
||||
|
||||
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 {
|
||||
GLOBAL,
|
||||
LOBBY,
|
||||
WHISPER
|
||||
}
|
||||
private final IntegerProperty count;
|
||||
|
||||
public int lobbyId;
|
||||
|
||||
/**
|
||||
* Creates a new ChatModel, given a username of the client
|
||||
* @param chattype
|
||||
* @param username
|
||||
* Constructs a new ChatModel for a specific chat type.
|
||||
*
|
||||
* @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) {
|
||||
public ChatModel(ChatType chattype, String username, int lobbyId, String target) {
|
||||
this.messages = new ArrayList<Message>();
|
||||
this.chattype = chattype;
|
||||
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() {
|
||||
count--;
|
||||
Message msg = messages.getLast();
|
||||
return String.format("[%s] %s: %s", msg.timestamp, msg.user, msg.getMessage());
|
||||
public ChatType getChattype() {
|
||||
return chattype;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new message
|
||||
* method used by the ChatController
|
||||
* @param msg
|
||||
* Adds a new message to the history and notifies all registered listeners. This method is
|
||||
* synchronized to ensure thread safety when updating the message list.
|
||||
*
|
||||
* @param msg The {@link Message} to be added.
|
||||
*/
|
||||
public synchronized void addMessage(Message 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() {
|
||||
for (int i = 0; i < this.messages.size(); i++) {
|
||||
Message msg = this.messages.get(i);
|
||||
}
|
||||
public void addListener(Consumer<Message> listener) {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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.format.DateTimeFormatter;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Message Object for internal handling of Chat-Messages
|
||||
* TODO: Should be used on both sides of the network
|
||||
*/
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
/** Message Object for internal handling of Chat-Messages */
|
||||
public class Message {
|
||||
private final MessageType type;
|
||||
private final ChatType type;
|
||||
private final String message;
|
||||
public String user;
|
||||
public String sender;
|
||||
public String timestamp;
|
||||
public int game_id = 0;
|
||||
public int lobbyId = 0;
|
||||
public String target = null;
|
||||
public enum MessageType {
|
||||
GLOBAL, LOBBY, WHISPER
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor for creating Messages with all information given
|
||||
* @param type - Either global, local or whisper
|
||||
* @param game_id - lobby id, or null, if the type is global
|
||||
* @param user - username
|
||||
* @param target - username of the target user, for whisper chat
|
||||
* @param message
|
||||
* Constructs a Message with a provided timestamp. Typically used when reconstructing messages
|
||||
* received from the server. Used for Messages in the Lobby Chat.
|
||||
*
|
||||
* @param lobbyId The ID of the lobby, or -1 if not applicable.
|
||||
* @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(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.game_id = game_id;
|
||||
this.user = user;
|
||||
this.lobbyId = -1;
|
||||
this.sender = sender;
|
||||
this.target = target;
|
||||
this.timestamp = timestamp;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for creating the Messages of the current user, using this client -> time of writing is being recorded
|
||||
* @param type - Either global, local or whisper
|
||||
* @param game_id - lobby id, or null, if the type is global
|
||||
* @param user - username
|
||||
* @param target - username of the target user, for whisper chat
|
||||
* @param message
|
||||
* Constructs a new Message for the current user. Automatically generates a timestamp based on
|
||||
* the local system time ("HH:mm").
|
||||
*
|
||||
* @param type The chat category (e.g., GLOBAL, LOBBY, or WHISPER).
|
||||
* @param lobbyId The ID of the lobby, or -1 if not applicable.
|
||||
* @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(MessageType type, int game_id, String user, String target, String message) {
|
||||
public Message(ChatType type, int lobbyId, String sender, String target, String message) {
|
||||
this.type = type;
|
||||
this.game_id = game_id;
|
||||
this.user = user;
|
||||
this.lobbyId = lobbyId;
|
||||
this.sender = sender;
|
||||
this.target = target;
|
||||
this.message = message;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -59,78 +75,135 @@ public class Message {
|
||||
this.timestamp = now.format(formatter);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the text content of the message.
|
||||
*
|
||||
* @return The message string.
|
||||
*/
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public MessageType getMessageType() {
|
||||
/**
|
||||
* Returns the type of chat this message belongs to.
|
||||
*
|
||||
* @return The {@link ChatType}.
|
||||
*/
|
||||
public ChatType getMessageType() {
|
||||
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
|
||||
* @return - request as specified in the network protocol, as String
|
||||
* Formats the message object into a string representation compatible with the network protocol
|
||||
* arguments.
|
||||
*
|
||||
* @return A formatted string containing all message attributes for server transmission.
|
||||
*/
|
||||
public String toArgsString() {
|
||||
return String.format("TYPE=%s GAME=%d USER=%s TARGET=%s TIME=%s TEXT=%s",
|
||||
this.type.toString(), this.game_id, this.user, this.target, this.timestamp, this.message);
|
||||
String gameIdString = "";
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Message toMessageReqPars(List<RequestParameter> parameters) {
|
||||
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"));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern, to analyze the response String with the given parameters
|
||||
* Helper method to extract a specific parameter value by its key.
|
||||
*
|
||||
* @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 Pattern msgRex = Pattern.compile(
|
||||
"TYPE=(?<type>\\w+) GAME=(?<game>\\w+) USER=(?<user>\\w+) TARGET=(?<target>\\w+) TIME=(?<time>[0-9:.]+) TEXT=(?<text>.*)$");
|
||||
private static @NonNull String getParString(
|
||||
List<RequestParameter> parameters, String keyString) {
|
||||
return getParString(parameters, keyString, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to create a Message Object, from the information given by the String
|
||||
* @param response - String that got sent as a response from the server
|
||||
* @return - New Message Object
|
||||
* Helper method to extract a specific parameter value by its key, with a fallback default
|
||||
* value.
|
||||
*
|
||||
* @param parameters The list of parameters to search.
|
||||
* @param keyString The key to look for.
|
||||
* @param defaultVal The value to return if the key is missing.
|
||||
* @return The found value or the default value.
|
||||
*/
|
||||
public static Message toMessage(String response) {
|
||||
Matcher m = msgRex.matcher(response);
|
||||
if (! m.matches()) {
|
||||
throw new RuntimeException("Can not parse message: '" + response+"'");
|
||||
private static @NonNull String getParString(
|
||||
List<RequestParameter> parameters, String keyString, String defaultVal) {
|
||||
Optional<String> parOption =
|
||||
parameters.stream()
|
||||
.filter((p) -> keyString.equals(p.key()))
|
||||
.findFirst()
|
||||
.map(RequestParameter::value);
|
||||
if (parOption.isEmpty()) {
|
||||
if (defaultVal == null) {
|
||||
throw new RuntimeException("No " + keyString + " found");
|
||||
} else {
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
String typeString=m.group("type");
|
||||
return parOption.get();
|
||||
}
|
||||
|
||||
switch (typeString) {
|
||||
case "GLOBAL":
|
||||
return new Message(MessageType.GLOBAL,
|
||||
0,
|
||||
m.group("user"),
|
||||
null,
|
||||
m.group("time"),
|
||||
m.group("text"));
|
||||
|
||||
case "LOBBY":
|
||||
return new Message(MessageType.LOBBY,
|
||||
Integer.parseInt(m.group("game")),
|
||||
m.group("user"),
|
||||
null,
|
||||
m.group("time"),
|
||||
m.group("text"));
|
||||
|
||||
case "WHISPER":
|
||||
return new Message(MessageType.WHISPER,
|
||||
Integer.parseInt(m.group("game")),
|
||||
m.group("user"),
|
||||
m.group("target"),
|
||||
m.group("time"),
|
||||
m.group("text"));
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unknown message type " + typeString);
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Represents a playing card with a value and suit.
|
||||
*/
|
||||
/** Represents a playing card with a value and suit. */
|
||||
public class Card {
|
||||
private String value;
|
||||
private String suit;
|
||||
|
||||
@@ -1,60 +1,71 @@
|
||||
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.server.network.command.parsing.RequestParameter;
|
||||
import java.util.ArrayList;
|
||||
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
|
||||
* retrieving messages from the server. It uses the ClientService to send
|
||||
* commands and receive responses from the server.
|
||||
* The ChatClient class is responsible for sending messages to the server and retrieving messages
|
||||
* from the server. It uses the ClientService to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public class ChatClient {
|
||||
|
||||
private ClientService clientService;
|
||||
private final ClientService clientService;
|
||||
private final Logger logger;
|
||||
|
||||
/**
|
||||
* Constructs a ChatClient with the given ClientService for communication.
|
||||
*
|
||||
* @param clientService The ClientService instance used to send commands and
|
||||
* receive responses from the server.
|
||||
* @param clientService The ClientService instance used to send commands and receive responses
|
||||
* from the server.
|
||||
*/
|
||||
public ChatClient(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
|
||||
* sending a "SEND_MESSAGE" command with the message content as arguments.
|
||||
* Send a Message to the server by converting it to a string format and sending a "SEND_MESSAGE"
|
||||
* command with the message content as arguments.
|
||||
*
|
||||
* @param message The Message object to be sent to the server.
|
||||
*/
|
||||
public void sendMessage(Message message) {
|
||||
String request = "SEND_MESSAGE " + message.toArgsString();
|
||||
logger.info("Writing to server: " + request);
|
||||
clientService.processCommand(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT"
|
||||
* command to determine how many messages are available and then sending
|
||||
* "GET_NEXT_MESSAGE" commands in a loop to retrieve each message. The
|
||||
* retrieved messages are parsed into Message objects and returned as a list.
|
||||
* Retrieve messages from the server by first sending a "GET_MESSAGE_COUNT" command to determine
|
||||
* how many messages are available and then sending "GET_NEXT_MESSAGE" commands in a loop to
|
||||
* retrieve each message. The retrieved messages are parsed into Message objects and returned as
|
||||
* a list.
|
||||
*
|
||||
* @return A list of Message objects representing the messages retrieved from
|
||||
* the server.
|
||||
* @return A list of Message objects representing the messages retrieved from the server.
|
||||
*/
|
||||
public List<Message> getMessages() {
|
||||
String countStr = clientService.processCommand("GET_MESSAGE_COUNT");
|
||||
int count = Integer.parseInt(countStr);
|
||||
System.out.println("Got " + count + " messages");
|
||||
List<Message> messages = new ArrayList<>();
|
||||
logger.info("Asking server for new messages");
|
||||
List<RequestParameter> countStr =
|
||||
ClientService.convertToRequestParameters(
|
||||
clientService.processCommand("GET_MESSAGE_COUNT"));
|
||||
RequestParameter countRes = countStr.getFirst();
|
||||
if (!countRes.key().equals("COUNT")) {
|
||||
logger.error("Not the right response from server");
|
||||
}
|
||||
int count = Integer.parseInt(countRes.value());
|
||||
logger.info("Got " + count + " messages");
|
||||
ArrayList<Message> messages = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
String message = clientService.processCommand("GET_NEXT_MESSAGE");
|
||||
if (message != null) {
|
||||
Message message1 = Message.toMessage(message);
|
||||
messages.add(message1);
|
||||
}
|
||||
List<RequestParameter> msgRes =
|
||||
ClientService.convertToRequestParameters(
|
||||
clientService.processCommand("GET_NEXT_MESSAGE"));
|
||||
Message msg = Message.toMessageReqPars(msgRes);
|
||||
messages.add(msg);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
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.TcpTransport;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import java.io.IOException;
|
||||
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
|
||||
* server,
|
||||
* sending commands, and receiving responses. It uses a TcpTransport to
|
||||
* communicate
|
||||
* with the server and an ExecutorService to handle asynchronous requests.
|
||||
* The ClientService class is responsible for managing the connection to the server, sending
|
||||
* commands, and receiving responses. It uses a TcpTransport to communicate with the server and an
|
||||
* ExecutorService to handle asynchronous requests.
|
||||
*/
|
||||
public class ClientService {
|
||||
|
||||
@@ -30,23 +32,28 @@ public class ClientService {
|
||||
|
||||
public static ArrayList<String> response;
|
||||
private final AtomicInteger idGenerator;
|
||||
private Logger logger;
|
||||
|
||||
/**
|
||||
* Constructs a ClientService with the given server IP and port. It establishes
|
||||
* a socket connection to the server and initializes the TcpTransport and
|
||||
* ExecutorService for communication.
|
||||
* Constructs a ClientService with the given server IP and port. It establishes a socket
|
||||
* connection to the server and initializes the TcpTransport and ExecutorService for
|
||||
* 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.
|
||||
*/
|
||||
public ClientService(String ip, int port) {
|
||||
|
||||
this.idGenerator = new AtomicInteger(0);
|
||||
|
||||
this.logger = LogManager.getLogger(ClientService.class);
|
||||
|
||||
this.offlineMode = false;
|
||||
|
||||
try {
|
||||
socket = new Socket(ip, port);
|
||||
clienttcptransport = new TcpTransport(socket);
|
||||
logger.info("Connected to server at " + ip);
|
||||
} catch (IOException i) {
|
||||
throw new RuntimeException(i);
|
||||
}
|
||||
@@ -55,8 +62,8 @@ public class ClientService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a ClientService in offline mode. No network connection will be
|
||||
* attempted and calls to processCommand will throw a RuntimeException.
|
||||
* Constructs a ClientService in offline mode. No network connection will be attempted and calls
|
||||
* to processCommand will throw a RuntimeException.
|
||||
*
|
||||
* @param offline true to create an offline (no-network) client service
|
||||
*/
|
||||
@@ -68,56 +75,109 @@ public class ClientService {
|
||||
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() {
|
||||
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
|
||||
* sent using the TcpTransport, and the response is read in a loop until a
|
||||
* valid response is received. The method handles "+OK" and "-ERROR" responses
|
||||
* from the server and returns the actual response content.
|
||||
* Removes escape characters from a string, specifically converting escaped single quotes (\')
|
||||
* back to regular single quotes (').
|
||||
*
|
||||
* @param message The command message to be sent to the server.
|
||||
* @return The response from the server as a string.
|
||||
* @param input The escaped string to process.
|
||||
* @return The unescaped string.
|
||||
*/
|
||||
protected String processCommand(String message) {
|
||||
if (offlineMode) {
|
||||
throw new RuntimeException("ClientService is offline: cannot process command");
|
||||
}
|
||||
AtomicReference<String> response = new AtomicReference<>();
|
||||
sendRequest(() -> {
|
||||
try {
|
||||
writeToTransport(message);
|
||||
String responseLine = null;
|
||||
do {
|
||||
responseLine = clienttcptransport.read().payload();
|
||||
System.out.println("Raw message '" + responseLine + "'");
|
||||
if ("+OK".equals(responseLine)) {
|
||||
return;
|
||||
} else if (("-ERROR").equals(responseLine)) {
|
||||
throw new RuntimeException(responseLine);
|
||||
}
|
||||
response.set(responseLine);
|
||||
} while (true);
|
||||
} catch (Exception e) {
|
||||
throw getRuntimeException(e);
|
||||
}
|
||||
});
|
||||
return response.get();
|
||||
private static String unescape(String input) {
|
||||
return input.replaceAll("\\\\'", "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to send a request to the server using the ExecutorService. It
|
||||
* submits the request as a Runnable task and waits for its completion. If
|
||||
* the task is interrupted or encounters an execution exception, it throws a
|
||||
* RuntimeException with the appropriate cause.
|
||||
* 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 request The Runnable task representing the request to be sent to the
|
||||
* server.
|
||||
* @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 {
|
||||
writeToTransport(message);
|
||||
String responseText = null;
|
||||
|
||||
responseText = clienttcptransport.read().payload();
|
||||
logger.info("Raw message '" + responseText + "'");
|
||||
Boolean success = null;
|
||||
int count = 0;
|
||||
for (String line : responseText.split("\n")) {
|
||||
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);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw getRuntimeException(e);
|
||||
}
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to send a request to the server using the ExecutorService. It submits the
|
||||
* request as a Runnable task and waits for its completion. If the task is interrupted or
|
||||
* encounters an execution exception, it throws a RuntimeException with the appropriate cause.
|
||||
*
|
||||
* @param request The Runnable task representing the request to be sent to the server.
|
||||
*/
|
||||
private void sendRequest(Runnable 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
|
||||
* RuntimeException. If the cause is null, it returns the original exception as
|
||||
* a RuntimeException. If the cause is already a RuntimeException, it returns
|
||||
* it directly. Otherwise, it wraps the cause in a new RuntimeException and
|
||||
* returns it.
|
||||
* Helper method to extract the cause of an exception and return it as a RuntimeException. If
|
||||
* the cause is null, it returns the original exception as a RuntimeException. If the cause is
|
||||
* already a RuntimeException, it returns it directly. Otherwise, it wraps the cause in a new
|
||||
* RuntimeException and returns it.
|
||||
*
|
||||
* @param e The exception from which to extract the cause.
|
||||
* @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
|
||||
* ExecutorService.
|
||||
* It also closes the TcpTransport used for communication. If any IOException
|
||||
* occurs during this process, it prints the exception to the console.
|
||||
* Closes the socket connection to the server and shuts down the ExecutorService. It also closes
|
||||
* the TcpTransport used for communication. If any IOException occurs during this process, it
|
||||
* prints the exception to the console.
|
||||
*/
|
||||
public void closeSocket() {
|
||||
try {
|
||||
executor.shutdown();
|
||||
if (clienttcptransport != null) {
|
||||
clienttcptransport.close();
|
||||
}
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
clienttcptransport.close();
|
||||
socket.close();
|
||||
logger.info("Socket closed");
|
||||
} catch (IOException j) {
|
||||
System.out.println(j);
|
||||
logger.debug(j);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to write a command string to the TcpTransport. It generates a
|
||||
* 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.
|
||||
* Method to write with the tcp transport to the server
|
||||
*
|
||||
* @param s The command string to be sent to the server.
|
||||
* @throws IOException If an I/O error occurs while writing to the transport.
|
||||
* @param s - Message to be sent
|
||||
* @throws IOException
|
||||
*/
|
||||
private void writeToTransport(String s) throws IOException {
|
||||
int id = this.idGenerator.incrementAndGet();
|
||||
this.clienttcptransport.write(new RawPacket(id, s));
|
||||
}
|
||||
|
||||
public void ping() {
|
||||
processCommand("PING");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
/**
|
||||
* The CoreClient class provides basic functionalities for communicating with
|
||||
* the
|
||||
* server, such as sending a ping command to check connectivity and logging in
|
||||
* with a username. It uses the ClientService to send commands and receive
|
||||
* responses from the server.
|
||||
* The CoreClient class provides basic functionalities for communicating with the server, such as
|
||||
* sending a ping command to check connectivity and logging in with a username. It uses the
|
||||
* ClientService to send commands and receive responses from the server.
|
||||
*/
|
||||
public class CoreClient {
|
||||
private final ClientService clientService;
|
||||
@@ -13,16 +11,16 @@ public class CoreClient {
|
||||
/**
|
||||
* Constructs a CoreClient with the given ClientService for communication.
|
||||
*
|
||||
* @param clientservice The ClientService instance used to send commands and
|
||||
* receive responses from the server.
|
||||
* @param clientservice The ClientService instance used to send commands and receive responses
|
||||
* from the server.
|
||||
*/
|
||||
public CoreClient(ClientService clientservice) {
|
||||
this.clientService = clientservice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a "PING" command to the server to check connectivity. The server
|
||||
* should respond with a "PONG" message if the connection is successful.
|
||||
* Sends a "PING" command to the server to check connectivity. The server should respond with a
|
||||
* "PONG" message if the connection is successful.
|
||||
*/
|
||||
public void ping() {
|
||||
clientService.processCommand("PING");
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.network;
|
||||
|
||||
/**
|
||||
* The LobbyClient class is responsible for communicating with the server to
|
||||
* manage game lobbies. It provides methods to create a lobby, join a lobby,
|
||||
* and fetch the current status of a lobby by sending appropriate commands to
|
||||
* the server and processing the responses.
|
||||
* The LobbyClient class is responsible for communicating with the server to manage game lobbies. It
|
||||
* provides methods to create a lobby, join a lobby, and fetch the current status of a lobby by
|
||||
* sending appropriate commands to the server and processing the responses.
|
||||
*/
|
||||
public class LobbyClient {
|
||||
private final ClientService client;
|
||||
@@ -12,8 +11,8 @@ public class LobbyClient {
|
||||
/**
|
||||
* Constructs a LobbyClient with the given ClientService for communication.
|
||||
*
|
||||
* @param client The ClientService instance used to send commands and receive
|
||||
* responses from the server.
|
||||
* @param client The ClientService instance used to send commands and receive responses from the
|
||||
* server.
|
||||
*/
|
||||
public LobbyClient(ClientService 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.
|
||||
*
|
||||
* @param lobbyId The id of the lobby to fetch the status for.
|
||||
* @return A string representing the current status of the lobby, as returned
|
||||
* by the server.
|
||||
* @return A string representing the current status of the lobby, as returned by the server.
|
||||
*/
|
||||
public String fetchLobbyStatusString(int lobbyId) {
|
||||
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
|
||||
* created lobby.
|
||||
* Request the server to create a new lobby and return the id of the newly created lobby.
|
||||
*
|
||||
* @return The id of the newly created lobby, as returned by the server.
|
||||
*/
|
||||
public int createLobby() {
|
||||
String response = client.processCommand("CREATE_LOBBY");
|
||||
String response = client.processCommand("CREATE_LOBBY").getFirst();
|
||||
return Integer.parseInt(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the server to return the id of the lobby that the client is
|
||||
* currently in.
|
||||
* Request the server to return the id of the lobby that the client is currently in.
|
||||
*
|
||||
* @return The id of the lobby that the client is currently in, as returned by
|
||||
* the server.
|
||||
* @return The id of the lobby that the client is currently in, as returned by the server.
|
||||
*/
|
||||
public int getLobbyId() {
|
||||
String response = client.processCommand("GET_LOBBY_ID");
|
||||
String response = client.processCommand("GET_LOBBY_ID").getFirst();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
-74
@@ -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.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.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
/** Responsible for the presentation of the ChatModel to the Client */
|
||||
public class ChatViewController implements Initializable {
|
||||
|
||||
/**
|
||||
* Responsible for the presentation of the ChatModel to the Client
|
||||
*/
|
||||
@FXML private Button sendButton;
|
||||
|
||||
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 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;
|
||||
|
||||
public ChatViewController() {
|
||||
this.username = null;
|
||||
this.chatmodel = null;
|
||||
this.controller = null;
|
||||
this.timer = new Timer();
|
||||
}
|
||||
|
||||
public ChatViewController(String username, ChatModel chatmodel, ChatController controller) {
|
||||
public ChatViewController(ChatController chatController, ChatModel chatModel, String username) {
|
||||
this.controller = chatController;
|
||||
this.username = username;
|
||||
this.chatmodel = chatmodel;
|
||||
this.controller = controller;
|
||||
this.timer = new Timer();
|
||||
if (this.controller != null) {
|
||||
timer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
System.err.println("tick");
|
||||
if (ChatViewController.this.controller.receiveMessage()) {
|
||||
showMessage();
|
||||
}
|
||||
}
|
||||
}, 0, 1000);
|
||||
}
|
||||
this.chatModel = chatModel;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
if (inputField != null) {
|
||||
inputField.setOnAction(event -> sendMessage());
|
||||
}
|
||||
if (sendButton != null) {
|
||||
sendButton.setOnAction(event -> sendMessage());
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public void initialize(URL location, ResourceBundle resourceBundle) {
|
||||
inputField.setOnAction(event -> sendMessage());
|
||||
sendButton.setOnAction(event -> sendMessage());
|
||||
scrollPane.vvalueProperty().bind(chat.heightProperty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
String message = inputField.getText().trim();
|
||||
if (!message.isEmpty()) {
|
||||
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,11 +41,13 @@ public class Casinomainui extends Application {
|
||||
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);
|
||||
stage.setTitle("Casono");
|
||||
javafx.scene.image.Image icon = new javafx.scene.image.Image(
|
||||
getClass().getResource("/images/logoinverted.png").toExternalForm());
|
||||
javafx.scene.image.Image icon =
|
||||
new javafx.scene.image.Image(
|
||||
getClass().getResource("/images/logoinverted.png").toExternalForm());
|
||||
stage.getIcons().add(icon);
|
||||
stage.setScene(scene);
|
||||
stage.setFullScreen(true);
|
||||
|
||||
+24
-35
@@ -1,12 +1,14 @@
|
||||
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.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
@@ -14,34 +16,20 @@ import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
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 {
|
||||
private static final Logger LOGGER = LogManager.getLogger(CasinomainuiController.class);
|
||||
|
||||
@FXML
|
||||
private AnchorPane rootPane;
|
||||
@FXML
|
||||
private Label titleLabel;
|
||||
@FXML
|
||||
private Label subtitleLabel;
|
||||
@FXML
|
||||
private ImageView logoView;
|
||||
@FXML
|
||||
private Rectangle greenBox;
|
||||
@FXML
|
||||
private Button exitbutton;
|
||||
@FXML
|
||||
private VBox casinoTable;
|
||||
@FXML
|
||||
private TextField usernameField;
|
||||
@FXML
|
||||
private Button loginButton;
|
||||
@FXML private AnchorPane rootPane;
|
||||
@FXML private Label titleLabel;
|
||||
@FXML private Label subtitleLabel;
|
||||
@FXML private ImageView logoView;
|
||||
@FXML private Rectangle greenBox;
|
||||
@FXML private Button exitbutton;
|
||||
@FXML private VBox casinoTable;
|
||||
@FXML private TextField usernameField;
|
||||
@FXML private Button loginButton;
|
||||
|
||||
private LobbyButtonTranslationManager translationManager;
|
||||
private LobbyButtonGridManager gridManager;
|
||||
@@ -67,10 +55,16 @@ public class CasinomainuiController {
|
||||
try {
|
||||
clientService = new ClientService(host, port);
|
||||
} 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
|
||||
}
|
||||
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
|
||||
// fail with RuntimeException
|
||||
lobbyClient = new LobbyClient(clientService);
|
||||
@@ -79,10 +73,7 @@ public class CasinomainuiController {
|
||||
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
|
||||
public void handleLoginButton() {
|
||||
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) {
|
||||
Alert alert = new Alert(AlertType.INFORMATION);
|
||||
alert.setTitle("Info");
|
||||
|
||||
+204
-246
@@ -1,21 +1,17 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.client.ui.lobbyui;
|
||||
|
||||
/**
|
||||
* Manages the grid for lobby buttons and rendering. Uses LobbyButtonTranslationManager for mapping
|
||||
* ButtonID to LobbyID.
|
||||
*/
|
||||
import java.util.Map;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.LobbyClient;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
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.control.Button;
|
||||
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.Logger;
|
||||
|
||||
/**
|
||||
* Manages the grid for lobby buttons and rendering. Uses
|
||||
* LobbyButtonTranslationManager for mapping
|
||||
* ButtonID to LobbyID.
|
||||
*/
|
||||
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 Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||
|
||||
/** 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 REFRESH_INTERVAL_SECONDS = 5;
|
||||
private static final int INITIAL_DELAY_SECONDS = 5;
|
||||
private static final int COLS = 4;
|
||||
|
||||
/** Image for a lobby in CREATED state. */
|
||||
/** Default fallback image. */
|
||||
private static final Logger LOGGER = LogManager.getLogger(LobbyButtonGridManager.class);
|
||||
|
||||
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";
|
||||
|
||||
/** Cache for loaded Images keyed by resource path. */
|
||||
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final GridPane gridPane;
|
||||
private final LobbyButtonTranslationManager translationManager;
|
||||
private final LobbyClient lobbyClient;
|
||||
|
||||
/** Executor for background status/network tasks. */
|
||||
private final ConcurrentHashMap<String, Image> imageCache = new ConcurrentHashMap<>();
|
||||
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
/** Scheduler for periodic refresh of lobby mappings. */
|
||||
|
||||
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(
|
||||
GridPane gridPane, LobbyButtonTranslationManager translationManager, LobbyClient lobbyClient) {
|
||||
GridPane gridPane,
|
||||
LobbyButtonTranslationManager translationManager,
|
||||
LobbyClient lobbyClient) {
|
||||
|
||||
this.gridPane = gridPane;
|
||||
// Always use the singleton
|
||||
this.translationManager = LobbyButtonTranslationManager.getInstance();
|
||||
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(
|
||||
GridPane gridPane, LobbyButtonTranslationManager translationManager, ClientService clientService) {
|
||||
GridPane gridPane,
|
||||
LobbyButtonTranslationManager translationManager,
|
||||
ClientService 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) {
|
||||
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() {
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
if (mapping.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
// Make a copy of entries to avoid concurrent modification
|
||||
|
||||
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(mapping.entrySet());
|
||||
|
||||
for (Map.Entry<Integer, Integer> e : entries) {
|
||||
int buttonId = e.getKey();
|
||||
int lobbyId = e.getValue();
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
String status = lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||
return status;
|
||||
} catch (Exception ex) {
|
||||
LOGGER.info("Lobby {} appears missing or error: {}", lobbyId, ex.getMessage());
|
||||
return null;
|
||||
}
|
||||
}, executor).thenAccept(status -> {
|
||||
if (status == null) {
|
||||
// remove mapping and update UI
|
||||
translationManager.removeLobbyButton(buttonId);
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
updateLobbyButtonImages();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
try {
|
||||
return lobbyClient.fetchLobbyStatusString(lobbyId);
|
||||
} catch (Exception ex) {
|
||||
LOGGER.info("Lobby {} missing: {}", lobbyId, ex.getMessage());
|
||||
return null;
|
||||
}
|
||||
},
|
||||
executor)
|
||||
.thenAccept(
|
||||
status -> {
|
||||
if (status == null) {
|
||||
translationManager.removeLobbyButton(buttonId);
|
||||
|
||||
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() {
|
||||
gridPane.getChildren().clear();
|
||||
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
if (mapping.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
|
||||
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);
|
||||
Button btn = new Button();
|
||||
btn.setId("lobbyBtn-" + buttonId);
|
||||
// placeholder image so UI remains responsive
|
||||
Image placeholder = safeLoadImage(BUTTON_FALLBACK_IMAGE);
|
||||
ImageView imageView = new ImageView(placeholder);
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView.fitWidthProperty().bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
|
||||
imageView.setSmooth(true);
|
||||
btn.setGraphic(imageView);
|
||||
btn.setMaxWidth(Double.MAX_VALUE);
|
||||
btn.setMaxHeight(Double.MAX_VALUE);
|
||||
btn.setMinWidth(BUTTON_MIN_SIZE);
|
||||
btn.setMinHeight(BUTTON_MIN_SIZE);
|
||||
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
final int bId = buttonId;
|
||||
btn.setOnAction(e -> {
|
||||
Integer targetLobbyId = translationManager.getLobbyIdForButton(bId);
|
||||
if (targetLobbyId != null) {
|
||||
joinLobby(targetLobbyId);
|
||||
}
|
||||
});
|
||||
// async fetch status and update image
|
||||
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
|
||||
.thenAccept(statusStr -> {
|
||||
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||
String path = getImagePathForButton(buttonId, status == null ? LobbyStatus.CREATED : status);
|
||||
Image img = safeLoadImage(path);
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
ImageView iv = new ImageView(img);
|
||||
iv.setPreserveRatio(true);
|
||||
iv.fitWidthProperty()
|
||||
.bind(gridPane.widthProperty().divide(COLS).subtract(BUTTON_WIDTH_MARGIN));
|
||||
iv.setSmooth(true);
|
||||
btn.setGraphic(iv);
|
||||
});
|
||||
});
|
||||
|
||||
Button btn = createLobbyButton(buttonId, lobbyId);
|
||||
|
||||
int row = index / COLS;
|
||||
int col = index % COLS;
|
||||
|
||||
gridPane.add(btn, col, row);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/** Possible lobby statuses. */
|
||||
private Button createLobbyButton(int buttonId, int lobbyId) {
|
||||
Button btn = new Button();
|
||||
btn.setId("lobbyBtn-" + buttonId);
|
||||
|
||||
ImageView imageView = new ImageView(safeLoadImage(BUTTON_FALLBACK_IMAGE));
|
||||
|
||||
imageView.setPreserveRatio(true);
|
||||
imageView
|
||||
.fitWidthProperty()
|
||||
.bind(gridPane.widthProperty().divide(COLS).subtract(BTN_WIDTH_MRG));
|
||||
|
||||
btn.setGraphic(imageView);
|
||||
btn.setMaxWidth(Double.MAX_VALUE);
|
||||
btn.setMaxHeight(Double.MAX_VALUE);
|
||||
btn.setMinWidth(BUTTON_MIN_SIZE);
|
||||
btn.setMinHeight(BUTTON_MIN_SIZE);
|
||||
|
||||
GridPane.setHgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
GridPane.setVgrow(btn, javafx.scene.layout.Priority.ALWAYS);
|
||||
|
||||
btn.setOnAction(
|
||||
e -> {
|
||||
Integer targetLobbyId = translationManager.getLobbyIdForButton(buttonId);
|
||||
|
||||
if (targetLobbyId != null) {
|
||||
joinLobby(targetLobbyId);
|
||||
}
|
||||
});
|
||||
|
||||
loadLobbyImageAsync(btn, buttonId, lobbyId);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void loadLobbyImageAsync(Button btn, int buttonId, int lobbyId) {
|
||||
|
||||
CompletableFuture.supplyAsync(() -> lobbyClient.fetchLobbyStatusString(lobbyId), executor)
|
||||
.thenAccept(
|
||||
statusStr -> {
|
||||
LobbyStatus status = parseLobbyStatus(statusStr);
|
||||
|
||||
String path =
|
||||
getImagePathForButton(
|
||||
buttonId,
|
||||
status == null ? LobbyStatus.CREATED : status);
|
||||
|
||||
Image img = safeLoadImage(path);
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
ImageView iv = new ImageView(img);
|
||||
iv.setPreserveRatio(true);
|
||||
iv.fitWidthProperty()
|
||||
.bind(
|
||||
gridPane.widthProperty()
|
||||
.divide(COLS)
|
||||
.subtract(BTN_WIDTH_MRG));
|
||||
btn.setGraphic(iv);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private enum LobbyStatus {
|
||||
CREATED,
|
||||
RUNNING
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current status of a lobby.
|
||||
*
|
||||
* @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)
|
||||
if (statusStr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return LobbyStatus.valueOf(statusStr.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
@@ -238,173 +209,160 @@ public class LobbyButtonGridManager {
|
||||
}
|
||||
|
||||
private String getImagePathForButton(int buttonId, LobbyStatus status) {
|
||||
|
||||
String statusStr = status == LobbyStatus.CREATED ? "created" : "running";
|
||||
|
||||
return String.format(BUTTON_IMAGE_TEMPLATE, buttonId, statusStr);
|
||||
}
|
||||
|
||||
private Image safeLoadImage(String path) {
|
||||
// Return cached image if present
|
||||
Image cached = imageCache.get(path);
|
||||
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
// Attempt to load the requested resource
|
||||
|
||||
java.io.InputStream is = getClass().getResourceAsStream(path);
|
||||
|
||||
if (is == null) {
|
||||
LOGGER.debug(
|
||||
"Image resource not found: {}. Falling back to {}",
|
||||
path,
|
||||
BUTTON_FALLBACK_IMAGE);
|
||||
is = getClass().getResourceAsStream(BUTTON_FALLBACK_IMAGE);
|
||||
}
|
||||
|
||||
Image loaded = null;
|
||||
|
||||
try {
|
||||
if (is != null) {
|
||||
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) {
|
||||
LOGGER.error("Failed to load image '{}'", path, e);
|
||||
}
|
||||
if (loaded == null) {
|
||||
// leave
|
||||
// null
|
||||
LOGGER.error("Image load failed: {}", path, e);
|
||||
}
|
||||
|
||||
if (loaded != null) {
|
||||
imageCache.put(path, loaded);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
||||
/** Update all lobby buttons' images according to the current lobby statuses. */
|
||||
public void updateLobbyButtonImages() {
|
||||
Map<Integer, Integer> mapping = translationManager.getButtonIdToLobbyId();
|
||||
|
||||
if (mapping.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<Integer> buttonIds = new ArrayList<>(mapping.keySet());
|
||||
Collections.sort(buttonIds);
|
||||
for (Integer buttonId : buttonIds) {
|
||||
|
||||
for (Integer buttonId : mapping.keySet()) {
|
||||
int lobbyId = mapping.get(buttonId);
|
||||
CompletableFuture.supplyAsync(() -> getLobbyStatus(lobbyId), executor)
|
||||
.thenAccept(status -> {
|
||||
String path = getImagePathForButton(buttonId, status);
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
for (Node node : gridPane.getChildren()) {
|
||||
if (node instanceof Button && ("lobbyBtn-" + buttonId).equals(node.getId())) {
|
||||
Button btn = (Button) node;
|
||||
ImageView iv = new ImageView(safeLoadImage(path));
|
||||
iv.setPreserveRatio(true);
|
||||
iv.fitWidthProperty()
|
||||
.bind(
|
||||
gridPane.widthProperty()
|
||||
.divide(COLS)
|
||||
.subtract(BUTTON_WIDTH_MARGIN));
|
||||
iv.setSmooth(true);
|
||||
btn.setGraphic(iv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
for (Node node : gridPane.getChildren()) {
|
||||
|
||||
boolean isButton = node instanceof Button;
|
||||
boolean idMatches =
|
||||
("lobbyBtn-" + buttonId)
|
||||
.equals(node.getId());
|
||||
|
||||
if (isButton && idMatches) {
|
||||
Button btn = (Button) node;
|
||||
|
||||
ImageView iv =
|
||||
new ImageView(safeLoadImage(path));
|
||||
|
||||
iv.setPreserveRatio(true);
|
||||
iv.fitWidthProperty()
|
||||
.bind(
|
||||
gridPane.widthProperty()
|
||||
.divide(COLS)
|
||||
.subtract(
|
||||
BTN_WIDTH_MRG));
|
||||
|
||||
btn.setGraphic(iv);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new lobby via the LobbyClient.
|
||||
*
|
||||
* @return The generated lobbyId
|
||||
*/
|
||||
public int createLobby() {
|
||||
try {
|
||||
int lobbyId = lobbyClient.createLobby();
|
||||
LOGGER.info("Lobby created via LobbyClient: {}", lobbyId);
|
||||
|
||||
if (lobbyId <= 0) {
|
||||
throw new RuntimeException("LobbyClient returned invalid lobby id: " + lobbyId);
|
||||
throw new RuntimeException("Invalid lobby id: " + lobbyId);
|
||||
}
|
||||
|
||||
return lobbyId;
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to create lobby via LobbyClient: {}", e.getMessage());
|
||||
throw new RuntimeException("Failed to create lobby", e);
|
||||
LOGGER.error("Create lobby failed: {}", e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for joining a lobby.
|
||||
*
|
||||
* @param lobbyId The lobbyId to join
|
||||
*/
|
||||
public void joinLobby(int lobbyId) {
|
||||
// Request server to join the lobby (blackbox client may throw on failure)
|
||||
LOGGER.info("Joining lobby: {}", lobbyId);
|
||||
try {
|
||||
lobbyClient.joinLobby(lobbyId);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("LobbyClient failed to join lobby {}: {}", lobbyId, e.getMessage());
|
||||
LOGGER.error("Join failed: {}", e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
javafx.application.Platform.runLater(
|
||||
() -> {
|
||||
// Hide lobby stage (do not close) so we can return later
|
||||
javafx.scene.Scene scene = gridPane.getScene();
|
||||
javafx.stage.Stage currentStage = (javafx.stage.Stage) scene.getWindow();
|
||||
javafx.stage.Stage currentStage =
|
||||
(javafx.stage.Stage) gridPane.getScene().getWindow();
|
||||
|
||||
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();
|
||||
|
||||
gameStage.setOnHidden(
|
||||
ev -> {
|
||||
try {
|
||||
currentStage.show();
|
||||
// refresh mappings immediately when returning from game
|
||||
refreshMappings();
|
||||
updateLobbyButtonImages();
|
||||
} catch (Exception ex) {
|
||||
LOGGER.error(
|
||||
"Error while returning to lobby: {}", ex.getMessage());
|
||||
}
|
||||
currentStage.show();
|
||||
refreshMappings();
|
||||
updateLobbyButtonImages();
|
||||
});
|
||||
// Start the Game UI using the prepared stage
|
||||
|
||||
try {
|
||||
// ClientService an GameUI übergeben
|
||||
ch.unibas.dmi.dbis.cs108.casono.client.ui.gameui.CasinoGameUI
|
||||
.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) {
|
||||
LOGGER.error("Error starting Game UI: {}", e.getMessage());
|
||||
// If starting fails, show the lobby again
|
||||
LOGGER.error("Game UI failed: {}", e.getMessage());
|
||||
currentStage.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the GridPane.
|
||||
*
|
||||
* @return The GridPane for the button grid
|
||||
*/
|
||||
public javafx.scene.layout.GridPane getGridPane() {
|
||||
public GridPane getGridPane() {
|
||||
return gridPane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the configured LobbyClient so callers can invoke its methods
|
||||
* directly (createLobby, fetchLobbyStatusString, joinLobby, ...).
|
||||
*/
|
||||
public LobbyClient getLobbyClient() {
|
||||
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() {
|
||||
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.CheckUsernameParser;
|
||||
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.ListUsersParser;
|
||||
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.PingParser;
|
||||
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.UserRegistry;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.NetworkManager;
|
||||
@@ -110,6 +119,20 @@ public class ServerApp {
|
||||
commandRouter.register(
|
||||
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());
|
||||
commandRouter.register(
|
||||
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;
|
||||
|
||||
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 java.time.Instant;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
@@ -88,6 +89,14 @@ public class User {
|
||||
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) {
|
||||
List<Message> allMessages = new ArrayDeque<>(messages).stream().toList();
|
||||
messages.clear();
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</VBox>
|
||||
|
||||
<!-- RECHTER BEREICH (Info-Box) -->
|
||||
<fx:include source="components/Chatbox.fxml" GridPane.columnIndex="2"/>
|
||||
<fx:include source="components/chatui/chatbox.fxml" GridPane.columnIndex="2"/>
|
||||
</children>
|
||||
</GridPane>
|
||||
</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-width: 12;
|
||||
-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 {
|
||||
@@ -83,7 +83,7 @@
|
||||
.gray-button {
|
||||
-fx-background-color: #333333;
|
||||
-fx-border-color: #666666;
|
||||
-fx-text-fill: #cccccc;
|
||||
-fx-text-fill: #ffffff;
|
||||
}
|
||||
|
||||
.gray-button, .yellow-button, .red-button {
|
||||
@@ -153,3 +153,54 @@
|
||||
.scroll-pane .scroll-bar:horizontal .thumb:hover {
|
||||
-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;
|
||||
|
||||
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.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ChatControllerTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
}
|
||||
public void setup() {}
|
||||
|
||||
@AfterEach
|
||||
public void teardown() {
|
||||
public void teardown() {}
|
||||
|
||||
}
|
||||
/*
|
||||
* @Test
|
||||
* 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.TcpTransport;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -14,9 +12,9 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Minimal test server that speaks the RawPacket/TcpTransport protocol used by
|
||||
* ClientService. Provides deterministic responses for CREATE_LOBBY and
|
||||
* GET_LOBBY_STATUS so unit tests can run without a real backend.
|
||||
* Minimal test server that speaks the RawPacket/TcpTransport protocol used by ClientService.
|
||||
* Provides deterministic responses for CREATE_LOBBY and GET_LOBBY_STATUS so unit tests can run
|
||||
* without a real backend.
|
||||
*/
|
||||
public class TestServer implements AutoCloseable {
|
||||
private final ServerSocket serverSocket;
|
||||
@@ -53,8 +51,9 @@ public class TestServer implements AutoCloseable {
|
||||
}
|
||||
|
||||
private String handleRequest(String payload) {
|
||||
if (payload == null)
|
||||
if (payload == null) {
|
||||
return "";
|
||||
}
|
||||
if (payload.startsWith("CREATE_LOBBY")) {
|
||||
int id = nextLobbyId.getAndIncrement();
|
||||
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 javafx.scene.layout.GridPane;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.client.network.ClientService;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
class LobbyButtonGridManagerTest {
|
||||
@@ -27,13 +27,14 @@ class LobbyButtonGridManagerTest {
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws Exception {
|
||||
if (testServer != null)
|
||||
if (testServer != null) {
|
||||
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