diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index e917175..655cfae 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -2,7 +2,7 @@ diff --git a/app/src/main/java/org/toop/Main.java b/app/src/main/java/org/toop/Main.java index 69b1b86..c929d5d 100644 --- a/app/src/main/java/org/toop/Main.java +++ b/app/src/main/java/org/toop/Main.java @@ -2,28 +2,25 @@ package org.toop; import org.toop.app.App; import org.toop.framework.audio.*; +import org.toop.framework.networking.NetworkingClientEventListener; import org.toop.framework.networking.NetworkingClientManager; -import org.toop.framework.networking.NetworkingInitializationException; import org.toop.framework.resource.ResourceLoader; import org.toop.framework.resource.ResourceManager; -import org.toop.framework.resource.ResourceMeta; import org.toop.framework.resource.resources.MusicAsset; import org.toop.framework.resource.resources.SoundEffectAsset; -import java.util.Arrays; -import java.util.List; - public final class Main { static void main(String[] args) { initSystems(); App.run(args); } - private static void initSystems() throws NetworkingInitializationException { + private static void initSystems() { ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets")); - new Thread(NetworkingClientManager::new).start(); + new Thread(() -> new NetworkingClientEventListener(new NetworkingClientManager())).start(); + new Thread(() -> { - MusicManager musicManager = new MusicManager<>(ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class)); + MusicManager musicManager = new MusicManager<>(ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class), true); SoundEffectManager soundEffectManager = new SoundEffectManager<>(ResourceManager.getAllOfType(SoundEffectAsset.class)); AudioVolumeManager audioVolumeManager = new AudioVolumeManager() .registerManager(VolumeControl.MASTERVOLUME, musicManager) diff --git a/app/src/main/java/org/toop/app/App.java b/app/src/main/java/org/toop/app/App.java index 64513d5..d9d1f7f 100644 --- a/app/src/main/java/org/toop/app/App.java +++ b/app/src/main/java/org/toop/app/App.java @@ -79,6 +79,7 @@ public final class App extends Application { public static void quit() { ViewStack.cleanup(); stage.close(); + System.exit(0); // TODO: This is like dropping a nuke } public static void reload() { diff --git a/app/src/main/java/org/toop/app/Server.java b/app/src/main/java/org/toop/app/Server.java index 8ec565a..8d71c4a 100644 --- a/app/src/main/java/org/toop/app/Server.java +++ b/app/src/main/java/org/toop/app/Server.java @@ -1,5 +1,6 @@ package org.toop.app; +import com.google.common.util.concurrent.AbstractScheduledService; import org.toop.app.game.ReversiGame; import org.toop.app.game.TicTacToeGame; import org.toop.app.view.ViewStack; @@ -9,19 +10,26 @@ import org.toop.app.view.views.OnlineView; import org.toop.app.view.views.SendChallengeView; import org.toop.app.view.views.ServerView; import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.clients.TournamentNetworkingClient; import org.toop.framework.networking.events.NetworkEvents; +import org.toop.framework.networking.interfaces.NetworkingClient; +import org.toop.framework.networking.types.NetworkingConnector; import org.toop.local.AppContext; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; public final class Server { private String user = ""; private long clientId = -1; private List onlinePlayers = new CopyOnWriteArrayList(); + private List gameList = new CopyOnWriteArrayList<>(); private ServerView view; @@ -58,8 +66,12 @@ public final class Server { } new EventFlow() - .addPostEvent(NetworkEvents.StartClient.class, ip, parsedPort) + .addPostEvent(NetworkEvents.StartClient.class, + new TournamentNetworkingClient(), + new NetworkingConnector(ip, parsedPort, 10, 1, TimeUnit.SECONDS) + ) .onResponse(NetworkEvents.StartClientResponse.class, e -> { + // TODO add if unsuccessful this.user = user; clientId = e.clientId(); @@ -68,30 +80,23 @@ public final class Server { view = new ServerView(user, this::sendChallenge, this::disconnect); ViewStack.push(view); - startPopulateThread(); + startPopulateScheduler(); + + populateGameList(); + }).postEvent(); new EventFlow().listen(this::handleReceivedChallenge); } - private void populatePlayerList() { - new EventFlow().listen(NetworkEvents.PlayerlistResponse.class, e -> { - if (e.clientId() == clientId) { - onlinePlayers = new ArrayList(List.of(e.playerlist())); - onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); + private void populatePlayerList(ScheduledExecutorService scheduler, Runnable populatingTask) { - view.update(onlinePlayers); - } - }); + final long DELAY = 5; - final EventFlow sendGetPlayerList = new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)); - - while (isPolling) { - sendGetPlayerList.postEvent(); - - try { - Thread.sleep(5000); - } catch (InterruptedException _) {} + if (!isPolling) scheduler.shutdown(); + else { + populatingTask.run(); + scheduler.schedule(() -> populatePlayerList(scheduler, populatingTask), DELAY, TimeUnit.SECONDS); } } @@ -173,6 +178,7 @@ public final class Server { private void disconnect() { new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent(); + isPolling = false; ViewStack.push(new OnlineView()); } @@ -184,27 +190,39 @@ public final class Server { forfeitGame(); ViewStack.push(view); - startPopulateThread(); + startPopulateScheduler(); } - private void startPopulateThread() { + private void startPopulateScheduler() { isPolling = true; - final Thread populateThread = new Thread(this::populatePlayerList); - populateThread.setDaemon(false); - populateThread.start(); + EventFlow getPlayerlistFlow = new EventFlow() + .addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)) + .listen(NetworkEvents.PlayerlistResponse.class, e -> { + if (e.clientId() == clientId) { + onlinePlayers = new ArrayList<>(List.of(e.playerlist())); + onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user)); + + view.update(onlinePlayers); + } + }, false); + + final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.schedule(() -> populatePlayerList(scheduler, getPlayerlistFlow::postEvent), 0, TimeUnit.MILLISECONDS); } - public List getGamesList() { - final List list = new ArrayList(); - list.add("tic-tac-toe"); // Todo: get games list from server and check if the game is supported - list.add("reversi"); + private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) { + gameList.addAll(List.of(event.gamelist())); + } + public void populateGameList() { new EventFlow().addPostEvent(new NetworkEvents.SendGetGamelist(clientId)) - .listen(NetworkEvents.GamelistResponse.class, e -> { - System.out.println(Arrays.toString(e.gamelist())); - }).postEvent(); - - return list; + .listen(NetworkEvents.GamelistResponse.class, + this::gamesListFromServerHandler, true + ).postEvent(); } + + public List getGameList() { + return gameList; + } } \ No newline at end of file diff --git a/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java b/app/src/main/java/org/toop/app/layer/layers/ConnectedLayer.java new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/org/toop/app/view/views/SendChallengeView.java b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java index a3bde9b..5d88a13 100644 --- a/app/src/main/java/org/toop/app/view/views/SendChallengeView.java +++ b/app/src/main/java/org/toop/app/view/views/SendChallengeView.java @@ -46,7 +46,7 @@ public final class SendChallengeView extends View { gameText.setText(AppContext.getString("to-a-game-of")); final ComboBox gamesCombobox = combobox(); - gamesCombobox.getItems().addAll(server.getGamesList()); + gamesCombobox.getItems().addAll(server.getGameList()); gamesCombobox.setValue(gamesCombobox.getItems().getFirst()); final Button sendButton = button(); diff --git a/framework/src/main/java/org/toop/framework/audio/MusicManager.java b/framework/src/main/java/org/toop/framework/audio/MusicManager.java index 2be1911..1d34765 100644 --- a/framework/src/main/java/org/toop/framework/audio/MusicManager.java +++ b/framework/src/main/java/org/toop/framework/audio/MusicManager.java @@ -26,10 +26,13 @@ public class MusicManager implements org.toop.framework private ScheduledExecutorService scheduler; - public MusicManager(List resources) { + public MusicManager(List resources, boolean shuffleMusic) { this.dispatcher = new JavaFXDispatcher(); this.resources = resources; - createShuffled(); + // Shuffle if wanting to shuffle + if (shuffleMusic) createShuffled(); + else backgroundMusic.addAll(resources); + // ------------------------------ } /** diff --git a/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java b/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java index 6c8745f..ee6bdfb 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java +++ b/framework/src/main/java/org/toop/framework/eventbus/GlobalEventBus.java @@ -134,7 +134,8 @@ public final class GlobalEventBus { for (Consumer listener : classListeners) { try { listener.accept(event); - } catch (Throwable ignored) { + } catch (Throwable e) { +// e.printStackTrace(); } } } @@ -146,7 +147,8 @@ public final class GlobalEventBus { for (Consumer listener : genericListeners) { try { listener.accept(event); - } catch (Throwable ignored) { + } catch (Throwable e) { + // e.printStackTrace(); } } } diff --git a/framework/src/main/java/org/toop/framework/eventbus/events/ResponseToUniqueEvent.java b/framework/src/main/java/org/toop/framework/eventbus/events/ResponseToUniqueEvent.java index 30328ce..6f65378 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/events/ResponseToUniqueEvent.java +++ b/framework/src/main/java/org/toop/framework/eventbus/events/ResponseToUniqueEvent.java @@ -4,6 +4,17 @@ import java.lang.reflect.RecordComponent; import java.util.HashMap; import java.util.Map; +/** + * MUST HAVE long identifier at the end. + * e.g. + * + *
{@code
+ * public record uniqueEventResponse(String content, long identifier) implements ResponseToUniqueEvent {};
+ * public record uniqueEventResponse(long identifier) implements ResponseToUniqueEvent {};
+ * public record uniqueEventResponse(String content, int number, long identifier) implements ResponseToUniqueEvent {};
+ * }
+ * + */ public interface ResponseToUniqueEvent extends UniqueEvent { default Map result() { Map map = new HashMap<>(); diff --git a/framework/src/main/java/org/toop/framework/eventbus/events/UniqueEvent.java b/framework/src/main/java/org/toop/framework/eventbus/events/UniqueEvent.java index bb68f61..6042c83 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/events/UniqueEvent.java +++ b/framework/src/main/java/org/toop/framework/eventbus/events/UniqueEvent.java @@ -1,5 +1,16 @@ package org.toop.framework.eventbus.events; +/** + * MUST HAVE long identifier at the end. + * e.g. + * + *
{@code
+ * public record uniqueEvent(String content, long identifier) implements UniqueEvent {};
+ * public record uniqueEvent(long identifier) implements UniqueEvent {};
+ * public record uniqueEvent(String content, int number, long identifier) implements UniqueEvent {};
+ * }
+ * + */ public interface UniqueEvent extends EventType { default long getIdentifier() { try { diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingClientEventListener.java b/framework/src/main/java/org/toop/framework/networking/NetworkingClientEventListener.java new file mode 100644 index 0000000..ef51a46 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/NetworkingClientEventListener.java @@ -0,0 +1,150 @@ +package org.toop.framework.networking; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.toop.framework.SnowflakeGenerator; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; +import org.toop.framework.networking.exceptions.ClientNotFoundException; +import org.toop.framework.networking.interfaces.NetworkingClientManager; + +public class NetworkingClientEventListener { + + private static final Logger logger = LogManager.getLogger(NetworkingClientEventListener.class); + private final NetworkingClientManager clientManager; + + /** Starts a connection manager, to manage, connections. */ + public NetworkingClientEventListener(NetworkingClientManager clientManager) { + this.clientManager = clientManager; + new EventFlow() + .listen(this::handleStartClient) + .listen(this::handleCommand) + .listen(this::handleSendLogin) + .listen(this::handleSendLogout) + .listen(this::handleSendGetPlayerlist) + .listen(this::handleSendGetGamelist) + .listen(this::handleSendSubscribe) + .listen(this::handleSendMove) + .listen(this::handleSendChallenge) + .listen(this::handleSendAcceptChallenge) + .listen(this::handleSendForfeit) + .listen(this::handleSendMessage) + .listen(this::handleSendHelp) + .listen(this::handleSendHelpForCommand) + .listen(this::handleCloseClient) + .listen(this::handleReconnect) + .listen(this::handleChangeAddress) + .listen(this::handleGetAllConnections) + .listen(this::handleShutdownAll); + } + + void handleStartClient(NetworkEvents.StartClient event) { + long clientId = SnowflakeGenerator.nextId(); + clientManager.startClient( + clientId, + event.networkingClient(), + event.networkingConnector(), + () -> new EventFlow().addPostEvent(new NetworkEvents.StartClientResponse(clientId, true, event.identifier())).postEvent(), + () -> new EventFlow().addPostEvent(new NetworkEvents.StartClientResponse(clientId, false, event.identifier())).postEvent() + ); + } + + private void sendCommand(long clientId, String command) { + try { + clientManager.sendCommand(clientId, command); + } catch (ClientNotFoundException e) { + logger.error(e); + } + } + + private void handleCommand(NetworkEvents.SendCommand event) { + String args = String.join(" ", event.args()); + sendCommand(event.clientId(), args); + } + + private void handleSendLogin(NetworkEvents.SendLogin event) { + sendCommand(event.clientId(), String.format("LOGIN %s", event.username())); + } + + private void handleSendLogout(NetworkEvents.SendLogout event) { + sendCommand(event.clientId(), "LOGOUT"); + } + + private void handleSendGetPlayerlist(NetworkEvents.SendGetPlayerlist event) { + sendCommand(event.clientId(), "GET PLAYERLIST"); + } + + private void handleSendGetGamelist(NetworkEvents.SendGetGamelist event) { + sendCommand(event.clientId(), "GET GAMELIST"); + } + + private void handleSendSubscribe(NetworkEvents.SendSubscribe event) { + sendCommand(event.clientId(), String.format("SUBSCRIBE %s", event.gameType())); + } + + private void handleSendMove(NetworkEvents.SendMove event) { + sendCommand(event.clientId(), String.format("MOVE %d", event.moveNumber())); + } + + private void handleSendChallenge(NetworkEvents.SendChallenge event) { + sendCommand(event.clientId(), String.format("CHALLENGE %s %s", event.usernameToChallenge(), event.gameType())); + } + + private void handleSendAcceptChallenge(NetworkEvents.SendAcceptChallenge event) { + sendCommand(event.clientId(), String.format("CHALLENGE ACCEPT %d", event.challengeId())); + } + + private void handleSendForfeit(NetworkEvents.SendForfeit event) { + sendCommand(event.clientId(), "FORFEIT"); + } + + private void handleSendMessage(NetworkEvents.SendMessage event) { + sendCommand(event.clientId(), String.format("MESSAGE %s", event.message())); + } + + private void handleSendHelp(NetworkEvents.SendHelp event) { + sendCommand(event.clientId(), "HELP"); + } + + private void handleSendHelpForCommand(NetworkEvents.SendHelpForCommand event) { + sendCommand(event.clientId(), String.format("HELP %s", event.command())); + } + + private void handleReconnect(NetworkEvents.Reconnect event) { + clientManager.startClient( + event.clientId(), + event.networkingClient(), + event.networkingConnector(), + () -> new EventFlow().addPostEvent(new NetworkEvents.ReconnectResponse(true, event.identifier())).postEvent(), + () -> new EventFlow().addPostEvent(new NetworkEvents.ReconnectResponse(false, event.identifier())).postEvent() + ); + } + + private void handleChangeAddress(NetworkEvents.ChangeAddress event) { + clientManager.startClient( + event.clientId(), + event.networkingClient(), + event.networkingConnector(), + () -> new EventFlow().addPostEvent(new NetworkEvents.ChangeAddressResponse(true, event.identifier())).postEvent(), + () -> new EventFlow().addPostEvent(new NetworkEvents.ChangeAddressResponse(false, event.identifier())).postEvent() + ); + } + + void handleCloseClient(NetworkEvents.CloseClient event) { + try { + this.clientManager.closeClient(event.clientId()); + } catch (ClientNotFoundException e) { + logger.error(e); + } + } + + void handleGetAllConnections(NetworkEvents.RequestsAllClients request) { +// List a = new ArrayList<>(this.networkClients.values()); +// request.future().complete(a); + // TODO + } + + public void handleShutdownAll(NetworkEvents.ForceCloseAllClients request) { + // TODO + } +} diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java b/framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java index d42ed9b..02434fc 100644 --- a/framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java +++ b/framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java @@ -2,196 +2,118 @@ package org.toop.framework.networking; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.toop.framework.SnowflakeGenerator; -import org.toop.framework.eventbus.EventFlow; -import org.toop.framework.networking.events.NetworkEvents; - -public class NetworkingClientManager { +import org.toop.framework.networking.exceptions.ClientNotFoundException; +import org.toop.framework.networking.exceptions.CouldNotConnectException; +import org.toop.framework.networking.interfaces.NetworkingClient; +import org.toop.framework.networking.types.NetworkingConnector; +public class NetworkingClientManager implements org.toop.framework.networking.interfaces.NetworkingClientManager { private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class); + private final Map networkClients = new ConcurrentHashMap<>(); - /** Map of serverId -> Server instances */ - final Map networkClients = new ConcurrentHashMap<>(); + public NetworkingClientManager() {} - /** Starts a connection manager, to manage, connections. */ - public NetworkingClientManager() throws NetworkingInitializationException { - try { - new EventFlow() - .listen(this::handleStartClient) - .listen(this::handleCommand) - .listen(this::handleSendLogin) - .listen(this::handleSendLogout) - .listen(this::handleSendGetPlayerlist) - .listen(this::handleSendGetGamelist) - .listen(this::handleSendSubscribe) - .listen(this::handleSendMove) - .listen(this::handleSendChallenge) - .listen(this::handleSendAcceptChallenge) - .listen(this::handleSendForfeit) - .listen(this::handleSendMessage) - .listen(this::handleSendHelp) - .listen(this::handleSendHelpForCommand) - .listen(this::handleCloseClient) - .listen(this::handleChangeClientHost) - .listen(this::handleGetAllConnections) - .listen(this::handleShutdownAll); - logger.info("NetworkingClientManager initialized"); - } catch (Exception e) { - logger.error("Failed to initialize the client manager", e); - throw e; + private void connectHelper( + long id, + NetworkingClient nClient, + NetworkingConnector nConnector, + Runnable onSuccess, + Runnable onFailure + ) { + ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + Runnable connectTask = new Runnable() { + int attempts = 0; + + @Override + public void run() { + + NetworkingClient qClient = networkClients.get(id); + if (qClient != null) { + qClient.closeConnection(); + networkClients.remove(id); + } + + try { + nClient.connect(id, nConnector.host(), nConnector.port()); + networkClients.put(id, nClient); + logger.info("New client started successfully for {}:{}", nConnector.host(), nConnector.port()); + onSuccess.run(); + scheduler.shutdown(); + } catch (CouldNotConnectException e) { + attempts++; + if (attempts < nConnector.reconnectAttempts()) { + logger.warn("Could not connect to {}:{}. Retrying in {} {}", + nConnector.host(), nConnector.port(), nConnector.timeout(), nConnector.timeUnit()); + scheduler.schedule(this, nConnector.timeout(), nConnector.timeUnit()); + } else { + logger.error("Failed to start client for {}:{} after {} attempts", nConnector.host(), nConnector.port(), attempts); + onFailure.run(); + scheduler.shutdown(); + } + } catch (Exception e) { + logger.error("Unexpected exception during startClient", e); + onFailure.run(); + scheduler.shutdown(); + } + } + }; + + scheduler.schedule(connectTask, 0, TimeUnit.MILLISECONDS); + } + + @Override + public void startClient( + long id, + NetworkingClient nClient, + NetworkingConnector nConnector, + Runnable onSuccess, + Runnable onFailure + ) { + connectHelper( + id, + nClient, + nConnector, + onSuccess, + onFailure + ); + } + + @Override + public void sendCommand(long id, String command) throws ClientNotFoundException { + logger.info("Sending command to client for {}:{}", id, command); + if (command.isEmpty()) { + IllegalArgumentException e = new IllegalArgumentException("command is empty"); + logger.error("Invalid command received", e); + return; } - } - long startClientRequest(String ip, int port) { - long connectionId = SnowflakeGenerator.nextId(); - try { - NetworkingClient client = - new NetworkingClient( - () -> new NetworkingGameClientHandler(connectionId), - ip, - port, - connectionId); - client.setConnectionId(connectionId); - this.networkClients.put(connectionId, client); - logger.info("New client started successfully for {}:{}", ip, port); - } catch (Exception e) { - logger.error(e); + NetworkingClient client = this.networkClients.get(id); + if (client == null) { + throw new ClientNotFoundException(id); } - return connectionId; + + String toSend = command.trim(); + + if (toSend.endsWith("\n")) { client.writeAndFlush(toSend); } + else { client.writeAndFlush(toSend + "\n"); } + } - private long startClientRequest(String ip, int port, long clientId) { - try { // With EventFlow - NetworkingClient client = - new NetworkingClient( - () -> new NetworkingGameClientHandler(clientId), ip, port, clientId); - client.setConnectionId(clientId); - this.networkClients.replace(clientId, client); - logger.info( - "New client started successfully for {}:{}, replaced: {}", ip, port, clientId); - } catch (Exception e) { - logger.error(e); + @Override + public void closeClient(long id) throws ClientNotFoundException { + NetworkingClient client = this.networkClients.get(id); + if (client == null) { + throw new ClientNotFoundException(id); } - logger.info("Client {} started", clientId); - return clientId; - } - void handleStartClient(NetworkEvents.StartClient event) { - long id = this.startClientRequest(event.ip(), event.port()); - new Thread( - () -> - new EventFlow() - .addPostEvent( - NetworkEvents.StartClientResponse.class, - id, - event.eventSnowflake()) - .asyncPostEvent()) - .start(); - } - - void handleCommand( - NetworkEvents.SendCommand - event) { // TODO: Move this to ServerConnection class, keep it internal. - NetworkingClient client = this.networkClients.get(event.clientId()); - String args = String.join(" ", event.args()); - sendCommand(client, args); - } - - void handleSendLogin(NetworkEvents.SendLogin event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, String.format("LOGIN %s", event.username())); - } - - private void handleSendLogout(NetworkEvents.SendLogout event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, "LOGOUT"); - } - - private void handleSendGetPlayerlist(NetworkEvents.SendGetPlayerlist event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, "GET PLAYERLIST"); - } - - private void handleSendGetGamelist(NetworkEvents.SendGetGamelist event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, "GET GAMELIST"); - } - - private void handleSendSubscribe(NetworkEvents.SendSubscribe event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, String.format("SUBSCRIBE %s", event.gameType())); - } - - private void handleSendMove(NetworkEvents.SendMove event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, String.format("MOVE %d", event.moveNumber())); - } - - private void handleSendChallenge(NetworkEvents.SendChallenge event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand( - client, - String.format("CHALLENGE %s %s", event.usernameToChallenge(), event.gameType())); - } - - private void handleSendAcceptChallenge(NetworkEvents.SendAcceptChallenge event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, String.format("CHALLENGE ACCEPT %d", event.challengeId())); - } - - private void handleSendForfeit(NetworkEvents.SendForfeit event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, "FORFEIT"); - } - - private void handleSendMessage(NetworkEvents.SendMessage event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, String.format("MESSAGE %s", event.message())); - } - - private void handleSendHelp(NetworkEvents.SendHelp event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, "HELP"); - } - - private void handleSendHelpForCommand(NetworkEvents.SendHelpForCommand event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - sendCommand(client, String.format("HELP %s", event.command())); - } - - private void sendCommand(NetworkingClient client, String command) { - logger.info( - "Preparing to send command: {} to server: {}:{}. clientId: {}", - command.trim(), - client.getHost(), - client.getPort(), - client.getId()); - client.writeAndFlushnl(command); - } - - private void handleChangeClientHost(NetworkEvents.ChangeClientHost event) { - NetworkingClient client = this.networkClients.get(event.clientId()); client.closeConnection(); - startClientRequest(event.ip(), event.port(), event.clientId()); - } - void handleCloseClient(NetworkEvents.CloseClient event) { - NetworkingClient client = this.networkClients.get(event.clientId()); - client.closeConnection(); - this.networkClients.remove(event.clientId()); - logger.info("Client {} closed successfully.", event.clientId()); - } - - void handleGetAllConnections(NetworkEvents.RequestsAllClients request) { - List a = new ArrayList<>(this.networkClients.values()); - request.future().complete(a); - } - - public void handleShutdownAll(NetworkEvents.ForceCloseAllClients request) { - this.networkClients.values().forEach(NetworkingClient::closeConnection); - this.networkClients.clear(); - logger.info("All servers shut down"); } } diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingClient.java b/framework/src/main/java/org/toop/framework/networking/clients/TournamentNetworkingClient.java similarity index 58% rename from framework/src/main/java/org/toop/framework/networking/NetworkingClient.java rename to framework/src/main/java/org/toop/framework/networking/clients/TournamentNetworkingClient.java index fd99bf7..74871e4 100644 --- a/framework/src/main/java/org/toop/framework/networking/NetworkingClient.java +++ b/framework/src/main/java/org/toop/framework/networking/clients/TournamentNetworkingClient.java @@ -1,4 +1,4 @@ -package org.toop.framework.networking; +package org.toop.framework.networking.clients; import io.netty.bootstrap.Bootstrap; import io.netty.channel.*; @@ -9,27 +9,27 @@ import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.util.CharsetUtil; -import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.toop.framework.eventbus.EventFlow; -import org.toop.framework.networking.events.NetworkEvents; +import org.toop.framework.networking.exceptions.CouldNotConnectException; +import org.toop.framework.networking.handlers.NetworkingGameClientHandler; +import org.toop.framework.networking.interfaces.NetworkingClient; -public class NetworkingClient { - private static final Logger logger = LogManager.getLogger(NetworkingClient.class); +import java.net.InetSocketAddress; - private long connectionId; - private String host; - private int port; +public class TournamentNetworkingClient implements NetworkingClient { + private static final Logger logger = LogManager.getLogger(TournamentNetworkingClient.class); private Channel channel; - private NetworkingGameClientHandler handler; - public NetworkingClient( - Supplier handlerFactory, - String host, - int port, - long connectionId) { - this.connectionId = connectionId; + public TournamentNetworkingClient() {} + + @Override + public InetSocketAddress getAddress() { + return (InetSocketAddress) channel.remoteAddress(); + } + + @Override + public void connect(long clientId, String host, int port) throws CouldNotConnectException { try { Bootstrap bootstrap = new Bootstrap(); EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); @@ -40,7 +40,7 @@ public class NetworkingClient { new ChannelInitializer() { @Override public void initChannel(SocketChannel ch) { - handler = handlerFactory.get(); + NetworkingGameClientHandler handler = new NetworkingGameClientHandler(clientId); ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n @@ -52,53 +52,28 @@ public class NetworkingClient { }); ChannelFuture channelFuture = bootstrap.connect(host, port).sync(); this.channel = channelFuture.channel(); - this.host = host; - this.port = port; - } catch (Exception e) { - logger.error("Failed to create networking client instance", e); + } catch (Exception _) { + throw new CouldNotConnectException(clientId); } } - public NetworkingGameClientHandler getHandler() { - return this.handler; - } - - public String getHost() { - return this.host; - } - - public int getPort() { - return this.port; - } - - public void setConnectionId(long connectionId) { - this.connectionId = connectionId; - } - - public boolean isChannelActive() { + @Override + public boolean isActive() { return this.channel != null && this.channel.isActive(); } + @Override public void writeAndFlush(String msg) { String literalMsg = msg.replace("\n", "\\n").replace("\r", "\\r"); - if (isChannelActive()) { + if (isActive()) { this.channel.writeAndFlush(msg); - logger.info( - "Connection {} sent message: '{}' ", this.channel.remoteAddress(), literalMsg); + 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 + "\r\n"); - logger.info("Connection {} sent message: '{}'", this.channel.remoteAddress(), msg); - } else { - logger.warn("Cannot send message: '{}', connection inactive.", msg); - } - } - + @Override public void closeConnection() { if (this.channel != null && this.channel.isActive()) { this.channel @@ -109,11 +84,6 @@ public class NetworkingClient { logger.info( "Connection {} closed successfully", this.channel.remoteAddress()); - new EventFlow() - .addPostEvent( - new NetworkEvents.ClosedConnection( - this.connectionId)) - .asyncPostEvent(); } else { logger.error( "Error closing connection {}. Error: {}", @@ -123,8 +93,4 @@ public class NetworkingClient { }); } } - - public long getId() { - return this.connectionId; - } } diff --git a/framework/src/main/java/org/toop/framework/networking/events/NetworkEvents.java b/framework/src/main/java/org/toop/framework/networking/events/NetworkEvents.java index 6574016..ac3de68 100644 --- a/framework/src/main/java/org/toop/framework/networking/events/NetworkEvents.java +++ b/framework/src/main/java/org/toop/framework/networking/events/NetworkEvents.java @@ -3,158 +3,215 @@ package org.toop.framework.networking.events; import java.util.*; import java.util.concurrent.CompletableFuture; -import org.toop.framework.eventbus.events.GenericEvent; -import org.toop.framework.eventbus.events.ResponseToUniqueEvent; -import org.toop.framework.eventbus.events.UniqueEvent; -import org.toop.framework.eventbus.events.EventsBase; import org.toop.annotations.AutoResponseResult; -import org.toop.framework.networking.NetworkingClient; +import org.toop.framework.eventbus.events.*; +import org.toop.framework.networking.interfaces.NetworkingClient; +import org.toop.framework.networking.types.NetworkingConnector; /** - * A collection of networking-related event records for use with the {@link - * org.toop.framework.eventbus.GlobalEventBus}. + * Defines all event types related to the networking subsystem. + *

+ * These events are used in conjunction with the {@link org.toop.framework.eventbus.GlobalEventBus} + * and {@link org.toop.framework.eventbus.EventFlow} to communicate between components + * such as networking clients, managers, and listeners. + *

* - *

This class defines all the events that can be posted or listened to in the networking - * subsystem. Events are separated into those with unique IDs (UniqueEvent) and those without - * (GenericEvent). + *

Important

+ * For all {@link UniqueEvent} and {@link ResponseToUniqueEvent} types: + * the {@code identifier} field is automatically generated and injected + * by {@link org.toop.framework.eventbus.EventFlow}. It should never + * be manually assigned by user code. (Exceptions may apply) */ public class NetworkEvents extends EventsBase { + // ------------------------------------------------------ + // Generic Request & Response Events (no identifier) + // ------------------------------------------------------ + /** - * Requests all active client connections. - * - *

This is a blocking event. The result will be delivered via the provided {@link - * CompletableFuture}. - * - * @param future CompletableFuture to receive the list of active {@link NetworkingClient} - * instances. + * Requests a list of all active networking clients. + *

+ * This is a blocking request that returns the list asynchronously + * via the provided {@link CompletableFuture}. */ public record RequestsAllClients(CompletableFuture> future) implements GenericEvent {} - /** Forces all active client connections to close immediately. */ + /** Signals all active clients should be forcefully closed. */ public record ForceCloseAllClients() implements GenericEvent {} - /** Response indicating a challenge was cancelled. */ - public record ChallengeCancelledResponse(long clientId, String challengeId) implements GenericEvent {} + /** Indicates a challenge was cancelled by the server. */ + public record ChallengeCancelledResponse(long clientId, String challengeId) + implements GenericEvent {} - /** Response indicating a challenge was received. */ + /** Indicates an incoming challenge from another player. */ public record ChallengeResponse(long clientId, String challengerName, String challengeId, String gameType) implements GenericEvent {} - /** Response containing a list of players for a client. */ - public record PlayerlistResponse(long clientId, String[] playerlist) implements GenericEvent {} + /** Contains the list of players currently available on the server. */ + public record PlayerlistResponse(long clientId, String[] playerlist) + implements GenericEvent {} - /** Response containing a list of games for a client. */ - public record GamelistResponse(long clientId, String[] gamelist) implements GenericEvent {} + /** Contains the list of available game types for a client. */ + public record GamelistResponse(long clientId, String[] gamelist) + implements GenericEvent {} - /** Response indicating a game match information for a client. */ + /** Provides match information when a new game starts. */ public record GameMatchResponse(long clientId, String playerToMove, String gameType, String opponent) implements GenericEvent {} - /** Response indicating the result of a game. */ - public record GameResultResponse(long clientId, String condition) implements GenericEvent {} + /** Indicates the outcome or completion of a game. */ + public record GameResultResponse(long clientId, String condition) + implements GenericEvent {} - /** Response indicating a game move occurred. */ - public record GameMoveResponse(long clientId, String player, String move, String details) implements GenericEvent {} + /** Indicates that a game move has been processed or received. */ + public record GameMoveResponse(long clientId, String player, String move, String details) + implements GenericEvent {} - /** Response indicating it is the player's turn. */ + /** Indicates it is the current player's turn to move. */ public record YourTurnResponse(long clientId, String message) implements GenericEvent {} - /** Request to send login credentials for a client. */ - public record SendLogin(long clientId, String username) implements GenericEvent {} + /** Requests a login operation for the given client. */ + public record SendLogin(long clientId, String username) + implements GenericEvent {} - /** Request to log out a client. */ - public record SendLogout(long clientId) implements GenericEvent {} + /** Requests logout for the specified client. */ + public record SendLogout(long clientId) + implements GenericEvent {} - /** Request to retrieve the player list for a client. */ - public record SendGetPlayerlist(long clientId) implements GenericEvent {} + /** Requests the player list from the server. */ + public record SendGetPlayerlist(long clientId) + implements GenericEvent {} - /** Request to retrieve the game list for a client. */ - public record SendGetGamelist(long clientId) implements GenericEvent {} + /** Requests the game list from the server. */ + public record SendGetGamelist(long clientId) + implements GenericEvent {} - /** Request to subscribe a client to a game type. */ - public record SendSubscribe(long clientId, String gameType) implements GenericEvent {} + /** Requests a subscription to updates for a given game type. */ + public record SendSubscribe(long clientId, String gameType) + implements GenericEvent {} - /** Request to make a move in a game. */ - public record SendMove(long clientId, short moveNumber) implements GenericEvent {} + /** Sends a game move command to the server. */ + public record SendMove(long clientId, short moveNumber) + implements GenericEvent {} - /** Request to challenge another player. */ - public record SendChallenge(long clientId, String usernameToChallenge, String gameType) implements GenericEvent {} + /** Requests to challenge another player to a game. */ + public record SendChallenge(long clientId, String usernameToChallenge, String gameType) + implements GenericEvent {} - /** Request to accept a challenge. */ - public record SendAcceptChallenge(long clientId, int challengeId) implements GenericEvent {} + /** Requests to accept an existing challenge. */ + public record SendAcceptChallenge(long clientId, int challengeId) + implements GenericEvent {} - /** Request to forfeit a game. */ - public record SendForfeit(long clientId) implements GenericEvent {} + /** Requests to forfeit the current game. */ + public record SendForfeit(long clientId) + implements GenericEvent {} - /** Request to send a message from a client. */ - public record SendMessage(long clientId, String message) implements GenericEvent {} + /** Sends a chat or informational message from a client. */ + public record SendMessage(long clientId, String message) + implements GenericEvent {} - /** Request to display help to a client. */ - public record SendHelp(long clientId) implements GenericEvent {} + /** Requests general help information from the server. */ + public record SendHelp(long clientId) + implements GenericEvent {} - /** Request to display help for a specific command. */ - public record SendHelpForCommand(long clientId, String command) implements GenericEvent {} + /** Requests help information specific to a given command. */ + public record SendHelpForCommand(long clientId, String command) + implements GenericEvent {} - /** Request to close a specific client connection. */ - public record CloseClient(long clientId) implements GenericEvent {} + /** Requests to close an active client connection. */ + public record CloseClient(long clientId) + implements GenericEvent {} + + /** A generic event indicating a raw server response. */ + public record ServerResponse(long clientId) + implements GenericEvent {} /** - * Event to start a new client connection. + * Sends a raw command string to the server. * - *

Carries IP, port, and a unique event ID for correlation with responses. - * - * @param ip Server IP address. - * @param port Server port. - * @param eventSnowflake Unique event identifier for correlation. - */ - public record StartClient(String ip, int port, long eventSnowflake) implements UniqueEvent {} - - /** - * Response confirming a client was started. - * - * @param clientId The client ID assigned to the new connection. - * @param identifier Event ID used for correlation. - */ - @AutoResponseResult - public record StartClientResponse(long clientId, long identifier) implements ResponseToUniqueEvent {} - - /** Generic server response. */ - public record ServerResponse(long clientId) implements GenericEvent {} - - /** - * Request to send a command to a server. - * - * @param clientId The client connection ID. + * @param clientId The client ID to send the command from. * @param args The command arguments. */ - public record SendCommand(long clientId, String... args) implements GenericEvent {} + public record SendCommand(long clientId, String... args) + implements GenericEvent {} - /** WIP (Not working) Request to reconnect a client to a previous address. */ - public record Reconnect(long clientId) implements GenericEvent {} + /** Event fired when a message is received from the server. */ + public record ReceivedMessage(long clientId, String message) + implements GenericEvent {} + + /** Indicates that a client connection has been closed. */ + public record ClosedConnection(long clientId) + implements GenericEvent {} + + // ------------------------------------------------------ + // Unique Request & Response Events (with identifier) + // ------------------------------------------------------ /** - * Response triggered when a message is received from a server. + * Requests creation and connection of a new client. + *

+ * The {@code identifier} is automatically assigned by {@link org.toop.framework.eventbus.EventFlow} + * to correlate with its corresponding {@link StartClientResponse}. + *

* - * @param clientId The connection ID that received the message. - * @param message The message content. + * @param networkingClient The client instance to start. + * @param networkingConnector Connection details (host, port, etc.). + * @param identifier Automatically injected unique identifier. */ - public record ReceivedMessage(long clientId, String message) implements GenericEvent {} + public record StartClient( + NetworkingClient networkingClient, + NetworkingConnector networkingConnector, + long identifier) + implements UniqueEvent {} /** - * Request to change a client connection to a new server. + * Response confirming that a client has been successfully started. + *

+ * The {@code identifier} value is automatically propagated from + * the original {@link StartClient} request by {@link org.toop.framework.eventbus.EventFlow}. + *

* - * @param clientId The client connection ID. - * @param ip The new server IP. - * @param port The new server port. + * @param clientId The newly assigned client ID. + * @param successful Whether the connection succeeded. + * @param identifier Automatically injected correlation ID. */ - public record ChangeClientHost(long clientId, String ip, int port) implements GenericEvent {} + @AutoResponseResult + public record StartClientResponse(long clientId, boolean successful, long identifier) + implements ResponseToUniqueEvent {} - /** WIP (Not working) Response indicating that the client could not connect. */ - public record CouldNotConnect(long clientId) implements GenericEvent {} + /** + * Requests reconnection of an existing client using its previous configuration. + *

+ * The {@code identifier} is automatically injected by {@link org.toop.framework.eventbus.EventFlow}. + *

+ */ + public record Reconnect( + long clientId, + NetworkingClient networkingClient, + NetworkingConnector networkingConnector, + long identifier) + implements UniqueEvent {} - /** Event indicating a client connection was closed. */ - public record ClosedConnection(long clientId) implements GenericEvent {} + /** Response to a {@link Reconnect} event, carrying the success result. */ + public record ReconnectResponse(boolean successful, long identifier) + implements ResponseToUniqueEvent {} + + /** + * Requests to change the connection target (host/port) for a client. + *

+ * The {@code identifier} is automatically injected by {@link org.toop.framework.eventbus.EventFlow}. + *

+ */ + public record ChangeAddress( + long clientId, + NetworkingClient networkingClient, + NetworkingConnector networkingConnector, + long identifier) + implements UniqueEvent {} + + /** Response to a {@link ChangeAddress} event, carrying the success result. */ + public record ChangeAddressResponse(boolean successful, long identifier) + implements ResponseToUniqueEvent {} } diff --git a/framework/src/main/java/org/toop/framework/networking/exceptions/ClientNotFoundException.java b/framework/src/main/java/org/toop/framework/networking/exceptions/ClientNotFoundException.java new file mode 100644 index 0000000..2506b26 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/exceptions/ClientNotFoundException.java @@ -0,0 +1,25 @@ +package org.toop.framework.networking.exceptions; + +/** + * Thrown when an operation is attempted on a networking client + * that does not exist or has already been closed. + */ +public class ClientNotFoundException extends RuntimeException { + + private final long clientId; + + public ClientNotFoundException(long clientId) { + super("Networking client with ID " + clientId + " was not found."); + this.clientId = clientId; + } + + public ClientNotFoundException(long clientId, Throwable cause) { + super("Networking client with ID " + clientId + " was not found.", cause); + this.clientId = clientId; + } + + public long getClientId() { + return clientId; + } + +} \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/networking/exceptions/CouldNotConnectException.java b/framework/src/main/java/org/toop/framework/networking/exceptions/CouldNotConnectException.java new file mode 100644 index 0000000..839fb0b --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/exceptions/CouldNotConnectException.java @@ -0,0 +1,21 @@ +package org.toop.framework.networking.exceptions; + +public class CouldNotConnectException extends RuntimeException { + + private final long clientId; + + public CouldNotConnectException(long clientId) { + super("Networking client with ID " + clientId + " could not connect."); + this.clientId = clientId; + } + + public CouldNotConnectException(long clientId, Throwable cause) { + super("Networking client with ID " + clientId + " could not connect.", cause); + this.clientId = clientId; + } + + public long getClientId() { + return clientId; + } + +} \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingInitializationException.java b/framework/src/main/java/org/toop/framework/networking/exceptions/NetworkingInitializationException.java similarity index 79% rename from framework/src/main/java/org/toop/framework/networking/NetworkingInitializationException.java rename to framework/src/main/java/org/toop/framework/networking/exceptions/NetworkingInitializationException.java index d9081d1..0ff430a 100644 --- a/framework/src/main/java/org/toop/framework/networking/NetworkingInitializationException.java +++ b/framework/src/main/java/org/toop/framework/networking/exceptions/NetworkingInitializationException.java @@ -1,4 +1,4 @@ -package org.toop.framework.networking; +package org.toop.framework.networking.exceptions; public class NetworkingInitializationException extends RuntimeException { public NetworkingInitializationException(String message, Throwable cause) { diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java b/framework/src/main/java/org/toop/framework/networking/handlers/NetworkingGameClientHandler.java similarity index 99% rename from framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java rename to framework/src/main/java/org/toop/framework/networking/handlers/NetworkingGameClientHandler.java index 388e420..25f9df9 100644 --- a/framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java +++ b/framework/src/main/java/org/toop/framework/networking/handlers/NetworkingGameClientHandler.java @@ -1,4 +1,4 @@ -package org.toop.framework.networking; +package org.toop.framework.networking.handlers; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; diff --git a/framework/src/main/java/org/toop/framework/networking/handlers/NetworkingTicTacToeClientHandler.java b/framework/src/main/java/org/toop/framework/networking/handlers/NetworkingTicTacToeClientHandler.java deleted file mode 100644 index e263072..0000000 --- a/framework/src/main/java/org/toop/framework/networking/handlers/NetworkingTicTacToeClientHandler.java +++ /dev/null @@ -1,12 +0,0 @@ -//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); -// -// -//} diff --git a/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClient.java b/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClient.java new file mode 100644 index 0000000..09b215c --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClient.java @@ -0,0 +1,13 @@ +package org.toop.framework.networking.interfaces; + +import org.toop.framework.networking.exceptions.CouldNotConnectException; + +import java.net.InetSocketAddress; + +public interface NetworkingClient { + InetSocketAddress getAddress(); + void connect(long clientId, String host, int port) throws CouldNotConnectException; + boolean isActive(); + void writeAndFlush(String msg); + void closeConnection(); +} diff --git a/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClientManager.java b/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClientManager.java new file mode 100644 index 0000000..c236080 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/interfaces/NetworkingClientManager.java @@ -0,0 +1,17 @@ +package org.toop.framework.networking.interfaces; + +import org.toop.framework.networking.exceptions.ClientNotFoundException; +import org.toop.framework.networking.exceptions.CouldNotConnectException; +import org.toop.framework.networking.types.NetworkingConnector; + +public interface NetworkingClientManager { + void startClient( + long id, + NetworkingClient nClient, + NetworkingConnector nConnector, + Runnable onSuccess, + Runnable onFailure + ) throws CouldNotConnectException; + void sendCommand(long id, String command) throws ClientNotFoundException; + void closeClient(long id) throws ClientNotFoundException; +} diff --git a/framework/src/main/java/org/toop/framework/networking/types/NetworkingConnector.java b/framework/src/main/java/org/toop/framework/networking/types/NetworkingConnector.java new file mode 100644 index 0000000..ee6ed44 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/types/NetworkingConnector.java @@ -0,0 +1,5 @@ +package org.toop.framework.networking.types; + +import java.util.concurrent.TimeUnit; + +public record NetworkingConnector(String host, int port, int reconnectAttempts, long timeout, TimeUnit timeUnit) {} \ No newline at end of file diff --git a/framework/src/main/java/org/toop/framework/networking/types/ServerCommand.java b/framework/src/main/java/org/toop/framework/networking/types/ServerCommand.java new file mode 100644 index 0000000..e11bb61 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/types/ServerCommand.java @@ -0,0 +1,3 @@ +package org.toop.framework.networking.types; + +public record ServerCommand(long clientId, String command) {} diff --git a/framework/src/main/java/org/toop/framework/networking/types/ServerMessage.java b/framework/src/main/java/org/toop/framework/networking/types/ServerMessage.java new file mode 100644 index 0000000..606607d --- /dev/null +++ b/framework/src/main/java/org/toop/framework/networking/types/ServerMessage.java @@ -0,0 +1,3 @@ +package org.toop.framework.networking.types; + +public record ServerMessage(String message) {} diff --git a/framework/src/test/java/org/toop/framework/audio/SoundEffectManagerTest.java b/framework/src/test/java/org/toop/framework/audio/SoundEffectManagerTest.java new file mode 100644 index 0000000..302858f --- /dev/null +++ b/framework/src/test/java/org/toop/framework/audio/SoundEffectManagerTest.java @@ -0,0 +1,119 @@ +package org.toop.framework.audio; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.toop.framework.resource.ResourceMeta; +import org.toop.framework.resource.resources.BaseResource; +import org.toop.framework.resource.types.AudioResource; + +import java.io.File; +import java.util.List; +import java.util.ArrayList; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for SoundEffectManager. + */ +class MockSoundEffectResource extends BaseResource implements AudioResource { + boolean played = false; + boolean stopped = false; + + public MockSoundEffectResource(String name) { + super(new File(name)); + } + + @Override + public String getName() { + return getFile().getName(); + } + + @Override + public void play() { + played = true; + } + + @Override + public void stop() { + stopped = true; + } + + @Override + public void setOnEnd(Runnable callback) {} + + @Override + public void setOnError(Runnable callback) {} + + @Override + public void updateVolume(double volume) {} +} + +public class SoundEffectManagerTest { + + private SoundEffectManager manager; + private MockAudioResource sfx1; + private MockAudioResource sfx2; + private MockAudioResource sfx3; + + @BeforeEach + void setUp() { + sfx1 = new MockAudioResource("explosion.wav"); + sfx2 = new MockAudioResource("laser.wav"); + sfx3 = new MockAudioResource("jump.wav"); + + List> resources = List.of( + new ResourceMeta<>("explosion", sfx1), + new ResourceMeta<>("laser", sfx2), + new ResourceMeta<>("jump", sfx3) + ); + + manager = new SoundEffectManager<>(resources); + } + + @Test + void testPlayValidSound() { + manager.play("explosion", false); + assertTrue(sfx1.played, "Sound 'explosion' should be played"); + } + + @Test + void testPlayInvalidSoundLogsWarning() { + // Nothing should crash or throw + assertDoesNotThrow(() -> manager.play("nonexistent", false)); + } + + @Test + void testStopValidSound() { + manager.stop("laser"); + assertTrue(sfx2.stopped, "Sound 'laser' should be stopped"); + } + + @Test + void testStopInvalidSoundDoesNotThrow() { + assertDoesNotThrow(() -> manager.stop("does_not_exist")); + } + + @Test + void testGetActiveAudioReturnsAll() { + Collection active = manager.getActiveAudio(); + assertEquals(3, active.size(), "All three sounds should be in active audio list"); + assertTrue(active.containsAll(List.of(sfx1, sfx2, sfx3))); + } + + @Test + void testDuplicateResourceKeepsLast() { + MockAudioResource oldRes = new MockAudioResource("duplicate_old.wav"); + MockAudioResource newRes = new MockAudioResource("duplicate_new.wav"); + + List> list = new ArrayList<>(); + list.add(new ResourceMeta<>("dup", oldRes)); + list.add(new ResourceMeta<>("dup", newRes)); // duplicate key + + SoundEffectManager dupManager = new SoundEffectManager<>(list); + dupManager.play("dup", false); + + assertTrue(newRes.played, "New duplicate resource should override old one"); + assertFalse(oldRes.played, "Old duplicate resource should be discarded"); + } +}