mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 10:54:51 +00:00
Networking moved to netty. Added a EventPublisher class for easy building of events.
This commit is contained in:
@@ -1,214 +1,214 @@
|
||||
package org.toop;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.eventbus.Events;
|
||||
import org.toop.eventbus.GlobalEventBus;
|
||||
import org.toop.game.tictactoe.*;
|
||||
import org.toop.game.tictactoe.ai.MinMaxTicTacToe;
|
||||
|
||||
public class ConsoleGui {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(ConsoleGui.class);
|
||||
|
||||
private Scanner scanner;
|
||||
|
||||
private TicTacToe game;
|
||||
private MinMaxTicTacToe ai;
|
||||
|
||||
private String ai1 = null;
|
||||
private String ai2 = null;
|
||||
|
||||
private String serverId = null;
|
||||
private String connectionId = null;
|
||||
private String ticTacToeGameId = null;
|
||||
|
||||
public ConsoleGui() throws ExecutionException, InterruptedException {
|
||||
scanner = new Scanner(System.in);
|
||||
Random random = new Random(3453498);
|
||||
|
||||
int mode = -1;
|
||||
|
||||
System.out.print(
|
||||
"""
|
||||
1. player vs player
|
||||
2. player vs ai
|
||||
3. ai vs player
|
||||
4. ai v ai
|
||||
Choose mode (default is 1):\s\
|
||||
""");
|
||||
String modeString = scanner.nextLine();
|
||||
|
||||
try {
|
||||
mode = Integer.parseInt(modeString);
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
String player1 = null;
|
||||
String player2 = null;
|
||||
|
||||
switch (mode) {
|
||||
// player vs ai
|
||||
case 2:
|
||||
{
|
||||
System.out.print("Please enter your name: ");
|
||||
String name = scanner.nextLine();
|
||||
|
||||
player1 = name;
|
||||
ai2 = player2 = "AI#" + random.nextInt();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// ai vs player
|
||||
case 3:
|
||||
{
|
||||
System.out.print("Enter your name: ");
|
||||
String name = scanner.nextLine();
|
||||
|
||||
ai1 = player1 = "AI#" + random.nextInt();
|
||||
player2 = name;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// ai vs ai
|
||||
case 4:
|
||||
{
|
||||
ai1 = player1 = "AI#" + random.nextInt();
|
||||
ai2 = player2 = "AI2" + random.nextInt();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// player vs player
|
||||
case 1:
|
||||
default:
|
||||
{
|
||||
System.out.print("Player 1. Please enter your name: ");
|
||||
String name1 = scanner.nextLine();
|
||||
|
||||
System.out.print("Player 2. Please enter your name: ");
|
||||
String name2 = scanner.nextLine();
|
||||
|
||||
player1 = name1;
|
||||
player2 = name2;
|
||||
}
|
||||
}
|
||||
|
||||
game = new TicTacToe(player1, player2);
|
||||
ai = new MinMaxTicTacToe();
|
||||
|
||||
CompletableFuture<String> serverIdFuture = new CompletableFuture<>();
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.StartServerRequest("5001", "tictactoe", serverIdFuture));
|
||||
serverId = serverIdFuture.get();
|
||||
|
||||
CompletableFuture<String> connectionIdFuture = new CompletableFuture<>();
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.StartConnectionRequest(
|
||||
"127.0.0.1", "5001", connectionIdFuture));
|
||||
connectionId = connectionIdFuture.get();
|
||||
|
||||
CompletableFuture<String> ticTacToeGame = new CompletableFuture<>();
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.CreateTicTacToeGameRequest(
|
||||
serverId, player1, player2, ticTacToeGame));
|
||||
ticTacToeGameId = ticTacToeGame.get();
|
||||
GlobalEventBus.post(new Events.ServerEvents.RunTicTacToeGame(serverId, ticTacToeGameId));
|
||||
}
|
||||
|
||||
public void print() {
|
||||
char[] seperator = new char[game.getSize() * 4 - 1];
|
||||
Arrays.fill(seperator, '-');
|
||||
|
||||
for (int i = 0; i < game.getSize(); i++) {
|
||||
String buffer = " ";
|
||||
|
||||
for (int j = 0; j < game.getSize() - 1; j++) {
|
||||
buffer += game.getGrid()[i * game.getSize() + j] + " | ";
|
||||
}
|
||||
|
||||
buffer += game.getGrid()[i * game.getSize() + game.getSize() - 1];
|
||||
System.out.println(buffer);
|
||||
|
||||
if (i < game.getSize() - 1) {
|
||||
System.out.println(seperator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean next() {
|
||||
Player current = game.getCurrentPlayer();
|
||||
int move = -1;
|
||||
|
||||
if (ai1 != null && current.getName() == ai1 || ai2 != null && current.getName() == ai2) {
|
||||
move = ai.findBestMove(game);
|
||||
} else {
|
||||
System.out.printf(
|
||||
"%s's (%c) turn. Please choose an empty cell between 0-8: ",
|
||||
current.getName(), current.getSymbol());
|
||||
String input = scanner.nextLine();
|
||||
|
||||
try {
|
||||
move = Integer.parseInt(input);
|
||||
} catch (NumberFormatException e) {
|
||||
}
|
||||
}
|
||||
|
||||
GameBase.State state = game.play(move);
|
||||
boolean keepRunning = true;
|
||||
|
||||
switch (state) {
|
||||
case INVALID:
|
||||
{
|
||||
System.out.println("Please select an empty cell. Between 0-8");
|
||||
return true;
|
||||
}
|
||||
|
||||
case DRAW:
|
||||
{
|
||||
System.out.println("Game ended in a draw.");
|
||||
keepRunning = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case WIN:
|
||||
{
|
||||
System.out.printf("%s has won the game.\n", current.getName());
|
||||
keepRunning = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case NORMAL:
|
||||
default:
|
||||
{
|
||||
keepRunning = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.SendCommand(
|
||||
connectionId,
|
||||
"gameid " + ticTacToeGameId,
|
||||
"player " + current.getName(),
|
||||
"MOVE",
|
||||
String.valueOf(move)));
|
||||
|
||||
if (!keepRunning) {
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.EndTicTacToeGame(serverId, ticTacToeGameId));
|
||||
}
|
||||
|
||||
return keepRunning;
|
||||
}
|
||||
|
||||
public GameBase getGame() {
|
||||
return game;
|
||||
}
|
||||
}
|
||||
//package org.toop;
|
||||
//
|
||||
//import java.util.*;
|
||||
//import java.util.concurrent.CompletableFuture;
|
||||
//import java.util.concurrent.ExecutionException;
|
||||
//import org.apache.logging.log4j.LogManager;
|
||||
//import org.apache.logging.log4j.Logger;
|
||||
//import org.toop.eventbus.events.Events;
|
||||
//import org.toop.eventbus.GlobalEventBus;
|
||||
//import org.toop.game.tictactoe.*;
|
||||
//import org.toop.game.tictactoe.ai.MinMaxTicTacToe;
|
||||
//
|
||||
//public class ConsoleGui {
|
||||
//
|
||||
// private static final Logger logger = LogManager.getLogger(ConsoleGui.class);
|
||||
//
|
||||
// private Scanner scanner;
|
||||
//
|
||||
// private TicTacToe game;
|
||||
// private MinMaxTicTacToe ai;
|
||||
//
|
||||
// private String ai1 = null;
|
||||
// private String ai2 = null;
|
||||
//
|
||||
// private String serverId = null;
|
||||
// private String connectionId = null;
|
||||
// private String ticTacToeGameId = null;
|
||||
//
|
||||
// public ConsoleGui() throws ExecutionException, InterruptedException {
|
||||
// scanner = new Scanner(System.in);
|
||||
// Random random = new Random(3453498);
|
||||
//
|
||||
// int mode = -1;
|
||||
//
|
||||
// System.out.print(
|
||||
// """
|
||||
// 1. player vs player
|
||||
// 2. player vs ai
|
||||
// 3. ai vs player
|
||||
// 4. ai v ai
|
||||
// Choose mode (default is 1):\s\
|
||||
// """);
|
||||
// String modeString = scanner.nextLine();
|
||||
//
|
||||
// try {
|
||||
// mode = Integer.parseInt(modeString);
|
||||
// } catch (Exception e) {
|
||||
// logger.error(e.getMessage());
|
||||
// }
|
||||
//
|
||||
// String player1 = null;
|
||||
// String player2 = null;
|
||||
//
|
||||
// switch (mode) {
|
||||
// // player vs ai
|
||||
// case 2:
|
||||
// {
|
||||
// System.out.print("Please enter your name: ");
|
||||
// String name = scanner.nextLine();
|
||||
//
|
||||
// player1 = name;
|
||||
// ai2 = player2 = "AI#" + random.nextInt();
|
||||
//
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// // ai vs player
|
||||
// case 3:
|
||||
// {
|
||||
// System.out.print("Enter your name: ");
|
||||
// String name = scanner.nextLine();
|
||||
//
|
||||
// ai1 = player1 = "AI#" + random.nextInt();
|
||||
// player2 = name;
|
||||
//
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// // ai vs ai
|
||||
// case 4:
|
||||
// {
|
||||
// ai1 = player1 = "AI#" + random.nextInt();
|
||||
// ai2 = player2 = "AI2" + random.nextInt();
|
||||
//
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// // player vs player
|
||||
// case 1:
|
||||
// default:
|
||||
// {
|
||||
// System.out.print("Player 1. Please enter your name: ");
|
||||
// String name1 = scanner.nextLine();
|
||||
//
|
||||
// System.out.print("Player 2. Please enter your name: ");
|
||||
// String name2 = scanner.nextLine();
|
||||
//
|
||||
// player1 = name1;
|
||||
// player2 = name2;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// game = new TicTacToe(player1, player2);
|
||||
// ai = new MinMaxTicTacToe();
|
||||
//
|
||||
// CompletableFuture<String> serverIdFuture = new CompletableFuture<>();
|
||||
// GlobalEventBus.post(
|
||||
// new Events.ServerEvents.StartServerRequest("5001", "tictactoe", serverIdFuture));
|
||||
// serverId = serverIdFuture.get();
|
||||
//
|
||||
// CompletableFuture<String> connectionIdFuture = new CompletableFuture<>();
|
||||
// GlobalEventBus.post(
|
||||
// new Events.ServerEvents.StartConnectionRequest(
|
||||
// "127.0.0.1", "5001", connectionIdFuture));
|
||||
// connectionId = connectionIdFuture.get();
|
||||
//
|
||||
// CompletableFuture<String> ticTacToeGame = new CompletableFuture<>();
|
||||
// GlobalEventBus.post(
|
||||
// new Events.ServerEvents.CreateTicTacToeGameRequest(
|
||||
// serverId, player1, player2, ticTacToeGame));
|
||||
// ticTacToeGameId = ticTacToeGame.get();
|
||||
// GlobalEventBus.post(new Events.ServerEvents.RunTicTacToeGame(serverId, ticTacToeGameId));
|
||||
// }
|
||||
//
|
||||
// public void print() {
|
||||
// char[] seperator = new char[game.getSize() * 4 - 1];
|
||||
// Arrays.fill(seperator, '-');
|
||||
//
|
||||
// for (int i = 0; i < game.getSize(); i++) {
|
||||
// String buffer = " ";
|
||||
//
|
||||
// for (int j = 0; j < game.getSize() - 1; j++) {
|
||||
// buffer += game.getGrid()[i * game.getSize() + j] + " | ";
|
||||
// }
|
||||
//
|
||||
// buffer += game.getGrid()[i * game.getSize() + game.getSize() - 1];
|
||||
// System.out.println(buffer);
|
||||
//
|
||||
// if (i < game.getSize() - 1) {
|
||||
// System.out.println(seperator);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public boolean next() {
|
||||
// Player current = game.getCurrentPlayer();
|
||||
// int move = -1;
|
||||
//
|
||||
// if (ai1 != null && current.getName() == ai1 || ai2 != null && current.getName() == ai2) {
|
||||
// move = ai.findBestMove(game);
|
||||
// } else {
|
||||
// System.out.printf(
|
||||
// "%s's (%c) turn. Please choose an empty cell between 0-8: ",
|
||||
// current.getName(), current.getSymbol());
|
||||
// String input = scanner.nextLine();
|
||||
//
|
||||
// try {
|
||||
// move = Integer.parseInt(input);
|
||||
// } catch (NumberFormatException e) {
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// GameBase.State state = game.play(move);
|
||||
// boolean keepRunning = true;
|
||||
//
|
||||
// switch (state) {
|
||||
// case INVALID:
|
||||
// {
|
||||
// System.out.println("Please select an empty cell. Between 0-8");
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// case DRAW:
|
||||
// {
|
||||
// System.out.println("Game ended in a draw.");
|
||||
// keepRunning = false;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// case WIN:
|
||||
// {
|
||||
// System.out.printf("%s has won the game.\n", current.getName());
|
||||
// keepRunning = false;
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// case NORMAL:
|
||||
// default:
|
||||
// {
|
||||
// keepRunning = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// GlobalEventBus.post(
|
||||
// new Events.ServerEvents.SendCommand(
|
||||
// connectionId,
|
||||
// "gameid " + ticTacToeGameId,
|
||||
// "player " + current.getName(),
|
||||
// "MOVE",
|
||||
// String.valueOf(move)));
|
||||
//
|
||||
// if (!keepRunning) {
|
||||
// GlobalEventBus.post(
|
||||
// new Events.ServerEvents.EndTicTacToeGame(serverId, ticTacToeGameId));
|
||||
// }
|
||||
//
|
||||
// return keepRunning;
|
||||
// }
|
||||
//
|
||||
// public GameBase getGame() {
|
||||
// return game;
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
package org.toop;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.backend.ServerManager;
|
||||
import org.toop.eventbus.Events;
|
||||
import org.toop.eventbus.EventPublisher;
|
||||
import org.toop.eventbus.EventRegistry;
|
||||
import org.toop.eventbus.events.Events;
|
||||
import org.toop.eventbus.GlobalEventBus;
|
||||
import org.toop.frontend.ConnectionManager;
|
||||
import org.toop.eventbus.events.NetworkEvents;
|
||||
import org.toop.frontend.UI.LocalServerSelector;
|
||||
import org.toop.frontend.networking.NetworkingClientManager;
|
||||
import org.toop.frontend.networking.NetworkingGameClientHandler;
|
||||
|
||||
public class Main {
|
||||
private static final Logger logger = LogManager.getLogger(Main.class);
|
||||
@@ -15,14 +24,55 @@ public class Main {
|
||||
|
||||
public static void main(String[] args) throws ExecutionException, InterruptedException {
|
||||
// Logging.disableAllLogs();
|
||||
// Logging.enableAllLogsForClass(LocalTicTacToe.class);
|
||||
Logging.enableAllLogsForClass(EventRegistry.class);
|
||||
// Logging.enableLogsForClass(ServerManager.class, Level.ALL);
|
||||
// Logging.enableLogsForClass(TicTacToeServer.class, Level.ALL);
|
||||
// Logging.enableLogsForClass(TcpClient.class, Level.ALL);
|
||||
// Logging.enableLogsForClass(ConnectionManager.class, Level.ALL);
|
||||
// Logging.enableLogsForClass(NetworkingClientManager.class, Level.ALL);
|
||||
|
||||
initSystems();
|
||||
registerEvents();
|
||||
|
||||
CompletableFuture<String> serverIdFuture = new CompletableFuture<>();
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.StartServerRequest(5001, "tictactoe", serverIdFuture));
|
||||
var serverId = serverIdFuture.get();
|
||||
|
||||
// CompletableFuture<String> conIdFuture = new CompletableFuture<>();
|
||||
// GlobalEventBus.post(
|
||||
// new NetworkEvents.StartClientRequest(NetworkingGameClientHandler::new,
|
||||
// "127.0.0.1", 5001, conIdFuture));
|
||||
// var conId = conIdFuture.get();
|
||||
|
||||
int numThreads = 100; // how many EventPublisher tests you want
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(200); // 20 threads in pool
|
||||
|
||||
for (int i = 0; i < numThreads; i++) {
|
||||
executor.submit(() -> {
|
||||
new EventPublisher<>(
|
||||
NetworkEvents.StartClient.class,
|
||||
(Supplier<NetworkingGameClientHandler>) NetworkingGameClientHandler::new,
|
||||
"127.0.0.1",
|
||||
5001
|
||||
).onEventById(
|
||||
NetworkEvents.StartClientSuccess.class,
|
||||
event -> GlobalEventBus.post(
|
||||
new NetworkEvents.CloseClient((String) event.connectionId()))
|
||||
).unregisterAfterSuccess()
|
||||
.postEvent();
|
||||
});
|
||||
}
|
||||
|
||||
// Shutdown after tasks complete
|
||||
executor.shutdown();
|
||||
|
||||
// GlobalEventBus.post(new NetworkEvents.SendCommand(conId, "move", "5"));
|
||||
// GlobalEventBus.post(new NetworkEvents.ForceCloseAllClients());
|
||||
// GlobalEventBus.post(new NetworkEvents.StartClient(
|
||||
// NetworkingGameClientHandler::new, "127.0.0.1", 5001, serverId
|
||||
// ));
|
||||
|
||||
// JFrame frame = new JFrame("Server Settings");
|
||||
// frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
// frame.setSize(800, 600);
|
||||
@@ -49,7 +99,7 @@ public class Main {
|
||||
|
||||
public static void initSystems() {
|
||||
new ServerManager();
|
||||
new ConnectionManager();
|
||||
new NetworkingClientManager();
|
||||
}
|
||||
|
||||
private static void quit() {
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.backend.tictactoe.TicTacToeServer;
|
||||
import org.toop.eventbus.Events;
|
||||
import org.toop.eventbus.events.Events;
|
||||
import org.toop.eventbus.GlobalEventBus;
|
||||
|
||||
// TODO more methods.
|
||||
@@ -37,13 +37,13 @@ public class ServerManager {
|
||||
Events.ServerEvents.EndTicTacToeGame.class, this::handleEndTicTacToeGameOnAServer);
|
||||
}
|
||||
|
||||
private String startServer(String port, String gameType) {
|
||||
private String startServer(int port, String gameType) {
|
||||
String serverId = UUID.randomUUID().toString();
|
||||
gameType = gameType.toLowerCase();
|
||||
try {
|
||||
TcpServer server = null;
|
||||
if (Objects.equals(gameType, "tictactoe")) {
|
||||
server = new TicTacToeServer(Integer.parseInt(port));
|
||||
server = new TicTacToeServer(port);
|
||||
} else {
|
||||
logger.error("Manager could not create a server for game type: {}", gameType);
|
||||
return null;
|
||||
|
||||
192
src/main/java/org/toop/eventbus/EventPublisher.java
Normal file
192
src/main/java/org/toop/eventbus/EventPublisher.java
Normal file
@@ -0,0 +1,192 @@
|
||||
package org.toop.eventbus;
|
||||
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import org.toop.eventbus.events.EventWithUuid;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* EventPublisher is a helper class for creating, posting, and optionally subscribing to events
|
||||
* in a type-safe and chainable manner. It automatically injects a unique UUID into the event
|
||||
* and supports filtering subscribers by this UUID.
|
||||
*
|
||||
* <p>Usage pattern (with chainable API):
|
||||
* <pre>{@code
|
||||
* new EventPublisher<>(StartClient.class, handlerFactory, "127.0.0.1", 5001)
|
||||
* .onEventById(ClientReady.class, clientReadyEvent -> logger.info(clientReadyEvent))
|
||||
* .unregisterAfterSuccess()
|
||||
* .postEvent();
|
||||
* }</pre>
|
||||
*
|
||||
* @param <T> the type of event to publish, must extend EventWithUuid
|
||||
*/
|
||||
public class EventPublisher<T extends EventWithUuid> {
|
||||
|
||||
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
|
||||
private static final Map<Class<?>, MethodHandle> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/** The UUID automatically assigned to this event */
|
||||
private final String eventId;
|
||||
|
||||
/** The event instance created by this publisher */
|
||||
private final T event;
|
||||
|
||||
/** The listener object returned by the global event bus subscription */
|
||||
private Object listener;
|
||||
|
||||
/** Flag indicating whether to unregister the listener after it is successfully triggered */
|
||||
private boolean unregisterAfterSuccess = false;
|
||||
|
||||
/** Results that came back from the subscribed event */
|
||||
private Map<String, Object> result = null;
|
||||
|
||||
/**
|
||||
* Constructs a new EventPublisher by instantiating the given event class.
|
||||
* A unique UUID is automatically generated and passed as the last constructor argument.
|
||||
*
|
||||
* @param postEventClass the class of the event to instantiate
|
||||
* @param args constructor arguments for the event, excluding the UUID
|
||||
* @throws RuntimeException if instantiation fails
|
||||
*/
|
||||
public EventPublisher(Class<T> postEventClass, Object... args) {
|
||||
this.eventId = UUID.randomUUID().toString();
|
||||
|
||||
try {
|
||||
MethodHandle ctorHandle = CONSTRUCTOR_CACHE.computeIfAbsent(postEventClass, cls -> {
|
||||
try {
|
||||
// Build signature dynamically (arg types + String for UUID)
|
||||
Class<?>[] paramTypes = cls.getDeclaredConstructors()[0].getParameterTypes();
|
||||
MethodType mt = MethodType.methodType(void.class, paramTypes);
|
||||
return LOOKUP.findConstructor(cls, mt);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to find constructor handle for " + cls, e);
|
||||
}
|
||||
});
|
||||
|
||||
// Append UUID to args
|
||||
Object[] finalArgs = new Object[args.length + 1];
|
||||
System.arraycopy(args, 0, finalArgs, 0, args.length);
|
||||
finalArgs[args.length] = this.eventId;
|
||||
// --------------------
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
T instance = (T) ctorHandle.invokeWithArguments(finalArgs);
|
||||
this.event = instance;
|
||||
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Failed to instantiate event", e);
|
||||
}
|
||||
}
|
||||
|
||||
public EventPublisher(EventBus eventbus, Class<T> postEventClass, Object... args) {
|
||||
this.eventId = UUID.randomUUID().toString();
|
||||
|
||||
try {
|
||||
MethodHandle ctorHandle = CONSTRUCTOR_CACHE.computeIfAbsent(postEventClass, cls -> {
|
||||
try {
|
||||
// Build signature dynamically (arg types + String for UUID)
|
||||
Class<?>[] paramTypes = cls.getDeclaredConstructors()[0].getParameterTypes();
|
||||
MethodType mt = MethodType.methodType(void.class, paramTypes);
|
||||
return LOOKUP.findConstructor(cls, mt);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to find constructor handle for " + cls, e);
|
||||
}
|
||||
});
|
||||
|
||||
// Append UUID to args
|
||||
Object[] finalArgs = new Object[args.length + 1];
|
||||
System.arraycopy(args, 0, finalArgs, 0, args.length);
|
||||
finalArgs[args.length] = this.eventId;
|
||||
// --------------------
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
T instance = (T) ctorHandle.invokeWithArguments(finalArgs);
|
||||
this.event = instance;
|
||||
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Failed to instantiate event", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes a listener for a specific event type, but only triggers the listener
|
||||
* if the incoming event's UUID matches this EventPublisher's UUID.
|
||||
*
|
||||
* @param eventClass the class of the event to subscribe to
|
||||
* @param action the action to execute when a matching event is received
|
||||
* @param <TT> the type of the event to subscribe to; must extend EventWithUuid
|
||||
* @return this EventPublisher instance, for chainable calls
|
||||
*/
|
||||
public <TT extends EventWithUuid> EventPublisher<T> onEventById(
|
||||
Class<TT> eventClass, Consumer<TT> action) {
|
||||
|
||||
this.listener = GlobalEventBus.subscribeAndRegister(eventClass, event -> {
|
||||
if (event.eventId().equals(this.eventId)) {
|
||||
action.accept(event);
|
||||
|
||||
if (unregisterAfterSuccess && listener != null) {
|
||||
GlobalEventBus.unregister(listener);
|
||||
}
|
||||
|
||||
this.result = event.result();
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts the event to the global event bus. This should generally be the
|
||||
* final call in the chain.
|
||||
*
|
||||
* @return this EventPublisher instance, for potential chaining
|
||||
*/
|
||||
public EventPublisher<T> postEvent() {
|
||||
GlobalEventBus.post(event);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the publisher so that any listener registered with
|
||||
* {@link #onEventById(Class, Consumer)} is automatically unregistered
|
||||
* after it is successfully triggered.
|
||||
*
|
||||
* @return this EventPublisher instance, for chainable calls
|
||||
*/
|
||||
public EventPublisher<T> unregisterAfterSuccess() {
|
||||
this.unregisterAfterSuccess = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getResult() {
|
||||
if (this.result != null) {
|
||||
return this.result;
|
||||
}
|
||||
return null;
|
||||
// TODO: Why check for null if return is null anyway?
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the event instance created by this publisher.
|
||||
*
|
||||
* @return the event instance
|
||||
*/
|
||||
public T getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UUID automatically assigned to this event.
|
||||
*
|
||||
* @return the UUID of the event
|
||||
*/
|
||||
public String getEventId() {
|
||||
return eventId;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package org.toop.eventbus;
|
||||
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/** A singleton Event Bus to be used for creating, triggering and activating events. */
|
||||
@@ -53,6 +54,18 @@ public class GlobalEventBus {
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> Object subscribe(Consumer<T> action) {
|
||||
return new Object() {
|
||||
@Subscribe
|
||||
public void handle(Object event) {
|
||||
try {
|
||||
action.accept((T) event); // unchecked cast
|
||||
} catch (ClassCastException ignored) {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a Consumer into a Guava @Subscribe-compatible listener and registers it.
|
||||
*
|
||||
@@ -66,10 +79,16 @@ public class GlobalEventBus {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public static <T> Object subscribeAndRegister(Consumer<T> action) {
|
||||
var listener = subscribe(action);
|
||||
register(listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for registering a listener.
|
||||
*
|
||||
* @param event The ready event to add to register.
|
||||
* @param listener The listener to register.
|
||||
*/
|
||||
public static void register(Object listener) {
|
||||
GlobalEventBus.get().register(listener);
|
||||
@@ -78,7 +97,7 @@ public class GlobalEventBus {
|
||||
/**
|
||||
* Wrapper for unregistering a listener.
|
||||
*
|
||||
* @param event The ready event to unregister.
|
||||
* @param listener The listener to unregister.
|
||||
*/
|
||||
public static void unregister(Object listener) {
|
||||
GlobalEventBus.get().unregister(listener);
|
||||
@@ -90,18 +109,6 @@ public class GlobalEventBus {
|
||||
* @param event The event to post.
|
||||
*/
|
||||
public static <T> void post(T event) {
|
||||
Class<T> type = (Class<T>) event.getClass();
|
||||
|
||||
// if (!EventRegistry.isReady(type)) {
|
||||
// throw new IllegalStateException("Event type not ready: " +
|
||||
// type.getSimpleName());
|
||||
// } TODO: Handling non ready events.
|
||||
|
||||
// store in registry
|
||||
EventMeta<T> eventMeta = new EventMeta<>(type, event);
|
||||
EventRegistry.storeEvent(eventMeta);
|
||||
|
||||
// post to Guava EventBus
|
||||
GlobalEventBus.get().post(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package org.toop.eventbus;
|
||||
|
||||
public interface IEvents {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.toop.eventbus.events;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface EventWithUuid {
|
||||
Map<String, Object> result();
|
||||
String eventId();
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package org.toop.eventbus;
|
||||
package org.toop.eventbus.events;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.toop.backend.tictactoe.TicTacToeServer;
|
||||
|
||||
import org.toop.core.Window;
|
||||
|
||||
/** Events that are used in the GlobalEventBus class. */
|
||||
@@ -18,7 +18,7 @@ public class Events implements IEvents {
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Object get(String eventName, Object... args) throws Exception {
|
||||
Class<?> clazz = Class.forName("org.toop.eventbus.Events$ServerEvents$" + eventName);
|
||||
Class<?> clazz = Class.forName("org.toop.eventbus.events.Events$ServerEvents$" + eventName);
|
||||
Class<?>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
return constructor.newInstance(args);
|
||||
@@ -36,7 +36,7 @@ public class Events implements IEvents {
|
||||
public static Object get(String eventCategory, String eventName, Object... args)
|
||||
throws Exception {
|
||||
Class<?> clazz =
|
||||
Class.forName("org.toop.eventbus.Events$" + eventCategory + "$" + eventName);
|
||||
Class.forName("org.toop.eventbus.events.Events$" + eventCategory + "$" + eventName);
|
||||
Class<?>[] paramTypes = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
|
||||
Constructor<?> constructor = clazz.getConstructor(paramTypes);
|
||||
return constructor.newInstance(args);
|
||||
@@ -72,14 +72,6 @@ public class Events implements IEvents {
|
||||
|
||||
public static class ServerEvents {
|
||||
|
||||
/**
|
||||
* BLOCKING Requests all active connections. The result is returned via the provided
|
||||
* CompletableFuture.
|
||||
*
|
||||
* @param future List of all connections in string form.
|
||||
*/
|
||||
public record RequestsAllConnections(CompletableFuture<String> future) {}
|
||||
|
||||
/**
|
||||
* BLOCKING Requests all active servers. The result is returned via the provided
|
||||
* CompletableFuture.
|
||||
@@ -88,9 +80,6 @@ public class Events implements IEvents {
|
||||
*/
|
||||
public record RequestsAllServers(CompletableFuture<String> future) {}
|
||||
|
||||
/** Forces closing all active connections immediately. */
|
||||
public record ForceCloseAllConnections() {}
|
||||
|
||||
/** Forces closing all active servers immediately. */
|
||||
public record ForceCloseAllServers() {}
|
||||
|
||||
@@ -100,7 +89,7 @@ public class Events implements IEvents {
|
||||
* @param port The port to open the server.
|
||||
* @param gameType Either "tictactoe" or ...
|
||||
*/
|
||||
public record StartServer(String port, String gameType) {}
|
||||
public record StartServer(int port, String gameType) {}
|
||||
|
||||
/**
|
||||
* BLOCKING Requests starting a server with a specific port and game type, and returns a
|
||||
@@ -111,7 +100,7 @@ public class Events implements IEvents {
|
||||
* @param future The uuid of the server.
|
||||
*/
|
||||
public record StartServerRequest(
|
||||
String port, String gameType, CompletableFuture<String> future) {}
|
||||
int port, String gameType, CompletableFuture<String> future) {}
|
||||
|
||||
/**
|
||||
* Represents a server that has successfully started.
|
||||
@@ -119,7 +108,7 @@ public class Events implements IEvents {
|
||||
* @param uuid The unique identifier of the server.
|
||||
* @param port The port the server is listening on.
|
||||
*/
|
||||
public record ServerStarted(String uuid, String port) {}
|
||||
public record ServerStarted(String uuid, int port) {}
|
||||
|
||||
/**
|
||||
* BLOCKING Requests creation of a TicTacToe game on a specific server.
|
||||
@@ -151,61 +140,9 @@ public class Events implements IEvents {
|
||||
*/
|
||||
public record EndTicTacToeGame(String serverUuid, String gameUuid) {}
|
||||
|
||||
/**
|
||||
* Triggers starting a server connection.
|
||||
*
|
||||
* @param ip The IP address of the server to connect to.
|
||||
* @param port The port of the server to connect to.
|
||||
*/
|
||||
public record StartConnection(String ip, String port) {}
|
||||
|
||||
/**
|
||||
* BLOCKING Triggers starting a server connection and returns a future.
|
||||
*
|
||||
* @param ip The IP address of the server to connect to.
|
||||
* @param port The port of the server to connect to.
|
||||
* @param future Returns the UUID of the connection, when connection is established.
|
||||
*/
|
||||
public record StartConnectionRequest(
|
||||
String ip, String port, CompletableFuture<String> future) {}
|
||||
|
||||
// public record StartGameConnectionRequest(String ip, String port,
|
||||
// CompletableFuture<String> future) {}
|
||||
|
||||
/**
|
||||
* BLOCKING Triggers starting a server connection and returns a future.
|
||||
*
|
||||
* @param ip The IP address of the server to connect to.
|
||||
* @param port The port of the server to connect to.
|
||||
*/
|
||||
public record ConnectionEstablished(Object connectionId, String ip, String port) {}
|
||||
|
||||
/**
|
||||
* Triggers sending a command to a server.
|
||||
*
|
||||
* @param connectionId The UUID of the connection to send the command on.
|
||||
* @param args The command arguments.
|
||||
*/
|
||||
public record SendCommand(String connectionId, String... args) {}
|
||||
|
||||
/**
|
||||
* WIP Triggers when a command is sent to a server.
|
||||
*
|
||||
* @param command The TicTacToeServer instance that executed the command.
|
||||
* @param args The command arguments.
|
||||
* @param result The result returned from executing the command.
|
||||
*/
|
||||
public record OnCommand(
|
||||
TicTacToeServer command, String[] args, String result) {} // TODO old
|
||||
|
||||
/**
|
||||
* Triggers when the server client receives a message.
|
||||
*
|
||||
* @param ConnectionUuid The UUID of the connection that received the message.
|
||||
* @param message The message received.
|
||||
*/
|
||||
public record ReceivedMessage(String ConnectionUuid, String message) {}
|
||||
|
||||
/**
|
||||
* Triggers on changing the server IP.
|
||||
*
|
||||
@@ -218,33 +155,7 @@ public class Events implements IEvents {
|
||||
*
|
||||
* @param port The new port.
|
||||
*/
|
||||
public record OnChangingServerPort(String port) {}
|
||||
|
||||
/**
|
||||
* Triggers reconnecting to a previous address.
|
||||
*
|
||||
* @param connectionId The identifier of the connection being reconnected.
|
||||
*/
|
||||
public record Reconnect(Object connectionId) {}
|
||||
|
||||
/**
|
||||
* Triggers changing connection to a new address.
|
||||
*
|
||||
* @param connectionId The identifier of the connection being changed.
|
||||
* @param ip The new IP address.
|
||||
* @param port The new port.
|
||||
*/
|
||||
public record ChangeConnection(Object connectionId, String ip, String port) {}
|
||||
|
||||
/**
|
||||
* Triggers when the server couldn't connect to the desired address.
|
||||
*
|
||||
* @param connectionId The identifier of the connection that failed.
|
||||
*/
|
||||
public record CouldNotConnect(Object connectionId) {}
|
||||
|
||||
/** WIP Triggers when a connection closes. */
|
||||
public record ClosedConnection() {}
|
||||
public record OnChangingServerPort(int port) {}
|
||||
|
||||
/** Triggers when a cell is clicked in one of the game boards. */
|
||||
public record CellClicked(int cell) {}
|
||||
3
src/main/java/org/toop/eventbus/events/IEvents.java
Normal file
3
src/main/java/org/toop/eventbus/events/IEvents.java
Normal file
@@ -0,0 +1,3 @@
|
||||
package org.toop.eventbus.events;
|
||||
|
||||
public interface IEvents {}
|
||||
190
src/main/java/org/toop/eventbus/events/NetworkEvents.java
Normal file
190
src/main/java/org/toop/eventbus/events/NetworkEvents.java
Normal file
@@ -0,0 +1,190 @@
|
||||
package org.toop.eventbus.events;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import org.toop.backend.tictactoe.TicTacToeServer;
|
||||
import org.toop.frontend.networking.NetworkingGameClientHandler;
|
||||
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class NetworkEvents extends Events {
|
||||
|
||||
/**
|
||||
* BLOCKING Requests all active connections. The result is returned via the provided
|
||||
* CompletableFuture.
|
||||
*
|
||||
* @param future List of all connections in string form.
|
||||
*/
|
||||
public record RequestsAllClients(CompletableFuture<String> future) {}
|
||||
|
||||
/** Forces closing all active connections immediately. */
|
||||
public record ForceCloseAllClients() {}
|
||||
|
||||
public record CloseClientRequest(CompletableFuture<String> future) {}
|
||||
|
||||
public record CloseClient(String connectionId) {}
|
||||
|
||||
/**
|
||||
* Event to start a new client connection to a server.
|
||||
* <p>
|
||||
* This event is typically posted to the {@code GlobalEventBus} to initiate the creation of
|
||||
* a client connection, and carries all information needed to establish that connection:
|
||||
* <br>
|
||||
* - A factory for creating the Netty handler that will manage the connection
|
||||
* <br>
|
||||
* - The server's IP address and port
|
||||
* <br>
|
||||
* - A unique event identifier for correlation with follow-up events
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The {@link #eventId()} allows callers to correlate the {@code StartClient} event
|
||||
* with subsequent success/failure events. For example, a {@code StartClientSuccess}
|
||||
* or {@code StartClientFailure} event may carry the same {@code eventId}.
|
||||
* </p>
|
||||
*
|
||||
* @param handlerFactory Factory for constructing a {@link NetworkingGameClientHandler}.
|
||||
* @param ip The IP address of the server to connect to.
|
||||
* @param port The port number of the server to connect to.
|
||||
* @param eventId A unique identifier for this event, typically injected
|
||||
* automatically by the {@link org.toop.eventbus.EventPublisher}.
|
||||
*/
|
||||
public record StartClient(
|
||||
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
|
||||
String ip,
|
||||
int port,
|
||||
String eventId
|
||||
) implements EventWithUuid {
|
||||
|
||||
/**
|
||||
* Returns a map representation of this event, where keys are record component names
|
||||
* and values are their corresponding values. Useful for generic logging, debugging,
|
||||
* or serializing events without hardcoding field names.
|
||||
*
|
||||
* @return a {@code Map<String, Object>} containing field names and values
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Stream.of(this.getClass().getRecordComponents())
|
||||
.collect(Collectors.toMap(
|
||||
RecordComponent::getName,
|
||||
rc -> {
|
||||
try {
|
||||
return rc.getAccessor().invoke(this);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique event identifier used for correlating this event.
|
||||
*
|
||||
* @return the event ID string
|
||||
*/
|
||||
@Override
|
||||
public String eventId() {
|
||||
return this.eventId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Update docs new input.
|
||||
* BLOCKING Triggers starting a server connection and returns a future.
|
||||
*
|
||||
* @param ip The IP address of the server to connect to.
|
||||
* @param port The port of the server to connect to.
|
||||
* @param future Returns the UUID of the connection, when connection is established.
|
||||
*/
|
||||
public record StartClientRequest(
|
||||
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
|
||||
String ip, int port, CompletableFuture<String> future) {}
|
||||
|
||||
/**
|
||||
* BLOCKING Triggers starting a server connection and returns a future.
|
||||
*
|
||||
* @param ip The IP address of the server to connect to.
|
||||
* @param port The port of the server to connect to.
|
||||
*/
|
||||
public record StartClientSuccess(Object connectionId, String ip, int port, String eventId)
|
||||
implements EventWithUuid {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Stream.of(this.getClass().getRecordComponents())
|
||||
.collect(Collectors.toMap(
|
||||
RecordComponent::getName,
|
||||
rc -> {
|
||||
try {
|
||||
return rc.getAccessor().invoke(this);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String eventId() {
|
||||
return this.eventId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers sending a command to a server.
|
||||
*
|
||||
* @param connectionId The UUID of the connection to send the command on.
|
||||
* @param args The command arguments.
|
||||
*/
|
||||
public record SendCommand(String connectionId, String... args) {}
|
||||
|
||||
/**
|
||||
* WIP Triggers when a command is sent to a server.
|
||||
*
|
||||
* @param command The TicTacToeServer instance that executed the command.
|
||||
* @param args The command arguments.
|
||||
* @param result The result returned from executing the command.
|
||||
*/
|
||||
public record OnCommand(
|
||||
TicTacToeServer command, String[] args, String result) {} // TODO old
|
||||
|
||||
/**
|
||||
* Triggers reconnecting to a previous address.
|
||||
*
|
||||
* @param connectionId The identifier of the connection being reconnected.
|
||||
*/
|
||||
public record Reconnect(Object connectionId) {}
|
||||
|
||||
|
||||
/**
|
||||
* Triggers when the server client receives a message.
|
||||
*
|
||||
* @param ConnectionUuid The UUID of the connection that received the message.
|
||||
* @param message The message received.
|
||||
*/
|
||||
public record ReceivedMessage(String ConnectionUuid, String message) {}
|
||||
|
||||
/**
|
||||
* Triggers changing connection to a new address.
|
||||
*
|
||||
* @param connectionId The identifier of the connection being changed.
|
||||
* @param ip The new IP address.
|
||||
* @param port The new port.
|
||||
*/
|
||||
public record ChangeClient(Object connectionId, String ip, int port) {}
|
||||
|
||||
|
||||
/**
|
||||
* Triggers when the server couldn't connect to the desired address.
|
||||
*
|
||||
* @param connectionId The identifier of the connection that failed.
|
||||
*/
|
||||
public record CouldNotConnect(Object connectionId) {}
|
||||
|
||||
/** WIP Triggers when a connection closes. */
|
||||
public record ClosedConnection() {}
|
||||
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package org.toop.frontend;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.eventbus.Events;
|
||||
import org.toop.eventbus.GlobalEventBus;
|
||||
|
||||
public class ConnectionManager {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(ConnectionManager.class);
|
||||
|
||||
/** Map of serverId -> Server instances */
|
||||
private final Map<String, ServerConnection> serverConnections = new ConcurrentHashMap<>();
|
||||
|
||||
/** Starts a connection manager, to manage, connections. */
|
||||
public ConnectionManager() {
|
||||
GlobalEventBus.subscribeAndRegister(
|
||||
Events.ServerEvents.StartConnectionRequest.class,
|
||||
this::handleStartConnectionRequest);
|
||||
GlobalEventBus.subscribeAndRegister(
|
||||
Events.ServerEvents.StartConnection.class, this::handleStartConnection);
|
||||
GlobalEventBus.subscribeAndRegister(
|
||||
Events.ServerEvents.SendCommand.class, this::handleCommand);
|
||||
GlobalEventBus.subscribeAndRegister(
|
||||
Events.ServerEvents.Reconnect.class, this::handleReconnect);
|
||||
// GlobalEventBus.subscribeAndRegister(Events.ServerEvents.ChangeConnection.class,
|
||||
// this::handleChangeConnection);
|
||||
GlobalEventBus.subscribeAndRegister(
|
||||
Events.ServerEvents.ForceCloseAllConnections.class, _ -> shutdownAll());
|
||||
GlobalEventBus.subscribeAndRegister(
|
||||
Events.ServerEvents.RequestsAllConnections.class, this::getAllConnections);
|
||||
}
|
||||
|
||||
private String startConnectionRequest(String ip, String port) {
|
||||
String connectionId = UUID.randomUUID().toString();
|
||||
try {
|
||||
if (!port.matches("[0-9]+")) {
|
||||
port = "0000";
|
||||
}
|
||||
ServerConnection connection = new ServerConnection(connectionId, ip, port);
|
||||
this.serverConnections.put(connectionId, connection);
|
||||
new Thread(connection, "Connection-" + connectionId).start();
|
||||
logger.info("Connected to server {} at {}:{}", connectionId, ip, port);
|
||||
} catch (IOException e) {
|
||||
logger.error("{}", e);
|
||||
}
|
||||
return connectionId;
|
||||
}
|
||||
|
||||
private void handleStartConnectionRequest(Events.ServerEvents.StartConnectionRequest request) {
|
||||
request.future()
|
||||
.complete(
|
||||
this.startConnectionRequest(
|
||||
request.ip(),
|
||||
request.port())); // TODO: Maybe post ConnectionEstablished event.
|
||||
}
|
||||
|
||||
private void handleStartConnection(Events.ServerEvents.StartConnection event) {
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.ConnectionEstablished(
|
||||
this.startConnectionRequest(event.ip(), event.port()),
|
||||
event.ip(),
|
||||
event.port()));
|
||||
}
|
||||
|
||||
private void handleCommand(
|
||||
Events.ServerEvents.SendCommand
|
||||
event) { // TODO: Move this to ServerConnection class, keep it internal.
|
||||
ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
|
||||
if (serverConnection != null) {
|
||||
serverConnection.sendCommandByString(event.args());
|
||||
} else {
|
||||
logger.warn("Server {} not found for command '{}'", event.connectionId(), event.args());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReconnect(Events.ServerEvents.Reconnect event) {
|
||||
ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
|
||||
if (serverConnection != null) {
|
||||
try {
|
||||
serverConnection.reconnect();
|
||||
logger.info("Server {} reconnected", event.connectionId());
|
||||
} catch (Exception e) {
|
||||
logger.error("Server {} failed to reconnect", event.connectionId(), e);
|
||||
GlobalEventBus.post(new Events.ServerEvents.CouldNotConnect(event.connectionId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// private void handleChangeConnection(Events.ServerEvents.ChangeConnection event) {
|
||||
// ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
|
||||
// if (serverConnection != null) {
|
||||
// try {
|
||||
// serverConnection.connect(event.ip(), event.port());
|
||||
// logger.info("Server {} changed connection to {}:{}", event.connectionId(),
|
||||
// event.ip(), event.port());
|
||||
// } catch (Exception e) {
|
||||
// logger.error("Server {} failed to change connection", event.connectionId(),
|
||||
// e);
|
||||
// GlobalEventBus.post(new
|
||||
// Events.ServerEvents.CouldNotConnect(event.connectionId()));
|
||||
// }
|
||||
// }
|
||||
// } TODO
|
||||
|
||||
private void getAllConnections(Events.ServerEvents.RequestsAllConnections request) {
|
||||
List<ServerConnection> a = new ArrayList<>(this.serverConnections.values());
|
||||
request.future().complete(a.toString());
|
||||
}
|
||||
|
||||
public void shutdownAll() {
|
||||
this.serverConnections.values().forEach(ServerConnection::closeConnection);
|
||||
this.serverConnections.clear();
|
||||
logger.info("All servers shut down");
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@ import java.util.concurrent.ExecutionException;
|
||||
import javax.swing.*;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.eventbus.Events;
|
||||
import org.toop.eventbus.events.Events;
|
||||
import org.toop.eventbus.GlobalEventBus;
|
||||
import org.toop.eventbus.events.NetworkEvents;
|
||||
import org.toop.frontend.games.LocalTicTacToe;
|
||||
import org.toop.frontend.networking.NetworkingGameClientHandler;
|
||||
|
||||
public class RemoteGameSelector {
|
||||
private static final Logger logger = LogManager.getLogger(RemoteGameSelector.class);
|
||||
@@ -56,7 +58,7 @@ public class RemoteGameSelector {
|
||||
CompletableFuture<String> serverIdFuture = new CompletableFuture<>();
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.StartServerRequest(
|
||||
portTextField.getText(),
|
||||
Integer.parseInt(portTextField.getText()), // TODO: Unsafe parse
|
||||
Objects.requireNonNull(gameSelectorBox.getSelectedItem())
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
@@ -71,9 +73,10 @@ public class RemoteGameSelector {
|
||||
|
||||
CompletableFuture<String> connectionIdFuture = new CompletableFuture<>();
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.StartConnectionRequest(
|
||||
new NetworkEvents.StartClientRequest(
|
||||
NetworkingGameClientHandler::new,
|
||||
ipTextField.getText(),
|
||||
portTextField.getText(),
|
||||
Integer.parseInt(portTextField.getText()), // TODO: Not safe parsing
|
||||
connectionIdFuture));
|
||||
String connectionId;
|
||||
try {
|
||||
@@ -83,7 +86,7 @@ public class RemoteGameSelector {
|
||||
} // TODO: Better error handling to not crash the system.
|
||||
|
||||
GlobalEventBus.subscribeAndRegister(
|
||||
Events.ServerEvents.ReceivedMessage.class,
|
||||
NetworkEvents.ReceivedMessage.class,
|
||||
event -> {
|
||||
if (event.message().equalsIgnoreCase("ok")) {
|
||||
logger.info("received ok from server.");
|
||||
@@ -93,7 +96,7 @@ public class RemoteGameSelector {
|
||||
.toLowerCase()
|
||||
.replace("gameid ", "");
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.SendCommand(
|
||||
new NetworkEvents.SendCommand(
|
||||
"start_game " + gameId));
|
||||
} else {
|
||||
logger.info("{}", event.message());
|
||||
@@ -101,7 +104,7 @@ public class RemoteGameSelector {
|
||||
});
|
||||
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.SendCommand(
|
||||
new NetworkEvents.SendCommand(
|
||||
connectionId,
|
||||
"create_game",
|
||||
nameTextField.getText(),
|
||||
@@ -127,7 +130,7 @@ public class RemoteGameSelector {
|
||||
frame.remove(mainMenu);
|
||||
localTicTacToe =
|
||||
LocalTicTacToe.createRemote(
|
||||
ipTextField.getText(), portTextField.getText());
|
||||
ipTextField.getText(), Integer.parseInt(portTextField.getText())); // TODO: Unsafe parse
|
||||
UIGameBoard ttt = new UIGameBoard(localTicTacToe, this); // TODO: Fix later
|
||||
frame.add(ttt.getTTTPanel()); // TODO: Fix later
|
||||
frame.revalidate();
|
||||
|
||||
@@ -3,9 +3,11 @@ package org.toop.frontend.games;
|
||||
import java.util.concurrent.*;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.eventbus.Events;
|
||||
import org.toop.eventbus.events.Events;
|
||||
import org.toop.eventbus.GlobalEventBus;
|
||||
import org.toop.eventbus.events.NetworkEvents;
|
||||
import org.toop.frontend.UI.UIGameBoard;
|
||||
import org.toop.frontend.networking.NetworkingGameClientHandler;
|
||||
import org.toop.game.tictactoe.GameBase;
|
||||
import org.toop.game.tictactoe.TicTacToe;
|
||||
import org.toop.game.tictactoe.ai.MinMaxTicTacToe;
|
||||
@@ -63,13 +65,12 @@ public class LocalTicTacToe { // TODO: Implement runnable
|
||||
* @param ip The IP of the server to connect to.
|
||||
* @param port The port of the server to connect to.
|
||||
*/
|
||||
private LocalTicTacToe(String ip, String port) {
|
||||
private LocalTicTacToe(String ip, int port) {
|
||||
this.receivedMessageListener =
|
||||
GlobalEventBus.subscribe(
|
||||
Events.ServerEvents.ReceivedMessage.class, this::receiveMessageAction);
|
||||
GlobalEventBus.subscribe(this::receiveMessageAction);
|
||||
GlobalEventBus.register(this.receivedMessageListener);
|
||||
this.connectionId = this.createConnection(ip, port);
|
||||
this.createGame(ip, port);
|
||||
this.createGame("X", "O");
|
||||
this.isLocal = false;
|
||||
this.executor.submit(this::remoteGameThread);
|
||||
}
|
||||
@@ -93,11 +94,11 @@ public class LocalTicTacToe { // TODO: Implement runnable
|
||||
return new LocalTicTacToe(aiPlayers);
|
||||
}
|
||||
|
||||
public static LocalTicTacToe createRemote(String ip, String port) {
|
||||
public static LocalTicTacToe createRemote(String ip, int port) {
|
||||
return new LocalTicTacToe(ip, port);
|
||||
}
|
||||
|
||||
private String createServer(String port) {
|
||||
private String createServer(int port) {
|
||||
CompletableFuture<String> serverIdFuture = new CompletableFuture<>();
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.StartServerRequest(port, "tictactoe", serverIdFuture));
|
||||
@@ -109,10 +110,11 @@ public class LocalTicTacToe { // TODO: Implement runnable
|
||||
return null;
|
||||
}
|
||||
|
||||
private String createConnection(String ip, String port) {
|
||||
private String createConnection(String ip, int port) {
|
||||
CompletableFuture<String> connectionIdFuture = new CompletableFuture<>();
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.StartConnectionRequest(
|
||||
new NetworkEvents.StartClientRequest(
|
||||
NetworkingGameClientHandler::new,
|
||||
ip,
|
||||
port,
|
||||
connectionIdFuture)); // TODO: what if server couldn't be started with port.
|
||||
@@ -232,7 +234,7 @@ public class LocalTicTacToe { // TODO: Implement runnable
|
||||
this.endListeners();
|
||||
}
|
||||
|
||||
private void receiveMessageAction(Events.ServerEvents.ReceivedMessage receivedMessage) {
|
||||
private void receiveMessageAction(NetworkEvents.ReceivedMessage receivedMessage) {
|
||||
if (!receivedMessage.ConnectionUuid().equals(this.connectionId)) {
|
||||
return;
|
||||
}
|
||||
@@ -247,7 +249,7 @@ public class LocalTicTacToe { // TODO: Implement runnable
|
||||
}
|
||||
|
||||
private void sendCommand(String... args) {
|
||||
GlobalEventBus.post(new Events.ServerEvents.SendCommand(this.connectionId, args));
|
||||
GlobalEventBus.post(new NetworkEvents.SendCommand(this.connectionId, args));
|
||||
}
|
||||
|
||||
private void endListeners() {
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.*;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.eventbus.*;
|
||||
import org.toop.eventbus.events.Events;
|
||||
import org.toop.frontend.graphics.Shader;
|
||||
|
||||
public class NodeManager {
|
||||
|
||||
139
src/main/java/org/toop/frontend/networking/NetworkingClient.java
Normal file
139
src/main/java/org/toop/frontend/networking/NetworkingClient.java
Normal file
@@ -0,0 +1,139 @@
|
||||
package org.toop.frontend.networking;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioIoHandler;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.LineBasedFrameDecoder;
|
||||
import io.netty.handler.codec.string.StringDecoder;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class NetworkingClient {
|
||||
private static final Logger logger = LogManager.getLogger(NetworkingClient.class);
|
||||
|
||||
final Bootstrap bootstrap = new Bootstrap();
|
||||
final EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
|
||||
|
||||
private String connectionUuid;
|
||||
private Channel channel;
|
||||
private NetworkingGameClientHandler handler;
|
||||
|
||||
public NetworkingClient(
|
||||
Supplier<? extends NetworkingGameClientHandler> handlerFactory,
|
||||
String host,
|
||||
int port) {
|
||||
try {
|
||||
this.bootstrap.group(this.workerGroup);
|
||||
this.bootstrap.channel(NioSocketChannel.class);
|
||||
this.bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
|
||||
this.bootstrap.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) {
|
||||
handler = handlerFactory.get();
|
||||
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n
|
||||
pipeline.addLast(new StringDecoder()); // bytes -> String
|
||||
pipeline.addLast(handler);
|
||||
}
|
||||
});
|
||||
ChannelFuture channelFuture = this.bootstrap.connect(host, port).sync();
|
||||
this.channel = channelFuture.channel();
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to create networking client instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkingGameClientHandler getHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
public void setConnectionUuid(String connectionUuid) {
|
||||
this.connectionUuid = connectionUuid;
|
||||
}
|
||||
|
||||
public boolean isChannelActive() {
|
||||
return this.channel != null && this.channel.isActive();
|
||||
}
|
||||
|
||||
public void writeAndFlush(String msg) {
|
||||
String literalMsg = msg.replace("\n", "\\n").replace("\r", "\\r");
|
||||
if (isChannelActive()) {
|
||||
this.channel.writeAndFlush(msg);
|
||||
logger.info("Connection {} sent message: {}", this.channel.remoteAddress(), literalMsg);
|
||||
} else {
|
||||
logger.warn("Cannot send message: {}, connection inactive.", literalMsg);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeAndFlushnl(String msg) {
|
||||
if (isChannelActive()) {
|
||||
this.channel.writeAndFlush(msg + "\n");
|
||||
logger.info("Connection {} sent message: {}", this.channel.remoteAddress(), msg);
|
||||
} else {
|
||||
logger.warn("Cannot send message: {}, connection inactive.", msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void login(String username) {
|
||||
this.writeAndFlush("login " + username + "\n");
|
||||
}
|
||||
|
||||
public void logout() {
|
||||
this.writeAndFlush("logout\n");
|
||||
}
|
||||
|
||||
public void sendMove(int move) {
|
||||
this.writeAndFlush("move " + move + "\n"); // append \n so server receives a full line
|
||||
}
|
||||
|
||||
public void getGamelist() {
|
||||
this.writeAndFlush("get gamelist\n");
|
||||
}
|
||||
|
||||
public void getPlayerlist() {
|
||||
this.writeAndFlush("get playerlist\n");
|
||||
}
|
||||
|
||||
public void subscribe(String gameType) {
|
||||
this.writeAndFlush("subscribe " + gameType + "\n");
|
||||
}
|
||||
|
||||
public void forfeit() {
|
||||
this.writeAndFlush("forfeit\n");
|
||||
}
|
||||
|
||||
public void challenge(String playerName, String gameType) {
|
||||
this.writeAndFlush("challenge " + playerName + " " + gameType + "\n");
|
||||
}
|
||||
|
||||
public void acceptChallenge(String challengeNumber) {
|
||||
this.writeAndFlush("challenge accept " + challengeNumber + "\n");
|
||||
}
|
||||
|
||||
public void sendChatMessage(String message) {
|
||||
this.writeAndFlush("message " + "\"" + message + "\"" + "\n");
|
||||
}
|
||||
|
||||
public void help(String command) {
|
||||
this.writeAndFlush("help " + command + "\n");
|
||||
}
|
||||
|
||||
public void closeConnection() {
|
||||
if (this.channel != null && this.channel.isActive()) {
|
||||
this.channel.close().addListener(future -> {
|
||||
if (future.isSuccess()) {
|
||||
logger.info("Connection {} closed successfully", this.channel.remoteAddress());
|
||||
} else {
|
||||
logger.error("Error closing connection {}. Error: {}",
|
||||
this.channel.remoteAddress(),
|
||||
future.cause().getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package org.toop.frontend.networking;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.google.common.base.Supplier;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.eventbus.events.Events;
|
||||
import org.toop.eventbus.GlobalEventBus;
|
||||
import org.toop.eventbus.events.NetworkEvents;
|
||||
|
||||
public class NetworkingClientManager {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class);
|
||||
|
||||
/** Map of serverId -> Server instances */
|
||||
private final Map<String, NetworkingClient> networkClients = new ConcurrentHashMap<>();
|
||||
|
||||
/** Starts a connection manager, to manage, connections. */
|
||||
public NetworkingClientManager() {
|
||||
GlobalEventBus.subscribeAndRegister(this::handleStartClientRequest);
|
||||
GlobalEventBus.subscribeAndRegister(this::handleStartClient);
|
||||
GlobalEventBus.subscribeAndRegister(this::handleCommand);
|
||||
GlobalEventBus.subscribeAndRegister(this::handleCloseClient);
|
||||
// GlobalEventBus.subscribeAndRegister(
|
||||
// Events.ServerEvents.Reconnect.class, this::handleReconnect);
|
||||
// GlobalEventBus.subscribeAndRegister(Events.ServerEvents.ChangeConnection.class,
|
||||
// this::handleChangeConnection);
|
||||
GlobalEventBus.subscribeAndRegister(this::shutdownAll);
|
||||
GlobalEventBus.subscribeAndRegister(this::getAllConnections);
|
||||
}
|
||||
|
||||
private String startConnectionRequest(Supplier<? extends NetworkingGameClientHandler> handlerFactory,
|
||||
String ip,
|
||||
int port) {
|
||||
String connectionUuid = UUID.randomUUID().toString();
|
||||
try {
|
||||
NetworkingClient client = new NetworkingClient(
|
||||
handlerFactory,
|
||||
ip,
|
||||
port);
|
||||
this.networkClients.put(connectionUuid, client);
|
||||
} catch (Exception e) {
|
||||
logger.error(e);
|
||||
}
|
||||
return connectionUuid;
|
||||
}
|
||||
|
||||
private void handleStartClientRequest(NetworkEvents.StartClientRequest request) {
|
||||
request.future()
|
||||
.complete(
|
||||
this.startConnectionRequest(
|
||||
request.handlerFactory(),
|
||||
request.ip(),
|
||||
request.port())); // TODO: Maybe post ConnectionEstablished event.
|
||||
}
|
||||
|
||||
private void handleStartClient(NetworkEvents.StartClient event) {
|
||||
GlobalEventBus.post(
|
||||
new NetworkEvents.StartClientSuccess(
|
||||
this.startConnectionRequest(
|
||||
event.handlerFactory(),
|
||||
event.ip(),
|
||||
event.port()),
|
||||
event.ip(),
|
||||
event.port(),
|
||||
event.eventId()
|
||||
));
|
||||
}
|
||||
|
||||
private void handleCommand(
|
||||
NetworkEvents.SendCommand
|
||||
event) { // TODO: Move this to ServerConnection class, keep it internal.
|
||||
NetworkingClient client = this.networkClients.get(event.connectionId());
|
||||
logger.info("Preparing to send command: {} to server: {}", event.args(), client);
|
||||
if (client != null) {
|
||||
String args = String.join(" ", event.args()) + "\n";
|
||||
client.writeAndFlush(args);
|
||||
} else {
|
||||
logger.warn("Server {} not found for command '{}'", event.connectionId(), event.args());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCloseClient(NetworkEvents.CloseClient event) {
|
||||
NetworkingClient client = this.networkClients.get(event.connectionId());
|
||||
client.closeConnection(); // TODO: Check if not blocking, what if error, mb not remove?
|
||||
this.networkClients.remove(event.connectionId());
|
||||
logger.info("Client {} closed successfully.", event.connectionId());
|
||||
}
|
||||
|
||||
// private void handleReconnect(Events.ServerEvents.Reconnect event) {
|
||||
// NetworkingClient client = this.networkClients.get(event.connectionId());
|
||||
// if (client != null) {
|
||||
// try {
|
||||
// client;
|
||||
// logger.info("Server {} reconnected", event.connectionId());
|
||||
// } catch (Exception e) {
|
||||
// logger.error("Server {} failed to reconnect", event.connectionId(), e);
|
||||
// GlobalEventBus.post(new Events.ServerEvents.CouldNotConnect(event.connectionId()));
|
||||
// }
|
||||
// }
|
||||
// } // TODO: Reconnect on disconnect
|
||||
|
||||
// private void handleChangeConnection(Events.ServerEvents.ChangeConnection event) {
|
||||
// ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
|
||||
// if (serverConnection != null) {
|
||||
// try {
|
||||
// serverConnection.connect(event.ip(), event.port());
|
||||
// logger.info("Server {} changed connection to {}:{}", event.connectionId(),
|
||||
// event.ip(), event.port());
|
||||
// } catch (Exception e) {
|
||||
// logger.error("Server {} failed to change connection", event.connectionId(),
|
||||
// e);
|
||||
// GlobalEventBus.post(new
|
||||
// Events.ServerEvents.CouldNotConnect(event.connectionId()));
|
||||
// }
|
||||
// }
|
||||
// } TODO
|
||||
|
||||
private void getAllConnections(NetworkEvents.RequestsAllClients request) {
|
||||
List<NetworkingClient> a = new ArrayList<>(this.networkClients.values());
|
||||
request.future().complete(a.toString());
|
||||
}
|
||||
|
||||
public void shutdownAll(NetworkEvents.ForceCloseAllClients request) {
|
||||
this.networkClients.values().forEach(NetworkingClient::closeConnection);
|
||||
this.networkClients.clear();
|
||||
logger.info("All servers shut down");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.toop.frontend.networking;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.Main;
|
||||
|
||||
public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
|
||||
private static final Logger logger = LogManager.getLogger(NetworkingGameClientHandler.class);
|
||||
|
||||
public NetworkingGameClientHandler() {}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
||||
logger.debug("Received message from server-{}, data: {}", ctx.channel().remoteAddress(), msg);
|
||||
|
||||
// TODO: Handle server messages
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
logger.error(cause.getMessage(), cause);
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package org.toop.frontend;
|
||||
package org.toop.frontend.networking;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.*;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.toop.eventbus.Events;
|
||||
import org.toop.eventbus.events.Events;
|
||||
import org.toop.eventbus.GlobalEventBus;
|
||||
import org.toop.eventbus.events.NetworkEvents;
|
||||
|
||||
public final class ServerConnection extends TcpClient implements Runnable {
|
||||
|
||||
@@ -85,7 +86,7 @@ public final class ServerConnection extends TcpClient implements Runnable {
|
||||
logger.info("Connection: {} received: '{}'", this.uuid, received);
|
||||
// this.addReceivedMessageToQueue(received); // TODO: Will never go empty
|
||||
GlobalEventBus.post(
|
||||
new Events.ServerEvents.ReceivedMessage(
|
||||
new NetworkEvents.ReceivedMessage(
|
||||
this.uuid, received)); // TODO: mb change
|
||||
} else {
|
||||
break;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.toop.frontend;
|
||||
package org.toop.frontend.networking;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
@@ -0,0 +1,12 @@
|
||||
//package org.toop.frontend.networking.handlers;
|
||||
//
|
||||
//import io.netty.channel.ChannelHandlerContext;
|
||||
//import org.apache.logging.log4j.LogManager;
|
||||
//import org.apache.logging.log4j.Logger;
|
||||
//import org.toop.frontend.networking.NetworkingGameClientHandler;
|
||||
//
|
||||
//public class NetworkingTicTacToeClientHandler extends NetworkingGameClientHandler {
|
||||
// static final Logger logger = LogManager.getLogger(NetworkingTicTacToeClientHandler.class);
|
||||
//
|
||||
//
|
||||
//}
|
||||
@@ -4,6 +4,7 @@ import org.lwjgl.glfw.*;
|
||||
import org.lwjgl.system.*;
|
||||
import org.toop.core.*;
|
||||
import org.toop.eventbus.*;
|
||||
import org.toop.eventbus.events.Events;
|
||||
|
||||
public class GlfwWindow extends Window {
|
||||
private long window;
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.toop.frontend.platform.graphics.opengl;
|
||||
import org.lwjgl.opengl.*;
|
||||
import org.lwjgl.system.*;
|
||||
import org.toop.eventbus.*;
|
||||
import org.toop.eventbus.events.Events;
|
||||
import org.toop.frontend.graphics.Renderer;
|
||||
import org.toop.frontend.graphics.Shader;
|
||||
|
||||
|
||||
88
src/test/java/org/toop/eventbus/EventPublisherSpeedTest.java
Normal file
88
src/test/java/org/toop/eventbus/EventPublisherSpeedTest.java
Normal file
@@ -0,0 +1,88 @@
|
||||
package org.toop.eventbus;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.toop.eventbus.events.EventWithUuid;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class EventPublisherPerformanceTest {
|
||||
|
||||
public record PerfEvent(String name, String eventId) implements EventWithUuid {
|
||||
@Override
|
||||
public java.util.Map<String, Object> result() {
|
||||
return java.util.Map.of("name", name, "eventId", eventId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEventCreationSpeed() {
|
||||
int iterations = 10_000;
|
||||
long start = System.nanoTime();
|
||||
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
new EventPublisher<>(PerfEvent.class, "event-" + i);
|
||||
}
|
||||
|
||||
long end = System.nanoTime();
|
||||
long durationMs = (end - start) / 1_000_000;
|
||||
|
||||
System.out.println("Created " + iterations + " events in " + durationMs + " ms");
|
||||
assertTrue(durationMs < 500, "Event creation too slow");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEventPostSpeed() {
|
||||
int iterations = 10_000;
|
||||
AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
GlobalEventBus.subscribeAndRegister(PerfEvent.class, e -> counter.incrementAndGet());
|
||||
|
||||
long start = System.nanoTime();
|
||||
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
new EventPublisher<>(PerfEvent.class, "event-" + i).postEvent();
|
||||
}
|
||||
|
||||
long end = System.nanoTime();
|
||||
long durationMs = (end - start) / 1_000_000;
|
||||
|
||||
System.out.println("Posted " + iterations + " events in " + durationMs + " ms");
|
||||
assertTrue(counter.get() == iterations, "Not all events were received");
|
||||
assertTrue(durationMs < 1000, "Posting events too slow");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConcurrentEventPostSpeed() throws InterruptedException {
|
||||
int threads = 20;
|
||||
int eventsPerThread = 5_000;
|
||||
AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
GlobalEventBus.subscribeAndRegister(PerfEvent.class, e -> counter.incrementAndGet());
|
||||
|
||||
Thread[] workers = new Thread[threads];
|
||||
|
||||
long start = System.nanoTime();
|
||||
|
||||
for (int t = 0; t < threads; t++) {
|
||||
workers[t] = new Thread(() -> {
|
||||
for (int i = 0; i < eventsPerThread; i++) {
|
||||
new EventPublisher<>(PerfEvent.class, "event-" + i).postEvent();
|
||||
}
|
||||
});
|
||||
workers[t].start();
|
||||
}
|
||||
|
||||
for (Thread worker : workers) {
|
||||
worker.join();
|
||||
}
|
||||
|
||||
long end = System.nanoTime();
|
||||
long durationMs = (end - start) / 1_000_000;
|
||||
|
||||
System.out.println("Posted " + (threads * eventsPerThread) + " events concurrently in " + durationMs + " ms");
|
||||
assertTrue(counter.get() == threads * eventsPerThread, "Some events were lost");
|
||||
assertTrue(durationMs < 5000, "Concurrent posting too slow");
|
||||
}
|
||||
}
|
||||
160
src/test/java/org/toop/eventbus/EventPublisherStressTest.java
Normal file
160
src/test/java/org/toop/eventbus/EventPublisherStressTest.java
Normal file
@@ -0,0 +1,160 @@
|
||||
package org.toop.eventbus;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.toop.eventbus.events.EventWithUuid;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class EventPublisherStressTest {
|
||||
|
||||
public record HeavyEvent(String payload, String eventId) implements EventWithUuid {
|
||||
@Override
|
||||
public java.util.Map<String, Object> result() {
|
||||
return java.util.Map.of("payload", payload, "eventId", eventId);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int THREADS = 100;
|
||||
private static final long EVENTS_PER_THREAD = 2_000_000;
|
||||
|
||||
@Tag("stress")
|
||||
@Test
|
||||
void extremeConcurrencyTest_progressWithMemory() throws InterruptedException {
|
||||
AtomicLong counter = new AtomicLong(0); // Big numbers safety
|
||||
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
|
||||
|
||||
GlobalEventBus.subscribeAndRegister(HeavyEvent.class, _ -> counter.incrementAndGet());
|
||||
|
||||
BigInteger totalEvents = BigInteger.valueOf(THREADS)
|
||||
.multiply(BigInteger.valueOf(EVENTS_PER_THREAD));
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// Monitor thread for EPS and memory
|
||||
Thread monitor = new Thread(() -> {
|
||||
long lastCount = 0;
|
||||
long lastTime = startTime;
|
||||
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
|
||||
while (counter.get() < totalEvents.longValue()) {
|
||||
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long completed = counter.get();
|
||||
long eventsThisSecond = completed - lastCount;
|
||||
double eps = eventsThisSecond / ((now - lastTime) / 1000.0);
|
||||
|
||||
// Memory usage
|
||||
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
|
||||
long maxMemory = runtime.maxMemory();
|
||||
double usedPercent = usedMemory * 100.0 / maxMemory;
|
||||
|
||||
System.out.printf(
|
||||
"Progress: %d/%d (%.2f%%), EPS: %.0f, Memory Used: %.2f MB (%.2f%%)\n",
|
||||
completed,
|
||||
totalEvents.longValue(),
|
||||
completed * 100.0 / totalEvents.doubleValue(),
|
||||
eps,
|
||||
usedMemory / 1024.0 / 1024.0,
|
||||
usedPercent
|
||||
);
|
||||
|
||||
lastCount = completed;
|
||||
lastTime = now;
|
||||
}
|
||||
});
|
||||
monitor.setDaemon(true);
|
||||
monitor.start();
|
||||
|
||||
// Submit events
|
||||
for (int t = 0; t < THREADS; t++) {
|
||||
executor.submit(() -> {
|
||||
for (int i = 0; i < EVENTS_PER_THREAD; i++) {
|
||||
new EventPublisher<>(HeavyEvent.class, "payload-" + i).postEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(20, TimeUnit.MINUTES); // allow extra time for huge tests
|
||||
|
||||
long endTime = System.currentTimeMillis();
|
||||
double durationSeconds = (endTime - startTime) / 1000.0;
|
||||
|
||||
System.out.println("Posted " + totalEvents + " events in " + durationSeconds + " seconds");
|
||||
double averageEps = totalEvents.doubleValue() / durationSeconds;
|
||||
System.out.printf("Average EPS: %.0f%n", averageEps);
|
||||
|
||||
assertEquals(totalEvents.longValue(), counter.get());
|
||||
}
|
||||
|
||||
@Tag("stress")
|
||||
@Test
|
||||
void efficientExtremeConcurrencyTest() throws InterruptedException {
|
||||
final int THREADS = Runtime.getRuntime().availableProcessors(); // threads ≈ CPU cores
|
||||
final int EVENTS_PER_THREAD = 5000;
|
||||
|
||||
ExecutorService executor = Executors.newFixedThreadPool(THREADS);
|
||||
ConcurrentLinkedQueue<HeavyEvent> processedEvents = new ConcurrentLinkedQueue<>();
|
||||
|
||||
long start = System.nanoTime();
|
||||
|
||||
for (int t = 0; t < THREADS; t++) {
|
||||
executor.submit(() -> {
|
||||
for (int i = 0; i < EVENTS_PER_THREAD; i++) {
|
||||
new EventPublisher<>(HeavyEvent.class, "payload-" + i)
|
||||
.onEventById(HeavyEvent.class, processedEvents::add)
|
||||
.postEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(10, TimeUnit.MINUTES);
|
||||
|
||||
long end = System.nanoTime();
|
||||
double durationSeconds = (end - start) / 1_000_000_000.0;
|
||||
|
||||
BigInteger totalEvents = BigInteger.valueOf((long) THREADS).multiply(BigInteger.valueOf(EVENTS_PER_THREAD));
|
||||
double eps = totalEvents.doubleValue() / durationSeconds;
|
||||
|
||||
System.out.printf("Posted %s events in %.3f seconds%n", totalEvents, durationSeconds);
|
||||
System.out.printf("Throughput: %.0f events/sec%n", eps);
|
||||
|
||||
// Memory snapshot
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
System.out.printf("Used memory: %.2f MB%n", (rt.totalMemory() - rt.freeMemory()) / 1024.0 / 1024.0);
|
||||
|
||||
// Ensure all events were processed
|
||||
assertEquals(totalEvents.intValue(), processedEvents.size());
|
||||
}
|
||||
|
||||
@Tag("stress")
|
||||
@Test
|
||||
void constructorCacheVsReflection() throws Throwable {
|
||||
int iterations = 1_000_000;
|
||||
long startReflect = System.nanoTime();
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
// Reflection every time
|
||||
HeavyEvent.class.getDeclaredConstructors()[0].newInstance("payload", "uuid-" + i);
|
||||
}
|
||||
long endReflect = System.nanoTime();
|
||||
|
||||
long startHandle = System.nanoTime();
|
||||
for (int i = 0; i < iterations; i++) {
|
||||
// Using cached MethodHandle
|
||||
EventPublisher<HeavyEvent> ep = new EventPublisher<>(HeavyEvent.class, "payload-" + i);
|
||||
}
|
||||
long endHandle = System.nanoTime();
|
||||
|
||||
System.out.println("Reflection: " + (endReflect - startReflect) / 1_000_000 + " ms");
|
||||
System.out.println("MethodHandle Cache: " + (endHandle - startHandle) / 1_000_000 + " ms");
|
||||
}
|
||||
}
|
||||
122
src/test/java/org/toop/eventbus/EventPublisherTest.java
Normal file
122
src/test/java/org/toop/eventbus/EventPublisherTest.java
Normal file
@@ -0,0 +1,122 @@
|
||||
package org.toop.eventbus;
|
||||
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.toop.eventbus.events.EventWithUuid;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class EventPublisherTest {
|
||||
|
||||
// Simple test event implementing EventWithUuid
|
||||
public record TestEvent(String name, String eventId) implements EventWithUuid {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of("name", name, "eventId", eventId);
|
||||
}
|
||||
}
|
||||
|
||||
public record TestResponseEvent(String msg, String eventId) implements EventWithUuid {
|
||||
@Override
|
||||
public Map<String, Object> result() {
|
||||
return Map.of("msg", msg, "eventId", eventId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEventPublisherGeneratesUuid() {
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "myTest");
|
||||
assertNotNull(publisher.getEventId());
|
||||
assertEquals(publisher.getEventId(), publisher.getEvent().eventId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPostEvent() {
|
||||
AtomicBoolean triggered = new AtomicBoolean(false);
|
||||
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "myTest");
|
||||
publisher.onEventById(TestEvent.class, event -> triggered.set(true))
|
||||
.postEvent();
|
||||
|
||||
assertTrue(triggered.get(), "Subscriber should have been triggered by postEvent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnEventByIdMatchesUuid() {
|
||||
AtomicBoolean triggered = new AtomicBoolean(false);
|
||||
|
||||
EventPublisher<TestEvent> publisher1 = new EventPublisher<>(TestEvent.class, "event1");
|
||||
EventPublisher<TestEvent> publisher2 = new EventPublisher<>(TestEvent.class, "event2");
|
||||
|
||||
publisher1.onEventById(TestEvent.class, event -> triggered.set(true));
|
||||
publisher2.postEvent();
|
||||
|
||||
// Only publisher1's subscriber should trigger for its UUID
|
||||
assertFalse(triggered.get(), "Subscriber should not trigger for a different UUID");
|
||||
|
||||
publisher1.postEvent();
|
||||
assertTrue(triggered.get(), "Subscriber should trigger for matching UUID");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnregisterAfterSuccess() {
|
||||
AtomicBoolean triggered = new AtomicBoolean(false);
|
||||
AtomicReference<Object> listenerRef = new AtomicReference<>();
|
||||
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "event");
|
||||
publisher.onEventById(TestEvent.class, event -> triggered.set(true))
|
||||
.unregisterAfterSuccess()
|
||||
.postEvent();
|
||||
|
||||
// Subscriber should have been removed after first trigger
|
||||
assertTrue(triggered.get(), "Subscriber should trigger first time");
|
||||
|
||||
triggered.set(false);
|
||||
publisher.postEvent();
|
||||
assertFalse(triggered.get(), "Subscriber should not trigger after unregister");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResultMapPopulated() {
|
||||
AtomicReference<Map<String, Object>> resultRef = new AtomicReference<>();
|
||||
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "myName");
|
||||
publisher.onEventById(TestEvent.class, event -> resultRef.set(event.result()))
|
||||
.postEvent();
|
||||
|
||||
Map<String, Object> result = resultRef.get();
|
||||
assertNotNull(result);
|
||||
assertEquals("myName", result.get("name"));
|
||||
assertEquals(publisher.getEventId(), result.get("eventId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleSubscribers() {
|
||||
AtomicBoolean firstTriggered = new AtomicBoolean(false);
|
||||
AtomicBoolean secondTriggered = new AtomicBoolean(false);
|
||||
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "multi");
|
||||
|
||||
publisher.onEventById(TestEvent.class, e -> firstTriggered.set(true))
|
||||
.onEventById(TestEvent.class, e -> secondTriggered.set(true))
|
||||
.postEvent();
|
||||
|
||||
assertTrue(firstTriggered.get());
|
||||
assertTrue(secondTriggered.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEventInstanceCreatedCorrectly() {
|
||||
EventPublisher<TestEvent> publisher = new EventPublisher<>(TestEvent.class, "hello");
|
||||
TestEvent event = publisher.getEvent();
|
||||
assertNotNull(event);
|
||||
assertEquals("hello", event.name());
|
||||
assertEquals(publisher.getEventId(), event.eventId());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user