Compare commits

...

65 Commits

Author SHA1 Message Date
d62e0b7188 chuuuut 2025-03-13 10:10:16 +01:00
ef7bbc8492 packetpool: remove simulation 2025-03-13 10:08:36 +01:00
Clément
eee63cc5c6 lil fix 2025-03-13 10:03:26 +01:00
Clément
a4c5d3f67b add roomss 2025-03-13 09:52:25 +01:00
Clément
709a92aa4c little cleanup 2025-03-12 23:38:30 +01:00
Clément
67124f4731 labelled the name of the room when in it 2025-03-12 23:34:08 +01:00
Clément
631cd25a9d updates the rooms every second 2025-03-12 23:29:22 +01:00
Clément
dfdaae163b documentation for all major modules 2025-03-12 23:16:39 +01:00
Clément
0b6f5193a0 wait animation for loading 2025-03-12 23:04:35 +01:00
Clément
4bb45cc3db tag + basic layout for rooms 2025-03-12 22:55:31 +01:00
Clément
39a65e1ca6 colored message (with evil regex) 2025-03-12 22:17:17 +01:00
Clément
2b17891379 working and prettier chat 2025-03-12 22:01:30 +01:00
Clément
bfd3c10e9e all functionality handled 2025-03-12 21:34:55 +01:00
Clément
11fc2e2ac8 added main components for joining and leaving rooms 2025-03-12 15:29:03 +01:00
Clément
2be11ec4a8 It works ! 2025-03-12 14:26:13 +01:00
3eec95e420 ne pas regarder 2025-03-12 13:18:27 +01:00
0900113bb8 add doc 2025-03-12 12:56:45 +01:00
Clément
07272732d4 Pass Client & ClientListener through all controllers 2025-03-10 13:52:35 +01:00
3abdc09819 generic signals 2025-03-10 13:51:48 +01:00
980527f45f mieux 2025-03-10 12:42:40 +01:00
Clément
a2a5e96dc5 forgot to do that 2025-03-07 11:25:36 +01:00
Clément
3ad5bcf819 README update 2025-03-07 11:24:40 +01:00
462307dabc network: process time out 2025-03-06 12:02:42 +01:00
72c62bb1b4 network: remove ack by default 2025-03-06 11:48:59 +01:00
c8a748fe71 uggly reliable 2025-03-06 11:46:45 +01:00
Clément
6950810b95 Basic structure for GUI 2025-03-05 23:29:58 +01:00
Clément
f87145ed69 styling and transition with login page 2025-03-05 18:38:54 +01:00
Clément
5cefe42a99 Login page 2025-03-05 18:08:26 +01:00
Clément
6d228aee55 Icon + loader for client 2025-03-05 12:55:35 +01:00
Clément
a83104d322 ServerGui view 2025-03-05 12:06:27 +01:00
583505d93a indent file 2025-03-04 19:31:16 +01:00
0a8006fd56 no more tasks 2025-03-04 19:30:42 +01:00
Clément
0560d23cd3 Added forgotten fxml 2025-03-04 19:19:47 +01:00
Clément
7866984e19 added gui for server 2025-03-04 19:11:21 +01:00
Clément
5669859ac1 Update README.md 2025-03-04 18:18:41 +01:00
2bb3e64f2b big socket refactor 2025-03-04 16:32:11 +01:00
76da347fb9 client gui + headless server 2025-03-04 16:07:35 +01:00
Clément
e536a45266 Migrate everything on Gradle
Also added tasks to run the server & client separately
2025-03-04 15:08:43 +01:00
3115d397a4 update Sudoku submodule 2025-03-03 22:02:18 +01:00
a5a41f573b remove unused imports 2025-03-03 22:01:52 +01:00
8f30f139cd better command interface 2025-03-03 21:29:54 +01:00
Clément
c83e39ea4b preventing regex injection 2025-03-03 12:23:44 +01:00
Clément
5befdd3080 Added tag feature 2025-03-03 12:15:17 +01:00
Clément
4b8adef72f fix to prevent server chat from displaying the handshakes spam 2025-03-02 13:07:09 +01:00
Clément
39a2afcd6e fix for message where there's no room available while typing /room 2025-03-02 12:42:44 +01:00
Clément
c9e564370e Fixed bug allowing client to be in two different rooms at the same time 2025-03-02 12:37:25 +01:00
Clément
e52066ce17 Added /room command to know in which room the user is 2025-03-02 12:28:53 +01:00
Clément
e9f1feaaad Refactor to help further graphics 2025-03-02 11:53:59 +01:00
Clément
7adb581e33 fixed bug when joining room while being in another 2025-03-01 19:56:02 +01:00
Clément
10f6b059b1 escape strings for real tui 2025-03-01 19:50:19 +01:00
Clément
f8f740f799 add command aliases 2025-03-01 14:55:59 +01:00
Clément
a2c4319182 handshaking 2025-03-01 14:52:05 +01:00
90f92281ef leave/join room messages 2025-03-01 13:26:09 +01:00
0533c16cf2 refactor server room operations 2025-03-01 13:06:55 +01:00
63ec7b3aaa add disconnect 2025-03-01 13:00:58 +01:00
07ad2ba05e ClientListener + ClientConsole 2025-03-01 12:41:40 +01:00
Clément
a041193ce2 Updated README.md 2025-03-01 11:36:30 +01:00
Clément
5986b2f43c Fix (user could join a room twice) + Simon's request
Simon's request
2025-03-01 09:51:08 +01:00
Clément
aaf2e83b35 Added Client Flushing its own messages (work only in terminal) 2025-03-01 09:36:10 +01:00
Clément
8f46e8dc91 Removed message sending confirmation 2025-03-01 08:58:50 +01:00
Clément
e40d9ac8b8 help menu 2025-02-28 15:12:58 +01:00
Clément
09637ba775 Correct format for hour 2025-02-28 15:02:03 +01:00
Clément
f0a9617649 Added colors 2025-02-28 14:58:42 +01:00
Clément
ac631cbe0f Merge remote-tracking branch 'origin/main' 2025-02-28 14:33:45 +01:00
Clément
3554e42718 username change 2025-02-28 14:33:24 +01:00
65 changed files with 2724 additions and 498 deletions

7
ChatApp/.gitignore vendored
View File

@@ -1,3 +1,8 @@
.vscode
bin
lib
lib
# Ignore Gradle project-specific cache directory
.gradle
# Ignore Gradle build output directory
build

View File

@@ -1,18 +1,82 @@
## Getting Started
# ChatAPP
An instant messaging app using Java and the UDP protocol.
Welcome to the VS Code Java world. Here is a guideline to help you get started to write Java code in Visual Studio Code.
## How to run
## Folder Structure
### Console
The workspace contains two folders by default, where:
#### Server + Client
- `src`: the folder to maintain sources
- `lib`: the folder to maintain dependencies
You can create a server and an "admin" client by using:
Meanwhile, the compiled output files will be generated in the `bin` folder by default.
```shell
./gradlew admin # Linux
.\gradlew.bat admin # Windows
```
> If you want to customize the folder structure, open `.vscode/settings.json` and update the related settings there.
You will receive the notifications of the server (handshakes, ...).
## Dependency Management
#### Server
The `JAVA PROJECTS` view allows you to manage your dependencies. More details can be found [here](https://github.com/microsoft/vscode-java-dependency#manage-dependencies).
You can create a server by launching:
```shell
./gradlew server # Linux
.\gradlew server # Windows
```
If you want to create a server on a specific port, you can use:
```shell
./gradlew server --args="port" # Linux
.\gradlew server --args="port" # Windows
```
where `port` is the port you want to use.
This will create the server on a random port available and indicate it through the terminal.
To launch the server on a particular port, simply use:
```shell
./gradlew server --args="port" # Linux
.\gradlew server --args="port" # Windows
```
where `port` is the port you want to use.
#### Client
To create a client, you may use:
```shell
./gradlew client # Linux
.\gradlew client # Windows
```
## How to use
As soon as you launch a client, you will be prompted to enter your name. You will then be in the lobby. From here you
can join whatever room that is created, and you will be able to chat with the other clients in the room.
You will also be able to create a new room.
## Commands
> [!TIP]
> The commands can be found by typing `/help` in the chat, in the lobby or in any room.
> [!NOTE]
> All the commands are prefixed by `/`.
- /createRoom *roomName*
- /listRooms
- /joinRoom *roomName*
- /leaveRoom
- /room
- /bye
- /help
> [!NOTE]
> There are some aliases for the commands:
> - /create
> - /list
> - /join
> - /leave

62
ChatApp/app/build.gradle Normal file
View File

@@ -0,0 +1,62 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java application project to get you started.
* For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.13/userguide/building_java_projects.html in the Gradle documentation.
*/
plugins {
// Apply the application plugin to add support for building a CLI application in Java.
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
}
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}
dependencies {
// Use JUnit Jupiter for testing.
testImplementation libs.junit.jupiter
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// This dependency is used by the application.
implementation libs.guava
}
// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
javafx {
modules = [ 'javafx.graphics', 'javafx.controls', 'javafx.fxml' ]
}
application {
// Define the main class for the application.
if (project.hasProperty('admin')) {
mainClass = 'ChatApp'
} else if (project.hasProperty('server')) {
mainClass = 'ChatAppServer'
} else if (project.hasProperty('client')) {
mainClass = 'ChatAppClient'
} else if (project.hasProperty('serverGui')) {
mainClass = 'server.ServerGui'
} else {
mainClass = 'client.ClientGui'
}
}
run {
standardInput = System.in
}
tasks.named('test') {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}

View File

@@ -0,0 +1,23 @@
import java.net.InetSocketAddress;
import client.ClientConsole;
import server.Server;
public class ChatApp {
public static void main(String[] args) throws Exception {
Server server = new Server(6665);
ClientConsole client = new ClientConsole(new InetSocketAddress("localhost", 6665));
client.onConnect.connect((arg) -> {
client.getClientInterface().SendCreateRoom("101");
});
client.onDisconnect.connect((arg) -> {
System.out.println("Stopping server ...");
server.close();
System.out.println("Done !");
});
}
}

View File

@@ -0,0 +1,15 @@
import client.ClientConsole;
import java.net.InetSocketAddress;
public class ChatAppClient {
public static void main(String[] args) {
ClientConsole console = new ClientConsole(new InetSocketAddress("localhost", 6665));
try {
console.joinThread();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("End !");
}
}

View File

@@ -0,0 +1,15 @@
import java.net.SocketException;
import server.Server;
public class ChatAppServer {
public static void main(String[] args) {
// run ./gradlew server --args="port" to launch the server with a specific port
try {
Server server = new Server(args.length > 0 ? Integer.parseInt(args[0]) : 0);
System.out.println("Server running on port " + server.getRunningPort() + "!");
} catch (SocketException e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,58 @@
package client;
import java.net.InetSocketAddress;
import java.net.SocketException;
import network.Socket;
import network.protocol.packets.*;
public class Client {
private final ClientConnexion connexion;
private ClientListener callback;
public Client(InetSocketAddress serverAddress, ClientListener callback, String pseudo) throws SocketException {
this.connexion = new ClientConnexion(new Socket(), serverAddress, callback);
this.callback = callback;
login(pseudo);
}
private void login(String pseudo) {
this.connexion.sendPacket(new LoginPacket(pseudo));
}
public void close() {
this.connexion.sendPacket(new DisconnectPacket("Leaving"));
this.connexion.close();
this.callback.handleDisconnect();
}
public void SendChatMessage(String message) {
this.connexion.sendPacket(new SendChatMessagePacket(message));
}
public void SendCreateRoom(String roomName) {
this.connexion.sendPacket(new CreateRoomPacket(roomName));
}
public void SendJoinRoom(String roomName) {
this.connexion.sendPacket(new JoinRoomPacket(roomName));
}
public void SendLeaveRoom() {
this.connexion.sendPacket(new LeaveRoomPacket());
}
public void RequestRoomList() {
this.connexion.sendPacket(new RequestRoomListPacket());
}
public void RequestActualRoom() {
this.connexion.sendPacket(new RequestActualRoomPacket());
}
public void changeCallback(ClientListener callback) {
this.callback = callback;
this.connexion.changeCallback(callback);
}
}

View File

@@ -1,42 +1,45 @@
package client;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import network.PacketHandler;
import network.SocketReader;
import network.SocketWriter;
import network.Socket;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
import network.protocol.packets.ChatMessagePacket;
import network.protocol.packets.CreateRoomPacket;
import network.protocol.packets.JoinRoomPacket;
import network.protocol.packets.LeaveRoomPacket;
import network.protocol.packets.LoginPacket;
import network.protocol.packets.RequestRoomListPacket;
import network.protocol.packets.RoomListPacket;
import network.protocol.packets.SendChatMessagePacket;
import network.protocol.packets.ServerResponsePacket;
import network.protocol.packets.*;
import network.protocol.packets.ServerResponsePacket.Response;
public class ClientConnexion implements PacketVisitor, PacketHandler{
public class ClientConnexion implements PacketVisitor, PacketHandler {
private final InetSocketAddress serverAddress;
private final SocketWriter writer;
private final SocketReader reader;
private final Socket socket;
private ClientListener callback;
public ClientConnexion(DatagramSocket socket, InetSocketAddress serverAddress) {
public ClientConnexion(Socket socket, InetSocketAddress serverAddress, ClientListener callback) {
this.serverAddress = serverAddress;
this.writer = new SocketWriter(socket);
this.reader = new SocketReader(socket, this);
this.socket = socket;
this.callback = callback;
this.socket.onClose.connect((args) -> onSocketClose());
this.socket.addHandler(this);
}
private void onSocketClose() {
this.callback.handleConnexionError();
}
public void close() {
this.reader.stop();
this.socket.close();
}
public void sendPacket(Packet packet) throws IOException {
this.writer.sendPacket(packet, serverAddress);
public void sendPacket(Packet packet) {
try {
this.socket.sendPacket(packet, serverAddress);
} catch (IOException e) {
this.close();
this.callback.handleConnexionError();
e.printStackTrace();
}
}
@Override
@@ -47,14 +50,39 @@ public class ClientConnexion implements PacketVisitor, PacketHandler{
@Override
public void visitPacket(ChatMessagePacket packet) {
StringBuilder sb = new StringBuilder();
String time = packet.getTime().toString();
sb.append(time, 11, 19); // We only take the HH:MM:SS part
sb.append(" ");
sb.append(packet.getChatter());
sb.append(" : ");
sb.append(packet.getContent());
System.out.println(sb);
this.callback.handleChatMessage(packet.getTime(), packet.getChatter(), packet.getContent());
}
@Override
public void visitPacket(RoomListPacket packet) {
this.callback.handleRoomList(packet.getRoomNames());
}
@Override
public void visitPacket(ServerResponsePacket packet) {
this.callback.handleServerResponse(packet.getResponse());
if (packet.getResponse() == Response.AuthSuccess)
this.callback.handleConnect();
}
@Override
public void visitPacket(ActualRoomPacket packet) {
this.callback.handleActualRoom(packet.getRoomName());
}
@Override
public void visitPacket(DisconnectPacket packet) {
this.close();
this.callback.handleDisconnect();
}
public void changeCallback(ClientListener callback) {
this.callback = callback;
}
@Override
public void visitPacket(RequestActualRoomPacket packet) {
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
@@ -82,24 +110,8 @@ public class ClientConnexion implements PacketVisitor, PacketHandler{
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(RoomListPacket packet) {
// System.out.println("Handled room list !");
// throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
System.out.println("Rooms :");
for (String room : packet.getRoomNames()) {
System.out.println("\t" + room);
}
}
@Override
public void visitPacket(SendChatMessagePacket packet) {
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(ServerResponsePacket packet) {
System.out.println(packet.getResponse());
}
}

View File

@@ -0,0 +1,187 @@
package client;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.function.Consumer;
import network.protocol.ANSIColor;
import network.protocol.packets.ServerResponsePacket;
import network.protocol.packets.ServerResponsePacket.Response;
import utilities.Signal;
public class ClientConsole implements ClientListener {
private Client client;
private final Thread inputThread;
private final Scanner scanner;
public final Signal onConnect = new Signal();
public final Signal onDisconnect = new Signal();
public ClientConsole(InetSocketAddress address) {
this.inputThread = new Thread(this::inputLoop);
this.scanner = new Scanner(System.in);
String pseudo = inputPseudo();
try {
this.client = new Client(address, this, pseudo);
this.inputThread.start();
} catch (SocketException e) {
e.printStackTrace();
}
}
private String inputPseudo() {
System.out.println("Enter your pseudo:");
String pseudo = "chatter";
pseudo = this.scanner.nextLine();
return pseudo;
}
public Client getClientInterface() {
return this.client;
}
private void inputLoop() {
while (!Thread.interrupted()) {
String message = this.scanner.nextLine();
if (Thread.interrupted())
break;
visitMessage(message);
}
}
public void joinThread() throws InterruptedException {
this.inputThread.join();
}
private void printHelp(Map<List<String>, Consumer<String>> commands) {
System.out.println("Available commands:");
for (var entry : commands.entrySet()) {
List<String> commandNames = entry.getKey();
System.out.println("\t" + commandNames.get(0) + (commandNames.size() == 2 ? " or " + commandNames.get(1) : ""));
}
// the clearLine eats the last line if a new line is not skipped
System.out.println("\n");
}
private void visitMessage(String message) {
if (!message.startsWith("/")) {
this.client.SendChatMessage(message);
clearLine();
return;
}
final Map<List<String>, Consumer<String>> commands = Map.of(
List.of("/createRoom <roomName>", "/create <roomName>"), (args) -> this.client.SendCreateRoom(args),
List.of("/listRooms", "/list"), (args) -> this.client.RequestRoomList(),
List.of("/joinRoom <roomName>", "/join <roomName>"), (args) -> this.client.SendJoinRoom(args),
List.of("/leaveRoom", "/leave"), (args) -> this.client.SendLeaveRoom(),
List.of("/actualRoom", "/room"), (args) -> this.client.RequestActualRoom(),
List.of("/goodbye", "/bye"), (args) -> this.client.close(),
List.of("/help"), (args) -> {});
boolean commandFound = false;
for (var entry : commands.entrySet()) {
List<String> commandNames = entry.getKey();
Consumer<String> runnable = entry.getValue();
for (String cmd : commandNames) {
String[] cmdParts = cmd.split(" ");
if (cmdParts.length > 1) {
// the command expects arguments
if (message.startsWith(cmdParts[0] + " ")) {
runnable.accept(message.substring(cmdParts[0].length() + 1).trim());
commandFound = true;
}
} else {
// the command does not expect arguments
if (message.equals(cmd)) {
runnable.accept(message.substring(cmd.length()).trim());
commandFound = true;
}
}
}
if (commandFound)
break;
}
if (commandFound) {
if (message.equals("/help"))
printHelp(commands);
clearLine();
return;
}
System.out.println(ANSIColor.formatString("&rUnknown command&n"));
printHelp(commands);
clearLine();
}
private void clearLine() {
System.out.print("\033[1A\r\033[2K"); // weird sequence to clear the line (but it works !)
System.out.flush();
}
private void stop() {
this.inputThread.interrupt();
}
@Override
public void handleDisconnect() {
System.out.println("Disconnected !");
stop();
this.onDisconnect.emit();
}
@Override
public void handleConnexionError() {
System.out.println("An error occured during the connexion !");
stop();
}
@Override
public void handleChatMessage(Instant time, String chatter, String content) {
StringBuilder sb = new StringBuilder();
String strTime = time.toString();
sb.append("&y[")
.append(strTime, 11, 19) // We only take the HH:MM:SS part
.append("]&n")
.append(" ")
.append(chatter)
.append(" : ")
.append(content).append("&n"); // make the color back to normal at the end of every message
System.out.println(ANSIColor.formatString(sb.toString()));
}
@Override
public void handleRoomList(List<String> roomNames) {
System.out.println(roomNames.isEmpty()
? "No rooms available"
: "Rooms : \n\t" + String.join("\n\t", roomNames));
}
@Override
public void handleActualRoom(String roomName) {
System.out.println(roomName != null ? ANSIColor.formatString("You are now in room &b" + roomName + "&n") : "You are not in a room");
}
@Override
public void handleServerResponse(Response response) {
if (response == ServerResponsePacket.Response.MessageSent
|| response == ServerResponsePacket.Response.MessageNotSent) {
return;
}
System.out.println(response);
}
@Override
public void handleConnect() {
System.out.println("Connected to server !");
this.onConnect.emit();
}
}

View File

@@ -0,0 +1,36 @@
package client;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Screen;
import javafx.stage.Stage;
import java.util.Objects;
public class ClientGui extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/client/clientLogin.fxml"));
Scene scene = new Scene(loader.load(), 400, 240);
double screenWidth = Screen.getPrimary().getVisualBounds().getWidth();
double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
double xPos = screenWidth / 2 - scene.getWidth() / 2;
double yPos = screenHeight / 2 - scene.getHeight() / 2;
scene.getStylesheets().add(getClass().getResource("clientStyle.css").toExternalForm());
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/liscord.png"))));
stage.setTitle("Liscord");
stage.setScene(scene);
stage.setX(xPos);
stage.setY(yPos);
stage.show();
}
}

View File

@@ -0,0 +1,283 @@
package client;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import javafx.util.Duration;
import network.protocol.packets.ServerResponsePacket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ClientGuiController implements ClientListener {
private Client client;
private boolean connected = true;
@FXML
private BorderPane vueContainer;
@FXML
private VBox roomList;
@FXML
private VBox chatList;
@FXML
private HBox chatInput;
@FXML
private ScrollPane chatPane;
@FXML
private Label roomName;
public void setClient(Client client) {
this.client = client;
}
@FXML
public void initialize() throws SocketException {
client = new Client(new InetSocketAddress("192.168.163.131", 6665), this, UsernameSingleton.getInstance().getUsername());
Platform.runLater(() -> {
Stage stage = (Stage) vueContainer.getScene().getWindow();
stage.setResizable(true); // Fixed a bug that made the close button disappear under my Wayland setup (don't know if it's the same for x11)
stage.setOnCloseRequest(event -> {
connected = false;
client.close();
});
chatList.heightProperty().addListener((obs, oldVal, newVal) -> chatPane.setVvalue(1.0));
});
requestRoomsRegularly();
}
/**
* Request the list of rooms from the server every second.
*/
private void requestRoomsRegularly() {
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(10), event -> client.RequestRoomList()));
timeline.setCycleCount(Timeline.INDEFINITE);
timeline.play();
}
@Override
public void handleDisconnect() {
System.out.println("Disconnected");
}
/**
* Create an alert dialog when the connection to the server fails.
*/
@Override
public void handleConnexionError() {
if(connected) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("Connection");
alert.setHeaderText("Connection failed");
Optional<ButtonType> res = alert.showAndWait();
if (res.isPresent() && res.get() == ButtonType.OK) {
((Stage) vueContainer.getScene().getWindow()).close();
}
});
}
}
/**
* Create an alert dialog when the connection to the server is successful.
*/
@Override
public void handleConnect() {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Connection");
alert.setHeaderText("Connection to the server successful");
alert.showAndWait();
});
}
/**
* Format and insert the received chat message into the chat list.
* @param time the time the message was sent
* @param chatter the name of the person who sent the message
* @param content the content of the message
*/
@Override
public void handleChatMessage(Instant time, String chatter, String content) {
String untagged = untag(content);
String messageColor = Objects.equals(content, untagged) ? "lightblue" : "lightgreen";
Platform.runLater(() -> {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm")
.withZone(ZoneId.systemDefault());
String timeString = formatter.format(time);
Text chatterText = new Text(chatter + ": ");
chatterText.setStyle("-fx-fill: black; -fx-font-weight: bold;");
TextFlow messageText = formatMessage(untagged);
TextFlow wholeMessage = new TextFlow(chatterText, messageText);
Text timeText = new Text(" " + timeString);
timeText.setStyle("-fx-fill: gray; -fx-font-size: 10px;");
HBox messageContainer = new HBox(5, wholeMessage, timeText);
messageContainer.setStyle("-fx-background-color: " + messageColor + "; -fx-padding: 8; -fx-background-radius: 5;");
messageContainer.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(messageText, Priority.ALWAYS);
chatList.getChildren().add(messageContainer);
});
}
/**
* Remove the tag from the message (handle ANSI characters encoded by the server)
* @param message the message to untag
* @return the untagged message
*/
public String untag(String message) {
Pattern pattern = Pattern.compile("\u001B\\[44;30m(.*?)\u001B\\[49;39m");
Matcher matcher = pattern.matcher(message);
StringBuilder result = new StringBuilder();
while (matcher.find()) {
String taggedName = matcher.group(1);
matcher.appendReplacement(result, taggedName);
}
matcher.appendTail(result);
return result.toString();
}
/**
* Display the list of rooms in the room list.
* @param roomNames the list of room names
*/
@Override
public void handleRoomList(List<String> roomNames) {
Platform.runLater(() -> {
roomList.getChildren().clear();
for (String roomName : roomNames) {
Button button = new Button(roomName);
button.setOnAction(event -> {
client.SendJoinRoom(roomName);
createChatEnv(roomName);
});
roomList.getChildren().add(button);
}
});
}
/**
* Create the chat (message wall + input) environment.
* @param roomName the name of the room
*/
private void createChatEnv(String roomName) {
Platform.runLater(() -> {
this.roomName.setText("Room: " + roomName);
chatList.getChildren().clear();
chatInput.getChildren().clear();
Button leaveButton = new Button("Leave room");
leaveButton.setOnAction(event -> {
client.SendLeaveRoom();
this.roomName.setText("");
chatList.getChildren().clear();
chatInput.getChildren().clear();
});
chatList.getChildren().add(leaveButton);
TextField messageInput = new TextField();
messageInput.setPromptText("Type a message...");
messageInput.getStyleClass().add("message-input");
HBox.setHgrow(messageInput, Priority.ALWAYS);
messageInput.onActionProperty().set(event -> {
client.SendChatMessage(messageInput.getText());
messageInput.clear();
});
chatInput.getChildren().add(messageInput);
Button sendButton = new Button("Send");
sendButton.getStyleClass().add("send-button");
sendButton.setOnAction(event -> {
client.SendChatMessage(messageInput.getText());
messageInput.clear();
});
chatInput.getChildren().add(sendButton);
});
}
/**
* Colorize the message according to the color codes.
* @param content the content of the message
* @return the formatted message
*/
private TextFlow formatMessage(String content) {
TextFlow textFlow = new TextFlow();
// Evil regex : match every sequence starting with a color code ending with the next color code
Pattern pattern = Pattern.compile("&([rbgyn])([^&]*)");
Matcher matcher = pattern.matcher(content);
int lastIndex = 0;
while (matcher.find()) {
if (matcher.start() > lastIndex) {
textFlow.getChildren().add(new Text(content.substring(lastIndex, matcher.start())));
}
String colorCode = matcher.group(1);
String textPart = matcher.group(2);
String color = switch (colorCode) {
case "r" -> "red";
case "b" -> "blue";
case "g" -> "green";
case "y" -> "gray";
default -> "black";
};
Text coloredText = new Text(textPart);
coloredText.setStyle("-fx-fill: " + color + ";");
textFlow.getChildren().add(coloredText);
lastIndex = matcher.end();
}
// Add the remaining text (works if there's no color code at all)
if (lastIndex < content.length()) {
textFlow.getChildren().add(new Text(content.substring(lastIndex)));
}
return textFlow;
}
@Override
public void handleServerResponse(ServerResponsePacket.Response response) {
}
@Override
public void handleActualRoom(String roomName) {
}
public void createRoom() {
TextInputDialog dialog = new TextInputDialog();
dialog.setTitle("Create a room");
dialog.setHeaderText("Enter the name of the room");
dialog.setContentText("Room name:");
Optional<String> result = dialog.showAndWait();
result.ifPresent(name -> {
client.SendCreateRoom(name);
createChatEnv(name);
});
}
}

View File

@@ -0,0 +1,18 @@
package client;
import java.time.Instant;
import java.util.List;
import network.protocol.packets.ServerResponsePacket;
public interface ClientListener {
void handleDisconnect();
void handleConnexionError();
void handleConnect();
void handleChatMessage(Instant time, String chatter, String content);
void handleRoomList(List<String> roomNames);
void handleServerResponse(ServerResponsePacket.Response response);
void handleActualRoom(String roomName);
}

View File

@@ -0,0 +1,55 @@
package client;
import javafx.animation.PauseTransition;
import javafx.animation.RotateTransition;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.shape.Arc;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.io.IOException;
import static utilities.FxUtilities.centerStage;
public class ClientLoading {
@FXML
private Arc spinnerArc;
/**
* Initialize the loading screen.
*/
public void initialize() {
// Rotation animation cycle
RotateTransition rotate = new RotateTransition(Duration.seconds(1), spinnerArc);
rotate.setByAngle(360);
rotate.setCycleCount(RotateTransition.INDEFINITE);
rotate.play();
// Yes, this loading screen is just for show. But it's fun and pretty.
// PauseTransition allows no to block the rotation animation.
PauseTransition pause = new PauseTransition(Duration.millis(1000));
pause.setOnFinished(event -> {
try {
login();
} catch (IOException e) {
e.printStackTrace();
}
});
pause.play();
}
/**
* Load the client GUI and switch to it.
* @throws IOException if the FXML file is not found.
*/
public void login() throws IOException {
var loader = new FXMLLoader(getClass().getResource("/client/clientVue.fxml"));
loader.load();
Stage stage = (Stage) spinnerArc.getScene().getWindow();
stage.setScene(new Scene(loader.getRoot(), 800, 600));
centerStage(stage);
stage.getScene().getStylesheets().add(getClass().getResource("clientStyle.css").toExternalForm());
}
}

View File

@@ -0,0 +1,33 @@
package client;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import java.io.IOException;
import static utilities.FxUtilities.centerStage;
public class ClientLogin {
@FXML
public TextField usernameField;
/**
* Sets the username and switches to the loading screen.
* @throws IOException if the FXML file is not found.
*/
@FXML
private void login() throws IOException {
String username = usernameField.getText();
if(username.isEmpty()) {
return;
}
UsernameSingleton.getInstance().setUsername(username);
var loader = new FXMLLoader(getClass().getResource("/client/clientLoading.fxml"));
loader.load();
Stage stage = (Stage) usernameField.getScene().getWindow();
stage.setScene(new Scene(loader.getRoot(), 800, 600));
centerStage(stage);
}
}

View File

@@ -0,0 +1,31 @@
package client;
/**
* This class aims to make the username available to all controllers. (Recommended by JavaFX, because otherwise, FXMLLoader bugs and doesn't switch scenes properly.)
*/
public class UsernameSingleton {
private static UsernameSingleton instance;
private String username;
private UsernameSingleton() {
}
/**
* Get the instance of the singleton.
* @return the instance
*/
public static UsernameSingleton getInstance() {
if (instance == null) {
instance = new UsernameSingleton();
}
return instance;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

View File

@@ -0,0 +1,35 @@
package network;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.net.InetAddress;
public class IPAddressFinder {
public static String findIPAddress(){
try {
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface networkInterface = networkInterfaces.nextElement();
Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
while (inetAddresses.hasMoreElements()) {
InetAddress inetAddress = inetAddresses.nextElement();
if (!inetAddress.isLoopbackAddress()) {
String ip = inetAddress.getHostAddress();
if (ip.startsWith("192.168")) {
return ip;
}
}
}
}
} catch (SocketException e) {
e.printStackTrace();
}
return "Not Found";
}
}

View File

@@ -0,0 +1,356 @@
package network;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Stack;
import network.protocol.Packet;
import network.protocol.packets.AcknowlegdementPacket;
import network.protocol.packets.DisconnectPacket;
public class PacketPool {
private final Stack<ReliablePacketAddress> packetQueue;
private final Map<InetSocketAddress, AdressContext> addressContexts;
private final Socket socket;
private static int MAX_SEND_TRY = 50;
private static long SEND_DELAY = 10;
private static long RETRY_INTERVAL = 100;
private static float PACKET_LOSS_PROBABILITY = 0.1f;
private static record ReliablePacketAddress(ReliablePacket packet, InetSocketAddress address) {
@Override
public final int hashCode() {
return Objects.hash(packet, address);
}
@Override
public final boolean equals(Object arg0) {
if (arg0 instanceof ReliablePacketAddress packetAddress)
return packetAddress.address().equals(this.address())
&& packetAddress.packet().getSeq() == this.packet().getSeq();
return false;
}
}
/**
* Data specific to one connexion
*/
private static class AdressContext {
public int currentSeqRecv = 0;
public int currentSeqSend = 0;
public final List<Thread> sendThreads = new ArrayList<>();
public final List<ReliablePacket> packetRecvBuffer = new ArrayList<>();
public final Map<ReliablePacket, Integer> packetsSentTries = new HashMap<>();
}
/**
* @param address the address to send to
* @return The next send sequence
*/
private int getNextSeqSend(InetSocketAddress address) {
return this.addressContexts.get(address).currentSeqSend++;
}
/**
* @param address the address which a packet was recieved
* @return the current recieve sequence number
*/
private int getCurrentSeqRecv(InetSocketAddress address) {
return this.addressContexts.get(address).currentSeqRecv;
}
/**
* Set the recieve sequence number
*
* @param address
* @param newValue
*/
private void setSeqRecv(InetSocketAddress address, int newValue) {
this.addressContexts.get(address).currentSeqRecv = newValue;
}
/**
* Try to add the address into memory
*
* @param address
*/
private void tryAddContext(InetSocketAddress address) {
this.addressContexts.putIfAbsent(address, new AdressContext());
}
/**
* Construct a PacketPool
*
* @param socket
*/
public PacketPool(Socket socket) {
this.socket = socket;
this.packetQueue = new Stack<>();
this.addressContexts = new HashMap<>();
}
private void debugPrint(String msg) {
// System.out.println(msg);
}
private void debugSend(ReliablePacket packet, InetSocketAddress address) {
boolean client = address.getPort() == 6665;
debugPrint((client ? "[Client]" : "[Server]") + " Sent " + packet.getPacket().getClass().getName()
+ " with seq : " + packet.getSeq());
}
private void debugRecv(ReliablePacket packet, InetSocketAddress address) {
boolean client = address.getPort() == 6665;
debugPrint((client ? "[Client]" : "[Server]") + " Received " + packet.getPacket().getClass().getName()
+ " with seq : " + packet.getSeq());
}
/**
* Send a packet to the socket
*
* @param packet
* @param address
* @throws IOException
*/
private void sendPacketToSocket(ReliablePacket packet, InetSocketAddress address) throws IOException {
var packetsSentTries = this.addressContexts.get(address).packetsSentTries;
new Thread(() -> {
// try {
// Thread.sleep(SEND_DELAY);
// if (Math.random() > PACKET_LOSS_PROBABILITY)
try {
this.socket.sendPacket(packet, address);
} catch (IOException e) {
// e.printStackTrace();
}
// } catch (InterruptedException | IOException e) {
// // e.printStackTrace();
// }
}).start();
if (packet.getPacket() instanceof AcknowlegdementPacket)
return;
Integer count = packetsSentTries.get(packet);
if (count == null) {
packetsSentTries.put(packet, 1);
} else {
packetsSentTries.put(packet, count + 1);
}
}
/**
* Send a packet to socket and try to resend if an acknowlegment was not
* recieved
*
* @param packet
* @param address
* @throws IOException
*/
private synchronized void sendPacket(ReliablePacket packet, InetSocketAddress address) throws IOException {
sendPacketToSocket(packet, address);
debugSend(packet, address);
ReliablePacketAddress reliablePacketAddress = new ReliablePacketAddress(packet, address);
if (!(packet.getPacket() instanceof AcknowlegdementPacket)) {
Thread newThread = new Thread(() -> tryResend(reliablePacketAddress));
this.addressContexts.get(address).sendThreads.add(newThread);
newThread.start();
}
}
/**
* Send a packet (and encapsulate it)
*
* @param packet
* @param address
* @throws IOException
*/
public synchronized void sendPacket(Packet packet, InetSocketAddress address) throws IOException {
tryAddContext(address);
ReliablePacket reliablePacket = new ReliablePacket(packet, getNextSeqSend(address));
sendPacket(reliablePacket, address);
}
/**
* Try to resend a packet
*
* @param reliablePacketAddress
*/
private void tryResend(ReliablePacketAddress reliablePacketAddress) {
try {
while (!Thread.interrupted()) {
Thread.sleep(RETRY_INTERVAL);
var packetsSentTries = this.addressContexts.get(reliablePacketAddress.address()).packetsSentTries;
// the packet has been received
if (!packetsSentTries.containsKey(reliablePacketAddress.packet()))
break;
Integer sendCount = packetsSentTries.get(reliablePacketAddress.packet());
if (sendCount > MAX_SEND_TRY) {
close(reliablePacketAddress.address());
debugPrint(
"Packet" + reliablePacketAddress.packet() + " not send after " + MAX_SEND_TRY + " tries");
// simulating a fake disconnect packet
this.socket.handlePacket(new DisconnectPacket("Timed out"), reliablePacketAddress.address());
break;
}
boolean client = reliablePacketAddress.address().getPort() == 6665;
debugPrint((client ? "[Client]" : "[Server]") + " Trying to resend the packet "
+ reliablePacketAddress.packet().getSeq() + " ...");
sendPacketToSocket(reliablePacketAddress.packet(), reliablePacketAddress.address());
}
} catch (InterruptedException e) {
// e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
AdressContext ctx = this.addressContexts.get(reliablePacketAddress.address());
if (ctx != null)
ctx.sendThreads.remove(Thread.currentThread());
}
/**
* @param address
* @return The smallest sequence number recieved
*/
private ReliablePacket getMinimumSeqReceived(InetSocketAddress address) {
List<ReliablePacket> packetRecvBuffer = this.addressContexts.get(address).packetRecvBuffer;
if (packetRecvBuffer.isEmpty())
return null;
return Collections.min(packetRecvBuffer, (rel1, rel2) -> {
return Integer.compare(rel1.getSeq(), rel2.getSeq());
});
}
/**
* Move packets in buffer to the packet recieved queue to be processed by the
* app
*
* @param address
* @return the sequence number of the last packet that was added to the queue
*/
private int fillPacketQueue(InetSocketAddress address) {
List<ReliablePacket> packetRecvBuffer = this.addressContexts.get(address).packetRecvBuffer;
ReliablePacket minimum = getMinimumSeqReceived(address);
int lastSeqProcessed = -1;
while (true) {
this.packetQueue.add(new ReliablePacketAddress(minimum, address));
packetRecvBuffer.remove(minimum);
lastSeqProcessed = minimum.getSeq();
ReliablePacket nextMinimum = getMinimumSeqReceived(address);
if (nextMinimum == null || nextMinimum.getSeq() != minimum.getSeq() + 1)
break;
minimum = nextMinimum;
}
Collections.reverse(this.packetQueue);
return lastSeqProcessed;
}
/**
* Process packet when recieved
*
* @param packet
* @param address
* @throws IOException
*/
public void onPacketReceived(ReliablePacket packet, InetSocketAddress address) throws IOException {
tryAddContext(address);
var packetsSentTries = this.addressContexts.get(address).packetsSentTries;
if (packet.getPacket() instanceof AcknowlegdementPacket ackPacket) {
assert (ackPacket.getAck() != -1);
for (var entry : packetsSentTries.entrySet()) {
ReliablePacket reliablePacket = entry.getKey();
if (entry.getKey().getSeq() == ackPacket.getAck()) {
packetsSentTries.remove(reliablePacket);
break;
}
}
return;
}
if (this.addressContexts.get(address).packetRecvBuffer.contains(packet)) {
debugPrint("The packet has already been received !");
sendPacketToSocket(
new ReliablePacket(new AcknowlegdementPacket(packet.getSeq()), -1), address);
return;
}
if (packet.getSeq() < getCurrentSeqRecv(address)) {
debugPrint("Packet too old, current : " + getCurrentSeqRecv(address));
sendPacketToSocket(
new ReliablePacket(new AcknowlegdementPacket(packet.getSeq()), -1), address);
return;
}
this.addressContexts.get(address).packetRecvBuffer.add(packet);
debugRecv(packet, address);
sendPacketToSocket(
new ReliablePacket(new AcknowlegdementPacket(packet.getSeq()), -1), address);
// got the packet in the right order
if (packet.getSeq() == getCurrentSeqRecv(address)) {
setSeqRecv(address, fillPacketQueue(address) + 1);
}
}
/**
* @return The next packet in the queue
*/
public Entry<Packet, InetSocketAddress> getNextPacket() {
if (this.packetQueue.isEmpty())
return null;
ReliablePacketAddress last = this.packetQueue.pop();
var entry = Map.entry(last.packet().getPacket(), last.address);
return entry;
}
/**
* Closes the connexion
*
* @param adress
*/
private void close(InetSocketAddress adress) {
var ctx = this.addressContexts.get(adress);
if (ctx != null)
close(ctx);
}
/**
* Stop the threads of the connexion
*
* @param adressContext
*/
private void close(AdressContext adressContext) {
for (Thread thread : adressContext.sendThreads) {
thread.interrupt();
}
}
/**
* Stop the PacketPool
*/
public void close() {
for (AdressContext adressContext : this.addressContexts.values()) {
close(adressContext);
}
}
}

View File

@@ -0,0 +1,38 @@
package network;
import java.io.Serializable;
import network.protocol.Packet;
public class ReliablePacket implements Serializable {
private final Packet packet;
private final int seq;
public ReliablePacket(Packet packet, int seq) {
this.packet = packet;
this.seq = seq;
}
/**
* @return The encapsulated packet
*/
public Packet getPacket() {
return packet;
}
/**
* @return The sequence number of this packet
*/
public int getSeq() {
return seq;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ReliablePacket packet)
return packet.getSeq() == this.getSeq();
return false;
}
}

View File

@@ -0,0 +1,158 @@
package network;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.List;
import network.protocol.Packet;
import utilities.Signal;
public class Socket {
private final DatagramSocket socket;
private final List<PacketHandler> handlers;
private final Thread readThread;
private final PacketPool packetPool;
public final Signal onClose = new Signal();
/**
* Construct a UDP Socket to connect to a server
* @throws SocketException
*/
public Socket() throws SocketException {
this.socket = new DatagramSocket();
this.handlers = new ArrayList<>();
this.packetPool = new PacketPool(this);
this.readThread = new Thread(this::readLoop);
this.readThread.start();
}
/**
* Construct a UDP Socket to listen to connexion at the specified port
* @param listeningPort the port to listen to
* @throws SocketException
*/
public Socket(int listeningPort) throws SocketException {
this.socket = new DatagramSocket(listeningPort);
this.handlers = new ArrayList<>();
this.packetPool = new PacketPool(this);
this.readThread = new Thread(this::readLoop);
this.readThread.start();
}
public void addHandler(PacketHandler handler) {
this.handlers.add(handler);
}
/**
* @return this.socket.getLocalPort()
*/
public int getLocalPort() {
return this.socket.getLocalPort();
}
// needs to be accessible by PacketPool
/**
* Send a packet to the specified address
* @param packet the packet to send
* @param address the address to send to
* @throws IOException
*/
void sendPacket(ReliablePacket packet, InetSocketAddress address) throws IOException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(stream);
oos.writeObject(packet);
oos.flush();
byte[] data = stream.toByteArray();
DatagramPacket dataPacket = new DatagramPacket(data, data.length, address.getAddress(),
address.getPort());
this.socket.send(dataPacket);
}
/**
* Try to recieve packets and send them to the PacketPool.
* This method blocks until something has been read
* @throws IOException
* @throws ClassNotFoundException
*/
private void recievePacketReliable()
throws IOException, ClassNotFoundException {
byte[] buffer = new byte[65535];
DatagramPacket dataPacket = new DatagramPacket(buffer, buffer.length);
socket.receive(dataPacket);
InetSocketAddress address = new InetSocketAddress(dataPacket.getAddress(), dataPacket.getPort());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(dataPacket.getData()));
ReliablePacket packet = (ReliablePacket) ois.readObject();
this.packetPool.onPacketReceived(packet, address);
}
/**
* Send a packet to the specified address
* @param packet the packet to send
* @param address the address to send to
* @throws IOException
*/
public void sendPacket(Packet packet, InetSocketAddress address) throws IOException {
this.packetPool.sendPacket(packet, address);
}
/**
* Recieve packet in a reliable way (packets are in the right order).
* This method is blocking
* @throws IOException
* @throws ClassNotFoundException
*/
private void recievePacket() throws IOException, ClassNotFoundException {
this.recievePacketReliable();
var entry = this.packetPool.getNextPacket();
while (entry != null) {
this.handlePacket(entry.getKey(), entry.getValue());
entry = this.packetPool.getNextPacket();
}
}
/**
* Close the socket
*/
public void close() {
this.socket.close();
this.packetPool.close();
this.readThread.interrupt();
this.onClose.emit();
}
/**
* Dispatch the packet
* @param packet the packet to dispatch
* @param address the incoming address
*/
void handlePacket(Packet packet, InetSocketAddress address) {
for (PacketHandler handler : this.handlers) {
handler.handlePacket(packet, address);
}
}
/**
* Loop to read all packets
*/
private void readLoop() {
try {
while (!Thread.interrupted()) {
recievePacket();
}
} catch (IOException | ClassNotFoundException e) {
// e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,24 @@
package network.protocol;
import java.util.regex.Pattern;
public class ANSIColor {
public static final String RESET = "\u001B[0m";
public static final String BLACK = "\u001B[30m";
public static final String RED = "\u001B[31m";
public static final String GREEN = "\u001B[32m";
public static final String BLUE = "\u001B[34m";
public static final String GREY = "\u001B[37m";
public static String formatString(String message){
return message.replace("&r", RED)
.replace("&g", GREEN)
.replace("&b", BLUE)
.replace("&y", GREY)
.replace("&n", RESET);
}
public static String tag(String message, String chatter){
return message.replaceAll("(@" + Pattern.quote(chatter) + ")", "\u001B[44;30m$1\u001B[49;39m" );
}
}

View File

@@ -4,6 +4,10 @@ import java.io.Serializable;
public abstract class Packet implements Serializable {
/**
* Used for double dispatch
* @param packetVisitor the visitor to dispatch to
*/
public abstract void accept(PacketVisitor packetVisitor);
}

View File

@@ -1,23 +1,20 @@
package network.protocol;
import network.protocol.packets.ChatMessagePacket;
import network.protocol.packets.CreateRoomPacket;
import network.protocol.packets.JoinRoomPacket;
import network.protocol.packets.LeaveRoomPacket;
import network.protocol.packets.LoginPacket;
import network.protocol.packets.RequestRoomListPacket;
import network.protocol.packets.RoomListPacket;
import network.protocol.packets.SendChatMessagePacket;
import network.protocol.packets.ServerResponsePacket;
import network.protocol.packets.*;
public interface PacketVisitor {
/**
* Use double dispatch to process a packet
* @param packet the packet to process
*/
default void visit(Packet packet) {
packet.accept(this);
}
void visitPacket(ChatMessagePacket packet);
void visitPacket(CreateRoomPacket packet);
void visitPacket(DisconnectPacket packet);
void visitPacket(JoinRoomPacket packet);
void visitPacket(LeaveRoomPacket packet);
void visitPacket(LoginPacket packet);
@@ -25,5 +22,7 @@ public interface PacketVisitor {
void visitPacket(RoomListPacket packet);
void visitPacket(SendChatMessagePacket packet);
void visitPacket(ServerResponsePacket packet);
void visitPacket(RequestActualRoomPacket packet);
void visitPacket(ActualRoomPacket packet);
}

View File

@@ -0,0 +1,23 @@
package network.protocol.packets;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
public class AcknowlegdementPacket extends Packet {
private final int ack;
public AcknowlegdementPacket(int ack) {
this.ack = ack;
}
@Override
public void accept(PacketVisitor packetVisitor) {
// this packet should not be handled by the app
}
public int getAck() {
return ack;
}
}

View File

@@ -0,0 +1,21 @@
package network.protocol.packets;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
public class ActualRoomPacket extends Packet {
private final String roomName;
public ActualRoomPacket(String roomName) {
this.roomName = roomName;
}
public String getRoomName() {
return roomName;
}
@Override
public void accept(PacketVisitor packetVisitor) {
packetVisitor.visitPacket(this);
}
}

View File

@@ -0,0 +1,23 @@
package network.protocol.packets;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
public class DisconnectPacket extends Packet {
private final String reason;
public DisconnectPacket(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
@Override
public void accept(PacketVisitor packetVisitor) {
packetVisitor.visitPacket(this);
}
}

View File

@@ -0,0 +1,14 @@
package network.protocol.packets;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
public class RequestActualRoomPacket extends Packet {
public RequestActualRoomPacket() {
}
@Override
public void accept(PacketVisitor packetVisitor) {
packetVisitor.visitPacket(this);
}
}

View File

@@ -1,19 +1,19 @@
package network.protocol.packets;
import java.util.ArrayList;
import java.util.List;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
public class RoomListPacket extends Packet {
private final ArrayList<String> roomNames;
private final List<String> roomNames;
public RoomListPacket(ArrayList<String> roomNames) {
public RoomListPacket(List<String> roomNames) {
this.roomNames = roomNames;
}
public ArrayList<String> getRoomNames() {
public List<String> getRoomNames() {
return roomNames;
}

View File

@@ -0,0 +1,153 @@
package server;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import network.PacketHandler;
import network.Socket;
import network.protocol.ANSIColor;
import network.protocol.Packet;
import network.protocol.packets.ChatMessagePacket;
import network.protocol.packets.SendChatMessagePacket;
public class Server implements PacketHandler {
private final Socket serverSocket;
private final Map<InetSocketAddress, ServerConnexion> connexions;
private final Map<InetSocketAddress, Instant> connexionTimes;
private final Map<String, ArrayList<ServerConnexion>> rooms;
public Server(int port) throws SocketException {
this.serverSocket = new Socket(port);
this.serverSocket.addHandler(this);
this.connexions = new HashMap<>();
this.connexionTimes = new HashMap<>();
this.rooms = new HashMap<>();
}
public ArrayList<String> getRoomNames() {
return rooms.keySet().stream().collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
public boolean isInRoom(ServerConnexion connexion) {
return getRoomName(connexion) != null;
}
public String getRoomName(ServerConnexion connexion) {
for (Map.Entry<String, ArrayList<ServerConnexion>> entry : rooms.entrySet()) {
if (entry.getValue().contains(connexion)) {
return entry.getKey();
}
}
return null;
}
public boolean createRoom(String roomName, ServerConnexion connexion) {
if (rooms.containsKey(roomName)) {
return false;
}
rooms.put(roomName, new ArrayList<>());
leaveRoom(connexion); // Leave the current room (auto handle if not in a room)
rooms.get(roomName).add(connexion); // Add the creator to the room
return true;
}
public boolean leaveRoom(ServerConnexion connexion) {
String roomName = getRoomName(connexion);
if (roomName != null) {
rooms.get(roomName).remove(connexion);
// Remove the room if it is empty
if (rooms.get(roomName).isEmpty()) {
rooms.remove(roomName);
}
return true;
}
return false;
}
public boolean joinRoom(String roomName, ServerConnexion connexion) {
leaveRoom(connexion); // Leave the current room (auto handle if not in a room)
if (rooms.containsKey(roomName) && !rooms.get(roomName).contains(connexion)) {
rooms.get(roomName).add(connexion);
return true;
}
return false;
}
public boolean sendToRoom(String roomName, ChatMessagePacket packet) {
if (roomName != null && rooms.containsKey(roomName)) {
rooms.get(roomName).forEach(con -> con.sendPacket(
new ChatMessagePacket(packet.getTime(), packet.getChatter(), ANSIColor.tag(packet.getContent(), con.getChatterName()))
));
return true;
}
return false;
}
public boolean sendToRoom(ServerConnexion connexion, ChatMessagePacket packet) {
String roomName = getRoomName(connexion);
return sendToRoom(roomName, packet);
}
public boolean sendToRoom(ServerConnexion connexion, SendChatMessagePacket packet) {
return sendToRoom(connexion,
new ChatMessagePacket(Instant.now(), connexion.getChatterName(), packet.getContent()));
}
public void close() {
this.serverSocket.close();
}
public boolean hasChatterName(String pseudo) {
return this.connexions.values().stream()
.anyMatch(connexion -> pseudo.equals(connexion.getChatterName()));
}
void removeConnexion(ServerConnexion connexion) {
for (var it = this.connexions.entrySet().iterator(); it.hasNext();) {
var entry = it.next();
if (entry.getValue() == connexion) {
it.remove();
break;
}
}
for (var entry : this.rooms.entrySet()) {
if (entry.getValue().remove(connexion))
return;
}
}
@Override
public void handlePacket(Packet packet, InetSocketAddress address) {
if (!connexions.containsKey(address)) {
this.connexions.put(address, new ServerConnexion(this, serverSocket, address));
}
this.connexions.get(address).visit(packet);
}
/**
* Avoid the server to spam the chat
* @param address the address of the connexion
* @return true if the connexion is the first received or older than 5 seconds
*/
public boolean handleConnexionTime(InetSocketAddress address) {
if (!connexionTimes.containsKey(address)) {
connexionTimes.put(address, Instant.now());
return true;
}
Instant lastConnexion = connexionTimes.get(address);
if (Instant.now().isAfter(lastConnexion.plusSeconds(5))) {
connexionTimes.put(address, Instant.now());
return true;
}
return false;
}
public int getRunningPort() {
return this.serverSocket.getLocalPort();
}
}

View File

@@ -0,0 +1,206 @@
package server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.time.Instant;
import network.Socket;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
import network.protocol.packets.*;
import network.protocol.packets.ServerResponsePacket.Response;
public class ServerConnexion implements PacketVisitor {
private final Server server;
private final InetSocketAddress clientAddress;
private final Socket socket;
private String chatterName;
public ServerConnexion(Server server, Socket socket, InetSocketAddress clientAddress) {
this.clientAddress = clientAddress;
this.server = server;
this.socket = socket;
}
public String getChatterName() {
return this.chatterName;
}
public void sendPacket(Packet packet) {
try {
this.socket.sendPacket(packet, clientAddress);
} catch (IOException e) {
e.printStackTrace();
this.server.removeConnexion(this);
}
}
/**
* Check if the client is logged in
* @return true if the client is logged in
*/
private boolean checkLogin() {
if (this.chatterName != null && this.chatterName.isEmpty()) {
sendPacket(new ServerResponsePacket(Response.AuthError));
return false;
}
return true;
}
/**
* Make the server create a room & enter it
* @param packet the packet containing the room name
*/
@Override
public void visitPacket(CreateRoomPacket packet) {
if (!checkLogin())
return;
boolean created = server.createRoom(packet.getRoomName(), this);
sendPacket(new ServerResponsePacket(created ? Response.RoomCreated : Response.RoomNotCreated));
if (created)
onRoomJoin();
}
/**
* Make the server join a room
* @param packet the packet containing the room name
*/
@Override
public void visitPacket(JoinRoomPacket packet) {
if (!checkLogin())
return;
if (server.getRoomName(this) != null) {
server.leaveRoom(this);
return;
}
boolean joined = server.joinRoom(packet.getRoomName(), this);
sendPacket(new ServerResponsePacket(joined ? Response.RoomJoined : Response.RoomNotJoined));
if (joined)
onRoomJoin();
}
/**
* Make the server leave a room
* @param packet the packet containing the room name
*/
@Override
public void visitPacket(LeaveRoomPacket packet) {
if (!checkLogin())
return;
String roomName = this.server.getRoomName(this);
boolean left = server.leaveRoom(this);
sendPacket(new ServerResponsePacket(left ? Response.RoomLeft : Response.RoomNotLeft));
if (left)
onRoomLeave(roomName);
}
/**
* Checks if the pseudo is available (nobody connected with the same pseudo)
* @param packet the packet containing the pseudo
*/
@Override
public void visitPacket(LoginPacket packet) {
if (packet.getPseudo().isEmpty() || server.hasChatterName(packet.getPseudo())) {
sendPacket(new ServerResponsePacket(Response.AuthError));
return;
}
this.chatterName = packet.getPseudo();
sendPacket(new ServerResponsePacket(Response.AuthSuccess));
sendPacket(new RoomListPacket(server.getRoomNames()));
System.out.println("[Server] Chatter " + packet.getPseudo() + " connected !");
}
/**
* Get the list of rooms
* @param packet the packet containing the request
*/
@Override
public void visitPacket(RequestRoomListPacket packet) {
if (!checkLogin())
return;
sendPacket(new RoomListPacket(server.getRoomNames()));
}
/**
* Send a message to the room
* @param packet the packet containing the message
*/
@Override
public void visitPacket(SendChatMessagePacket packet) {
if (!checkLogin())
return;
boolean messageSent = server.sendToRoom(this, packet);
sendPacket(new ServerResponsePacket(messageSent ? Response.MessageSent : Response.MessageNotSent));
}
/**
* Disconnect the client
* @param packet the packet containing the request
*/
@Override
public void visitPacket(DisconnectPacket packet) {
this.onDisconnect();
}
/**
* Remove all room connections while disconnecting the client
*/
private void onDisconnect() {
if (this.server.isInRoom(this)) {
this.onRoomLeave(this.server.getRoomName(this));
}
this.server.removeConnexion(this);
if (chatterName != null)
System.out.println("[Server] Chatter " + chatterName + " disconnected !");
}
/**
* Send a message to the room when a client joins
*/
private void onRoomJoin() {
String joinMessage = "Chatter " + this.chatterName + " joined the room !";
this.server.sendToRoom(this, new ChatMessagePacket(Instant.now(), "", joinMessage));
}
/**
* Send a message to the room when a client leaves
* @param roomName the name of the room
*/
private void onRoomLeave(String roomName) {
String joinMessage = "Chatter " + this.chatterName + " left the room !";
this.server.sendToRoom(roomName, new ChatMessagePacket(Instant.now(), "", joinMessage));
}
@Override
public void visitPacket(RoomListPacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(ChatMessagePacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(ServerResponsePacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(RequestActualRoomPacket packet) {
sendPacket(new ActualRoomPacket(server.getRoomName(this)));
}
@Override
public void visitPacket(ActualRoomPacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
}

View File

@@ -0,0 +1,37 @@
package server;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.image.Image;
import javafx.stage.Screen;
import java.io.IOException;
import java.util.Objects;
public class ServerGui extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/server/serverVue.fxml"));
Scene scene = new Scene(loader.load(), 400, 240);
double screenWidth = Screen.getPrimary().getVisualBounds().getWidth();
double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
double xPos = screenWidth / 2 - scene.getWidth() / 2;
double yPos = screenHeight / 2 - scene.getHeight() / 2;
scene.getStylesheets().add(getClass().getResource("serverStyle.css").toExternalForm());
stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/liscord.png"))));
stage.setTitle("Liscord Server");
stage.setScene(scene);
stage.setX(xPos);
stage.setY(yPos);
stage.show();
}
public static void main(String[] args) {
launch();
}
}

View File

@@ -0,0 +1,42 @@
package server;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import network.IPAddressFinder;
import java.net.SocketException;
import java.net.UnknownHostException;
public class ServerGuiController {
@FXML
private Label IPAddress;
@FXML
private Button toggleServerButton;
private Server server;
@FXML
public void initialize() {
IPAddress.setText("IP Address: " + IPAddressFinder.findIPAddress());
}
@FXML
public void toggleServer() throws SocketException {
if(server == null){
toggleServerButton.setDisable(true);
server = new Server(6665);
toggleServerButton.setDisable(false);
toggleServerButton.setText("Stop Server");
System.out.println("Server started on port 6665");
} else {
toggleServerButton.setDisable(true);
server.close();
server = null;
toggleServerButton.setDisable(false);
toggleServerButton.setText("Start Server");
}
}
}

View File

@@ -0,0 +1,24 @@
package utilities;
import java.util.regex.Pattern;
public class ANSIColor {
public static final String RESET = "\u001B[0m";
public static final String BLACK = "\u001B[30m";
public static final String RED = "\u001B[31m";
public static final String GREEN = "\u001B[32m";
public static final String BLUE = "\u001B[34m";
public static final String GREY = "\u001B[37m";
public static String formatString(String message){
return message.replace("&r", RED)
.replace("&g", GREEN)
.replace("&b", BLUE)
.replace("&y", GREY)
.replace("&n", RESET);
}
public static String tag(String message, String chatter){
return message.replaceAll("(@" + Pattern.quote(chatter) + ")", "\u001B[44;30m$1\u001B[49;39m" );
}
}

View File

@@ -0,0 +1,17 @@
package utilities;
import javafx.stage.Screen;
import javafx.stage.Stage;
public class FxUtilities {
public static void centerStage(Stage stage) {
double screenWidth = Screen.getPrimary().getVisualBounds().getWidth();
double screenHeight = Screen.getPrimary().getVisualBounds().getHeight();
double xPos = screenWidth / 2 - stage.getWidth() / 2;
double yPos = screenHeight / 2 - stage.getHeight() / 2;
stage.setX(xPos);
stage.setY(yPos);
}
}

View File

@@ -0,0 +1,38 @@
package utilities;
import java.util.HashSet;
import java.util.Set;
public class Signal {
private final Class<?>[] types;
private final Set<Slot> listeners;
public Signal(Class<?>... types) {
this.types = types;
this.listeners = new HashSet<>();
}
public void connect(Slot listener) {
this.listeners.add(listener);
}
public void clear() {
this.listeners.clear();
}
public void emit(Object... values) {
for (Slot listener : this.listeners) {
if (values.length != types.length) {
throw new UnsupportedOperationException("The number of provided arguments is not right");
}
for (int i = 0; i < values.length; i++) {
if (!types[i].isInstance(values[i])) {
throw new UnsupportedOperationException("Incorrect value at index " + i);
}
}
listener.run(values);
}
}
}

View File

@@ -0,0 +1,5 @@
package utilities;
public interface Slot {
void run(Object ... args);
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.shape.Arc?>
<?import javafx.scene.control.Label?>
<StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="client.ClientLoading" style="-fx-background-color: lightgrey;">
<Arc fx:id="spinnerArc" centerX="50" centerY="50" radiusX="30" radiusY="30"
startAngle="30" length="330" fill="transparent" stroke="blue" strokeWidth="5" onMouseClicked="#login"
/>
</StackPane>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<VBox alignment="CENTER" spacing="10.0"
xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="client.ClientLogin"
prefHeight="400.0" prefWidth="600.0"
styleClass="login-box">
<padding>
<Insets bottom="10.0" left="100.0" right="100.0" top="10.0"/>
</padding>
<Label text="Enter your username"/>
<TextField fx:id="usernameField" onAction="#login" styleClass="login"/>
<Button text="Login" onAction="#login"/>
</VBox>

View File

@@ -0,0 +1,17 @@
.login-box, .logo-box, .rooms-list, .chat-list {
-fx-background-color: lightgray;
}
.login {
-fx-border-color: transparent;
}
.login:focused {
-fx-border-color: lightgrey;
}
.rooms-label {
-fx-font-size: 18px;
-fx-padding: 10px;
-fx-background-color: lightgray;
}

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="client.ClientGuiController"
prefHeight="400.0" prefWidth="600.0" fx:id="vueContainer">
<top>
<HBox alignment="CENTER" styleClass="logo-box">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
<ImageView fitHeight="50.0" fitWidth="50.0">
<Image url="@/liscord.png" />
</ImageView>
<Label text="Liscord" style="-fx-font-size: 24px; -fx-font-weight: bold; -fx-padding: 0 0 0 10px"/>
</HBox>
</top>
<left>
<ScrollPane prefWidth="200.0" fitToWidth="true">
<BorderPane>
<center>
<VBox spacing="10.0" alignment="CENTER" VBox.vgrow="ALWAYS">
<Label text="Rooms" styleClass="rooms-label"/>
<VBox fx:id="roomList" styleClass="rooms-list" spacing="5.0" VBox.vgrow="ALWAYS" alignment="CENTER"/>
</VBox>
</center>
<bottom>
<Button text="Create room" onAction="#createRoom" />
</bottom>
</BorderPane>
</ScrollPane>
</left>
<center>
<BorderPane>
<top>
<HBox alignment="CENTER">
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
</padding>
<Label fx:id="roomName" style="-fx-font-size: 18px; -fx-font-weight: bold;"/>
</HBox>
</top>
<center>
<ScrollPane fx:id="chatPane" fitToWidth="true">
<VBox spacing="10.0">
<VBox fx:id="chatList" styleClass="chat-list" spacing="5.0" VBox.vgrow="ALWAYS"/>
</VBox>
</ScrollPane>
</center>
<bottom>
<HBox fx:id="chatInput" spacing="5.0" style="-fx-padding: 10;" />
</bottom>
</BorderPane>
</center>
</BorderPane>

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

View File

@@ -0,0 +1,27 @@
.root{
-fx-background-color: darkblue;
-fx-text-fill: blue;
-fx-font-weight: bold;
}
.address {
-fx-text-fill: white;
-fx-font-size: 25px;
}
.toggle-button {
-fx-background-color: darkblue;
-fx-text-fill: white;
-fx-font-size: 20px;
-fx-padding: 10px;
-fx-border-width: 2px;
-fx-border-color: white;
-fx-cursor: hand;
-fx-border-radius: 5;
}
.toggle-button:hover {
-fx-background-color: white;
-fx-text-fill: darkblue;
transition: -fx-background-color 0.3s, -fx-text-fill 0.3s;
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox alignment="CENTER" spacing="20.0" xmlns="http://javafx.com/javafx/11.0.14-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="server.ServerGuiController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<Label fx:id="IPAddress" styleClass="address" />
<Button text="Start Server" onAction="#toggleServer" fx:id="toggleServerButton" styleClass="toggle-button"/>
</VBox>

View File

@@ -0,0 +1,6 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
org.gradle.configuration-cache=false
org.gradle.console=plain
org.gradle.logging.level=quiet

View File

@@ -0,0 +1,10 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
[versions]
guava = "33.3.1-jre"
junit-jupiter = "5.11.3"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
ChatApp/gradlew vendored Executable file
View File

@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
ChatApp/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

14
ChatApp/settings.gradle Normal file
View File

@@ -0,0 +1,14 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.13/userguide/multi_project_builds.html in the Gradle documentation.
*/
plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0'
}
rootProject.name = 'ChatApp'
include('app')

View File

@@ -1,22 +0,0 @@
import java.net.InetSocketAddress;
import java.util.Scanner;
import client.Client;
import server.Server;
public class ChatApp {
public static void main(String[] args) throws Exception {
Server server = new Server(6665);
Client client = new Client(new InetSocketAddress("localhost", 6665));
client.SendCreateRoom("Room1");
client.RequestRoomList();
client.SendChatMessage("Hello");
Scanner scanner = new Scanner(System.in);
while (true) {
String message = scanner.nextLine();
client.visitMessage(message);
}
}
}

View File

@@ -1,105 +0,0 @@
package client;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.Objects;
import java.util.Scanner;
import network.protocol.packets.*;
public class Client {
private final ClientConnexion connexion;
public static void main(String[] args) {
try {
Client client = new Client(new InetSocketAddress("localhost", 6665));
Scanner scanner = new Scanner(System.in);
while(true) {
String message = scanner.nextLine();
client.visitMessage(message);
}
} catch (SocketException e) {
e.printStackTrace();
}
}
public Client(InetSocketAddress serverAddress) throws SocketException {
this.connexion = new ClientConnexion(new DatagramSocket(), serverAddress);
login("Moi");
}
public void visitMessage(String message){
try {
if(message.startsWith("/")){
if(message.startsWith("/createRoom")) {
String roomName = message.substring(12).trim();
SendCreateRoom(roomName);
} else if(message.startsWith("/listRooms")) {
RequestRoomList();
} else if(message.startsWith("/joinRoom")) {
String roomName = message.substring(10).trim();
SendJoinRoom(roomName);
} else if(message.startsWith("/leaveRoom")) {
SendLeaveRoom();
} else {
System.out.println("Unknown command");
}
} else {
SendChatMessage(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void login(String pseudo) {
try {
this.connexion.sendPacket(new LoginPacket(pseudo));
} catch (IOException e) {
e.printStackTrace();
}
}
public void SendChatMessage(String message) {
try {
this.connexion.sendPacket(new SendChatMessagePacket(message));
} catch (IOException e) {
e.printStackTrace();
}
}
public void SendCreateRoom(String roomName) {
try {
this.connexion.sendPacket(new CreateRoomPacket(roomName));
} catch (Exception e) {
e.printStackTrace();
}
}
public void SendJoinRoom(String roomName) {
try {
this.connexion.sendPacket(new JoinRoomPacket(roomName));
} catch (Exception e) {
e.printStackTrace();
}
}
public void SendLeaveRoom() {
try {
this.connexion.sendPacket(new LeaveRoomPacket());
} catch (Exception e) {
e.printStackTrace();
}
}
public void RequestRoomList() {
try {
this.connexion.sendPacket(new RequestRoomListPacket());
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -1,48 +0,0 @@
package network;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import network.protocol.Packet;
public class SocketReader {
private final DatagramSocket socket;
private final Thread readThread;
private final PacketHandler handler;
public SocketReader(DatagramSocket socket, PacketHandler handler) {
this.socket = socket;
this.handler = handler;
this.readThread = new Thread(this::readLoop);
this.readThread.start();
}
public void stop() {
this.readThread.interrupt();
}
private void readLoop() {
while (!Thread.interrupted()) {
try {
byte[] buffer = new byte[65535];
DatagramPacket dataPacket = new DatagramPacket(buffer, buffer.length);
socket.receive(dataPacket);
InetSocketAddress address = new InetSocketAddress(dataPacket.getAddress(), dataPacket.getPort());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(dataPacket.getData()));
Packet packet = (Packet) ois.readObject();
this.handler.handlePacket(packet, address);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}

View File

@@ -1,30 +0,0 @@
package network;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import network.protocol.Packet;
public class SocketWriter {
private final DatagramSocket socket;
public SocketWriter(DatagramSocket socket) {
this.socket = socket;
}
public void sendPacket(Packet packet, InetSocketAddress address) throws IOException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(stream);
oos.writeObject(packet);
oos.flush();
byte[] data = stream.toByteArray();
DatagramPacket dataPacket = new DatagramPacket(data, data.length, address.getAddress(),
address.getPort());
this.socket.send(dataPacket);
}
}

View File

@@ -1,100 +0,0 @@
package server;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import network.PacketHandler;
import network.SocketReader;
import network.protocol.Packet;
import network.protocol.packets.ChatMessagePacket;
import network.protocol.packets.SendChatMessagePacket;
public class Server implements PacketHandler {
private final DatagramSocket serverSocket;
private final Map<InetSocketAddress, ServerConnexion> connexions;
private final SocketReader reader;
private final Map<String, ArrayList<ServerConnexion>> roomNames;
public Server(int port) throws SocketException {
this.serverSocket = new DatagramSocket(port);
this.connexions = new HashMap<>();
this.reader = new SocketReader(serverSocket, this);
this.roomNames = new HashMap<>();
}
public ArrayList<String> getRoomNames() {
return roomNames.keySet().stream().collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
public String getRoomName(ServerConnexion connexion) {
for (Map.Entry<String, ArrayList<ServerConnexion>> entry : roomNames.entrySet()) {
if(entry.getValue().contains(connexion)) {
return entry.getKey();
}
}
return null;
}
public void createRoom(String roomName, ServerConnexion connexion) throws SocketException {
if(roomNames.containsKey(roomName)) {
throw new SocketException("Room already exists");
}
roomNames.put(roomName, new ArrayList<>());
roomNames.get(roomName).add(connexion);
}
public void leaveRoom(ServerConnexion connexion) throws SocketException {
String roomName = getRoomName(connexion);
if(roomName != null) {
roomNames.get(roomName).remove(connexion);
// Remove the room if it is empty
if(roomNames.get(roomName).isEmpty()) {
roomNames.remove(roomName);
}
return;
}
throw new SocketException("Room does not exist");
}
public void joinRoom(String roomName, ServerConnexion connexion) throws SocketException {
if(roomNames.containsKey(roomName)) {
roomNames.get(roomName).add(connexion);
return;
}
throw new SocketException("Room does not exist");
}
public void sendToRoom(ServerConnexion connexion, SendChatMessagePacket packet) throws SocketException {
String roomName = getRoomName(connexion);
ChatMessagePacket chatPacket = new ChatMessagePacket(Instant.now(), connexion.getChatterName(), packet.getContent());
if(roomName != null && roomNames.containsKey(roomName)) {
roomNames.get(roomName).forEach(con -> con.sendPacket(chatPacket));
return;
}
throw new SocketException("You are not in a room or the room does not exist");
}
public void close() {
this.reader.stop();
}
public boolean hasChatterName(String pseudo) {
return this.connexions.values().stream()
.anyMatch(connexion -> pseudo.equals(connexion.getChatterName()));
}
@Override
public void handlePacket(Packet packet, InetSocketAddress address) {
if (!connexions.containsKey(address)) {
this.connexions.put(address, new ServerConnexion(this, serverSocket, address));
}
this.connexions.get(address).visit(packet);
}
}

View File

@@ -1,122 +0,0 @@
package server;
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import network.SocketWriter;
import network.protocol.Packet;
import network.protocol.PacketVisitor;
import network.protocol.packets.ChatMessagePacket;
import network.protocol.packets.CreateRoomPacket;
import network.protocol.packets.JoinRoomPacket;
import network.protocol.packets.LeaveRoomPacket;
import network.protocol.packets.LoginPacket;
import network.protocol.packets.RequestRoomListPacket;
import network.protocol.packets.RoomListPacket;
import network.protocol.packets.SendChatMessagePacket;
import network.protocol.packets.ServerResponsePacket;
import network.protocol.packets.ServerResponsePacket.Response;
public class ServerConnexion implements PacketVisitor {
private final Server server;
private final InetSocketAddress clientAddress;
private final SocketWriter writer;
private String chatterName;
public ServerConnexion(Server server, DatagramSocket socket, InetSocketAddress clientAddress) {
this.clientAddress = clientAddress;
this.server = server;
this.writer = new SocketWriter(socket);
}
public String getChatterName() {
return this.chatterName;
}
public void sendPacket(Packet packet) {
try {
this.writer.sendPacket(packet, clientAddress);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void visitPacket(ChatMessagePacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(CreateRoomPacket packet) {
try {
server.createRoom(packet.getRoomName(), this);
sendPacket(new ServerResponsePacket(Response.RoomCreated));
} catch (SocketException e) {
sendPacket(new ServerResponsePacket(Response.RoomNotCreated));
}
}
@Override
public void visitPacket(JoinRoomPacket packet) {
try {
server.joinRoom(packet.getRoomName(), this);
sendPacket(new ServerResponsePacket(Response.RoomJoined));
} catch (SocketException e) {
sendPacket(new ServerResponsePacket(Response.RoomNotJoined));
}
}
@Override
public void visitPacket(LeaveRoomPacket packet) {
try {
server.leaveRoom(this);
sendPacket(new ServerResponsePacket(Response.RoomLeft));
} catch (SocketException e) {
sendPacket(new ServerResponsePacket(Response.RoomNotLeft));
}
}
@Override
public void visitPacket(LoginPacket packet) {
if (server.hasChatterName(packet.getPseudo())) {
sendPacket(new ServerResponsePacket(Response.AuthError));
return;
}
this.chatterName = packet.getPseudo();
sendPacket(new RoomListPacket(server.getRoomNames()));
System.out.println("[Server] Chatter " + packet.getPseudo() + " connected !");
}
@Override
public void visitPacket(RequestRoomListPacket packet) {
sendPacket(new RoomListPacket(server.getRoomNames()));
}
@Override
public void visitPacket(RoomListPacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
@Override
public void visitPacket(SendChatMessagePacket packet) {
try {
server.sendToRoom(this, packet);
sendPacket(new ServerResponsePacket(Response.MessageSent));
} catch (SocketException e) {
sendPacket(new ServerResponsePacket(Response.MessageNotSent));
}
}
@Override
public void visitPacket(ServerResponsePacket packet) {
// I'm never supposed to receive this from the client
throw new UnsupportedOperationException("Unimplemented method 'visitPacket'");
}
}

2
Sudoku

Submodule Sudoku updated: af0ac0ff77...05df8a56a7