Merge remote-tracking branch 'origin/UI' into AudioDisplay

This commit is contained in:
lieght
2025-10-17 20:13:05 +02:00
25 changed files with 725 additions and 393 deletions

View File

@@ -2,7 +2,7 @@
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="AutoCloseableResource" enabled="true" level="WARNING" enabled_by_default="true">
<option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,java.lang.foreign.Arena,ofAuto,java.lang.foreign.Arena,global,org.toop.framework.audio.AudioPlayer,play,java.util.Map,remove" /> <option name="METHOD_MATCHER_CONFIG" value="java.util.Formatter,format,java.io.Writer,append,com.google.common.base.Preconditions,checkNotNull,org.hibernate.Session,close,java.io.PrintWriter,printf,java.io.PrintStream,printf,java.lang.foreign.Arena,ofAuto,java.lang.foreign.Arena,global,org.toop.framework.audio.AudioPlayer,play,java.util.Map,remove,java.util.concurrent.Executors,newSingleThreadScheduledExecutor" />
</inspection_tool> </inspection_tool>
<inspection_tool class="WriteOnlyObject" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="WriteOnlyObject" enabled="false" level="WARNING" enabled_by_default="false" />
</profile> </profile>

View File

@@ -2,28 +2,25 @@ package org.toop;
import org.toop.app.App; import org.toop.app.App;
import org.toop.framework.audio.*; import org.toop.framework.audio.*;
import org.toop.framework.networking.NetworkingClientEventListener;
import org.toop.framework.networking.NetworkingClientManager; import org.toop.framework.networking.NetworkingClientManager;
import org.toop.framework.networking.NetworkingInitializationException;
import org.toop.framework.resource.ResourceLoader; import org.toop.framework.resource.ResourceLoader;
import org.toop.framework.resource.ResourceManager; 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.MusicAsset;
import org.toop.framework.resource.resources.SoundEffectAsset; import org.toop.framework.resource.resources.SoundEffectAsset;
import java.util.Arrays;
import java.util.List;
public final class Main { public final class Main {
static void main(String[] args) { static void main(String[] args) {
initSystems(); initSystems();
App.run(args); App.run(args);
} }
private static void initSystems() throws NetworkingInitializationException { private static void initSystems() {
ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets")); ResourceManager.loadAssets(new ResourceLoader("app/src/main/resources/assets"));
new Thread(NetworkingClientManager::new).start(); new Thread(() -> new NetworkingClientEventListener(new NetworkingClientManager())).start();
new Thread(() -> { new Thread(() -> {
MusicManager<MusicAsset> musicManager = new MusicManager<>(ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class)); MusicManager<MusicAsset> musicManager = new MusicManager<>(ResourceManager.getAllOfTypeAndRemoveWrapper(MusicAsset.class), true);
SoundEffectManager<SoundEffectAsset> soundEffectManager = new SoundEffectManager<>(ResourceManager.getAllOfType(SoundEffectAsset.class)); SoundEffectManager<SoundEffectAsset> soundEffectManager = new SoundEffectManager<>(ResourceManager.getAllOfType(SoundEffectAsset.class));
AudioVolumeManager audioVolumeManager = new AudioVolumeManager() AudioVolumeManager audioVolumeManager = new AudioVolumeManager()
.registerManager(VolumeControl.MASTERVOLUME, musicManager) .registerManager(VolumeControl.MASTERVOLUME, musicManager)

View File

@@ -79,6 +79,7 @@ public final class App extends Application {
public static void quit() { public static void quit() {
ViewStack.cleanup(); ViewStack.cleanup();
stage.close(); stage.close();
System.exit(0); // TODO: This is like dropping a nuke
} }
public static void reload() { public static void reload() {

View File

@@ -1,5 +1,6 @@
package org.toop.app; package org.toop.app;
import com.google.common.util.concurrent.AbstractScheduledService;
import org.toop.app.game.ReversiGame; import org.toop.app.game.ReversiGame;
import org.toop.app.game.TicTacToeGame; import org.toop.app.game.TicTacToeGame;
import org.toop.app.view.ViewStack; 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.SendChallengeView;
import org.toop.app.view.views.ServerView; import org.toop.app.view.views.ServerView;
import org.toop.framework.eventbus.EventFlow; 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.events.NetworkEvents;
import org.toop.framework.networking.interfaces.NetworkingClient;
import org.toop.framework.networking.types.NetworkingConnector;
import org.toop.local.AppContext; import org.toop.local.AppContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public final class Server { public final class Server {
private String user = ""; private String user = "";
private long clientId = -1; private long clientId = -1;
private List<String> onlinePlayers = new CopyOnWriteArrayList<String>(); private List<String> onlinePlayers = new CopyOnWriteArrayList<String>();
private List<String> gameList = new CopyOnWriteArrayList<>();
private ServerView view; private ServerView view;
@@ -58,8 +66,12 @@ public final class Server {
} }
new EventFlow() 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 -> { .onResponse(NetworkEvents.StartClientResponse.class, e -> {
// TODO add if unsuccessful
this.user = user; this.user = user;
clientId = e.clientId(); clientId = e.clientId();
@@ -68,30 +80,23 @@ public final class Server {
view = new ServerView(user, this::sendChallenge, this::disconnect); view = new ServerView(user, this::sendChallenge, this::disconnect);
ViewStack.push(view); ViewStack.push(view);
startPopulateThread(); startPopulateScheduler();
populateGameList();
}).postEvent(); }).postEvent();
new EventFlow().listen(this::handleReceivedChallenge); new EventFlow().listen(this::handleReceivedChallenge);
} }
private void populatePlayerList() { private void populatePlayerList(ScheduledExecutorService scheduler, Runnable populatingTask) {
new EventFlow().listen(NetworkEvents.PlayerlistResponse.class, e -> {
if (e.clientId() == clientId) {
onlinePlayers = new ArrayList<String>(List.of(e.playerlist()));
onlinePlayers.removeIf(name -> name.equalsIgnoreCase(user));
view.update(onlinePlayers); final long DELAY = 5;
}
});
final EventFlow sendGetPlayerList = new EventFlow().addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId)); if (!isPolling) scheduler.shutdown();
else {
while (isPolling) { populatingTask.run();
sendGetPlayerList.postEvent(); scheduler.schedule(() -> populatePlayerList(scheduler, populatingTask), DELAY, TimeUnit.SECONDS);
try {
Thread.sleep(5000);
} catch (InterruptedException _) {}
} }
} }
@@ -173,6 +178,7 @@ public final class Server {
private void disconnect() { private void disconnect() {
new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent(); new EventFlow().addPostEvent(new NetworkEvents.CloseClient(clientId)).postEvent();
isPolling = false;
ViewStack.push(new OnlineView()); ViewStack.push(new OnlineView());
} }
@@ -184,27 +190,39 @@ public final class Server {
forfeitGame(); forfeitGame();
ViewStack.push(view); ViewStack.push(view);
startPopulateThread(); startPopulateScheduler();
} }
private void startPopulateThread() { private void startPopulateScheduler() {
isPolling = true; isPolling = true;
final Thread populateThread = new Thread(this::populatePlayerList); EventFlow getPlayerlistFlow = new EventFlow()
populateThread.setDaemon(false); .addPostEvent(new NetworkEvents.SendGetPlayerlist(clientId))
populateThread.start(); .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<String> getGamesList() { private void gamesListFromServerHandler(NetworkEvents.GamelistResponse event) {
final List<String> list = new ArrayList<String>(); gameList.addAll(List.of(event.gamelist()));
list.add("tic-tac-toe"); // Todo: get games list from server and check if the game is supported }
list.add("reversi");
public void populateGameList() {
new EventFlow().addPostEvent(new NetworkEvents.SendGetGamelist(clientId)) new EventFlow().addPostEvent(new NetworkEvents.SendGetGamelist(clientId))
.listen(NetworkEvents.GamelistResponse.class, e -> { .listen(NetworkEvents.GamelistResponse.class,
System.out.println(Arrays.toString(e.gamelist())); this::gamesListFromServerHandler, true
}).postEvent(); ).postEvent();
}
return list; public List<String> getGameList() {
return gameList;
} }
} }

View File

@@ -46,7 +46,7 @@ public final class SendChallengeView extends View {
gameText.setText(AppContext.getString("to-a-game-of")); gameText.setText(AppContext.getString("to-a-game-of"));
final ComboBox<String> gamesCombobox = combobox(); final ComboBox<String> gamesCombobox = combobox();
gamesCombobox.getItems().addAll(server.getGamesList()); gamesCombobox.getItems().addAll(server.getGameList());
gamesCombobox.setValue(gamesCombobox.getItems().getFirst()); gamesCombobox.setValue(gamesCombobox.getItems().getFirst());
final Button sendButton = button(); final Button sendButton = button();

View File

@@ -26,10 +26,13 @@ public class MusicManager<T extends AudioResource> implements org.toop.framework
private ScheduledExecutorService scheduler; private ScheduledExecutorService scheduler;
public MusicManager(List<T> resources) { public MusicManager(List<T> resources, boolean shuffleMusic) {
this.dispatcher = new JavaFXDispatcher(); this.dispatcher = new JavaFXDispatcher();
this.resources = resources; this.resources = resources;
createShuffled(); // Shuffle if wanting to shuffle
if (shuffleMusic) createShuffled();
else backgroundMusic.addAll(resources);
// ------------------------------
} }
/** /**

View File

@@ -134,7 +134,8 @@ public final class GlobalEventBus {
for (Consumer<? super EventType> listener : classListeners) { for (Consumer<? super EventType> listener : classListeners) {
try { try {
listener.accept(event); listener.accept(event);
} catch (Throwable ignored) { } catch (Throwable e) {
// e.printStackTrace();
} }
} }
} }
@@ -146,7 +147,8 @@ public final class GlobalEventBus {
for (Consumer<? super EventType> listener : genericListeners) { for (Consumer<? super EventType> listener : genericListeners) {
try { try {
listener.accept(event); listener.accept(event);
} catch (Throwable ignored) { } catch (Throwable e) {
// e.printStackTrace();
} }
} }
} }

View File

@@ -4,6 +4,17 @@ import java.lang.reflect.RecordComponent;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
/**
* MUST HAVE long identifier at the end.
* e.g.
*
* <pre>{@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 {};
* }</pre>
*
*/
public interface ResponseToUniqueEvent extends UniqueEvent { public interface ResponseToUniqueEvent extends UniqueEvent {
default Map<String, Object> result() { default Map<String, Object> result() {
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();

View File

@@ -1,5 +1,16 @@
package org.toop.framework.eventbus.events; package org.toop.framework.eventbus.events;
/**
* MUST HAVE long identifier at the end.
* e.g.
*
* <pre>{@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 {};
* }</pre>
*
*/
public interface UniqueEvent extends EventType { public interface UniqueEvent extends EventType {
default long getIdentifier() { default long getIdentifier() {
try { try {

View File

@@ -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<NetworkingClient> a = new ArrayList<>(this.networkClients.values());
// request.future().complete(a);
// TODO
}
public void handleShutdownAll(NetworkEvents.ForceCloseAllClients request) {
// TODO
}
}

View File

@@ -2,196 +2,118 @@ package org.toop.framework.networking;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; 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.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.toop.framework.SnowflakeGenerator; import org.toop.framework.networking.exceptions.ClientNotFoundException;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.exceptions.CouldNotConnectException;
import org.toop.framework.networking.events.NetworkEvents; import org.toop.framework.networking.interfaces.NetworkingClient;
import org.toop.framework.networking.types.NetworkingConnector;
public class NetworkingClientManager {
public class NetworkingClientManager implements org.toop.framework.networking.interfaces.NetworkingClientManager {
private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class); private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class);
private final Map<Long, NetworkingClient> networkClients = new ConcurrentHashMap<>();
/** Map of serverId -> Server instances */ public NetworkingClientManager() {}
final Map<Long, NetworkingClient> networkClients = new ConcurrentHashMap<>();
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);
}
/** Starts a connection manager, to manage, connections. */
public NetworkingClientManager() throws NetworkingInitializationException {
try { try {
new EventFlow() nClient.connect(id, nConnector.host(), nConnector.port());
.listen(this::handleStartClient) networkClients.put(id, nClient);
.listen(this::handleCommand) logger.info("New client started successfully for {}:{}", nConnector.host(), nConnector.port());
.listen(this::handleSendLogin) onSuccess.run();
.listen(this::handleSendLogout) scheduler.shutdown();
.listen(this::handleSendGetPlayerlist) } catch (CouldNotConnectException e) {
.listen(this::handleSendGetGamelist) attempts++;
.listen(this::handleSendSubscribe) if (attempts < nConnector.reconnectAttempts()) {
.listen(this::handleSendMove) logger.warn("Could not connect to {}:{}. Retrying in {} {}",
.listen(this::handleSendChallenge) nConnector.host(), nConnector.port(), nConnector.timeout(), nConnector.timeUnit());
.listen(this::handleSendAcceptChallenge) scheduler.schedule(this, nConnector.timeout(), nConnector.timeUnit());
.listen(this::handleSendForfeit) } else {
.listen(this::handleSendMessage) logger.error("Failed to start client for {}:{} after {} attempts", nConnector.host(), nConnector.port(), attempts);
.listen(this::handleSendHelp) onFailure.run();
.listen(this::handleSendHelpForCommand) scheduler.shutdown();
.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;
} }
} catch (Exception e) {
logger.error("Unexpected exception during startClient", e);
onFailure.run();
scheduler.shutdown();
}
}
};
scheduler.schedule(connectTask, 0, TimeUnit.MILLISECONDS);
} }
long startClientRequest(String ip, int port) { @Override
long connectionId = SnowflakeGenerator.nextId(); public void startClient(
try { long id,
NetworkingClient client = NetworkingClient nClient,
new NetworkingClient( NetworkingConnector nConnector,
() -> new NetworkingGameClientHandler(connectionId), Runnable onSuccess,
ip, Runnable onFailure
port, ) {
connectionId); connectHelper(
client.setConnectionId(connectionId);
this.networkClients.put(connectionId, client);
logger.info("New client started successfully for {}:{}", ip, port);
} catch (Exception e) {
logger.error(e);
}
return connectionId;
}
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);
}
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, id,
event.eventSnowflake()) nClient,
.asyncPostEvent()) nConnector,
.start(); onSuccess,
onFailure
);
} }
void handleCommand( @Override
NetworkEvents.SendCommand public void sendCommand(long id, String command) throws ClientNotFoundException {
event) { // TODO: Move this to ServerConnection class, keep it internal. logger.info("Sending command to client for {}:{}", id, command);
NetworkingClient client = this.networkClients.get(event.clientId()); if (command.isEmpty()) {
String args = String.join(" ", event.args()); IllegalArgumentException e = new IllegalArgumentException("command is empty");
sendCommand(client, args); logger.error("Invalid command received", e);
return;
} }
void handleSendLogin(NetworkEvents.SendLogin event) { NetworkingClient client = this.networkClients.get(id);
NetworkingClient client = this.networkClients.get(event.clientId()); if (client == null) {
sendCommand(client, String.format("LOGIN %s", event.username())); throw new ClientNotFoundException(id);
} }
private void handleSendLogout(NetworkEvents.SendLogout event) { String toSend = command.trim();
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, "LOGOUT"); if (toSend.endsWith("\n")) { client.writeAndFlush(toSend); }
else { client.writeAndFlush(toSend + "\n"); }
} }
private void handleSendGetPlayerlist(NetworkEvents.SendGetPlayerlist event) { @Override
NetworkingClient client = this.networkClients.get(event.clientId()); public void closeClient(long id) throws ClientNotFoundException {
sendCommand(client, "GET PLAYERLIST"); NetworkingClient client = this.networkClients.get(id);
if (client == null) {
throw new ClientNotFoundException(id);
} }
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(); 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<NetworkingClient> 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");
} }
} }

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking; package org.toop.framework.networking.clients;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*; 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.StringDecoder;
import io.netty.handler.codec.string.StringEncoder; import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import java.util.function.Supplier;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.EventFlow; import org.toop.framework.networking.exceptions.CouldNotConnectException;
import org.toop.framework.networking.events.NetworkEvents; import org.toop.framework.networking.handlers.NetworkingGameClientHandler;
import org.toop.framework.networking.interfaces.NetworkingClient;
public class NetworkingClient { import java.net.InetSocketAddress;
private static final Logger logger = LogManager.getLogger(NetworkingClient.class);
private long connectionId; public class TournamentNetworkingClient implements NetworkingClient {
private String host; private static final Logger logger = LogManager.getLogger(TournamentNetworkingClient.class);
private int port;
private Channel channel; private Channel channel;
private NetworkingGameClientHandler handler;
public NetworkingClient( public TournamentNetworkingClient() {}
Supplier<NetworkingGameClientHandler> handlerFactory,
String host, @Override
int port, public InetSocketAddress getAddress() {
long connectionId) { return (InetSocketAddress) channel.remoteAddress();
this.connectionId = connectionId; }
@Override
public void connect(long clientId, String host, int port) throws CouldNotConnectException {
try { try {
Bootstrap bootstrap = new Bootstrap(); Bootstrap bootstrap = new Bootstrap();
EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory());
@@ -40,7 +40,7 @@ public class NetworkingClient {
new ChannelInitializer<SocketChannel>() { new ChannelInitializer<SocketChannel>() {
@Override @Override
public void initChannel(SocketChannel ch) { public void initChannel(SocketChannel ch) {
handler = handlerFactory.get(); NetworkingGameClientHandler handler = new NetworkingGameClientHandler(clientId);
ChannelPipeline pipeline = ch.pipeline(); ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n
@@ -52,53 +52,28 @@ public class NetworkingClient {
}); });
ChannelFuture channelFuture = bootstrap.connect(host, port).sync(); ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
this.channel = channelFuture.channel(); this.channel = channelFuture.channel();
this.host = host; } catch (Exception _) {
this.port = port; throw new CouldNotConnectException(clientId);
} catch (Exception e) {
logger.error("Failed to create networking client instance", e);
} }
} }
public NetworkingGameClientHandler getHandler() { @Override
return this.handler; public boolean isActive() {
}
public String getHost() {
return this.host;
}
public int getPort() {
return this.port;
}
public void setConnectionId(long connectionId) {
this.connectionId = connectionId;
}
public boolean isChannelActive() {
return this.channel != null && this.channel.isActive(); return this.channel != null && this.channel.isActive();
} }
@Override
public void writeAndFlush(String msg) { public void writeAndFlush(String msg) {
String literalMsg = msg.replace("\n", "\\n").replace("\r", "\\r"); String literalMsg = msg.replace("\n", "\\n").replace("\r", "\\r");
if (isChannelActive()) { if (isActive()) {
this.channel.writeAndFlush(msg); this.channel.writeAndFlush(msg);
logger.info( logger.info("Connection {} sent message: '{}' ", this.channel.remoteAddress(), literalMsg);
"Connection {} sent message: '{}' ", this.channel.remoteAddress(), literalMsg);
} else { } else {
logger.warn("Cannot send message: '{}', connection inactive. ", literalMsg); logger.warn("Cannot send message: '{}', connection inactive. ", literalMsg);
} }
} }
public void writeAndFlushnl(String msg) { @Override
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);
}
}
public void closeConnection() { public void closeConnection() {
if (this.channel != null && this.channel.isActive()) { if (this.channel != null && this.channel.isActive()) {
this.channel this.channel
@@ -109,11 +84,6 @@ public class NetworkingClient {
logger.info( logger.info(
"Connection {} closed successfully", "Connection {} closed successfully",
this.channel.remoteAddress()); this.channel.remoteAddress());
new EventFlow()
.addPostEvent(
new NetworkEvents.ClosedConnection(
this.connectionId))
.asyncPostEvent();
} else { } else {
logger.error( logger.error(
"Error closing connection {}. Error: {}", "Error closing connection {}. Error: {}",
@@ -123,8 +93,4 @@ public class NetworkingClient {
}); });
} }
} }
public long getId() {
return this.connectionId;
}
} }

View File

@@ -3,158 +3,215 @@ package org.toop.framework.networking.events;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; 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.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 * Defines all event types related to the networking subsystem.
* org.toop.framework.eventbus.GlobalEventBus}. * <p>
* 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.
* </p>
* *
* <p>This class defines all the events that can be posted or listened to in the networking * <h2>Important</h2>
* subsystem. Events are separated into those with unique IDs (UniqueEvent) and those without * For all {@link UniqueEvent} and {@link ResponseToUniqueEvent} types:
* (GenericEvent). * the {@code identifier} field is automatically generated and injected
* by {@link org.toop.framework.eventbus.EventFlow}. It should <strong>never</strong>
* be manually assigned by user code. (Exceptions may apply)
*/ */
public class NetworkEvents extends EventsBase { public class NetworkEvents extends EventsBase {
// ------------------------------------------------------
// Generic Request & Response Events (no identifier)
// ------------------------------------------------------
/** /**
* Requests all active client connections. * Requests a list of all active networking clients.
* * <p>
* <p>This is a blocking event. The result will be delivered via the provided {@link * This is a blocking request that returns the list asynchronously
* CompletableFuture}. * via the provided {@link CompletableFuture}.
*
* @param future CompletableFuture to receive the list of active {@link NetworkingClient}
* instances.
*/ */
public record RequestsAllClients(CompletableFuture<List<NetworkingClient>> future) public record RequestsAllClients(CompletableFuture<List<NetworkingClient>> future)
implements GenericEvent {} implements GenericEvent {}
/** Forces all active client connections to close immediately. */ /** Signals all active clients should be forcefully closed. */
public record ForceCloseAllClients() implements GenericEvent {} public record ForceCloseAllClients() implements GenericEvent {}
/** Response indicating a challenge was cancelled. */ /** Indicates a challenge was cancelled by the server. */
public record ChallengeCancelledResponse(long clientId, String challengeId) implements GenericEvent {} 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) public record ChallengeResponse(long clientId, String challengerName, String challengeId, String gameType)
implements GenericEvent {} implements GenericEvent {}
/** Response containing a list of players for a client. */ /** Contains the list of players currently available on the server. */
public record PlayerlistResponse(long clientId, String[] playerlist) implements GenericEvent {} public record PlayerlistResponse(long clientId, String[] playerlist)
implements GenericEvent {}
/** Response containing a list of games for a client. */ /** Contains the list of available game types for a client. */
public record GamelistResponse(long clientId, String[] gamelist) implements GenericEvent {} 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) public record GameMatchResponse(long clientId, String playerToMove, String gameType, String opponent)
implements GenericEvent {} implements GenericEvent {}
/** Response indicating the result of a game. */ /** Indicates the outcome or completion of a game. */
public record GameResultResponse(long clientId, String condition) implements GenericEvent {} public record GameResultResponse(long clientId, String condition)
implements GenericEvent {}
/** Response indicating a game move occurred. */ /** Indicates that a game move has been processed or received. */
public record GameMoveResponse(long clientId, String player, String move, String details) implements GenericEvent {} 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) public record YourTurnResponse(long clientId, String message)
implements GenericEvent {} implements GenericEvent {}
/** Request to send login credentials for a client. */ /** Requests a login operation for the given client. */
public record SendLogin(long clientId, String username) implements GenericEvent {} public record SendLogin(long clientId, String username)
implements GenericEvent {}
/** Request to log out a client. */ /** Requests logout for the specified client. */
public record SendLogout(long clientId) implements GenericEvent {} public record SendLogout(long clientId)
implements GenericEvent {}
/** Request to retrieve the player list for a client. */ /** Requests the player list from the server. */
public record SendGetPlayerlist(long clientId) implements GenericEvent {} public record SendGetPlayerlist(long clientId)
implements GenericEvent {}
/** Request to retrieve the game list for a client. */ /** Requests the game list from the server. */
public record SendGetGamelist(long clientId) implements GenericEvent {} public record SendGetGamelist(long clientId)
implements GenericEvent {}
/** Request to subscribe a client to a game type. */ /** Requests a subscription to updates for a given game type. */
public record SendSubscribe(long clientId, String gameType) implements GenericEvent {} public record SendSubscribe(long clientId, String gameType)
implements GenericEvent {}
/** Request to make a move in a game. */ /** Sends a game move command to the server. */
public record SendMove(long clientId, short moveNumber) implements GenericEvent {} public record SendMove(long clientId, short moveNumber)
implements GenericEvent {}
/** Request to challenge another player. */ /** Requests to challenge another player to a game. */
public record SendChallenge(long clientId, String usernameToChallenge, String gameType) implements GenericEvent {} public record SendChallenge(long clientId, String usernameToChallenge, String gameType)
implements GenericEvent {}
/** Request to accept a challenge. */ /** Requests to accept an existing challenge. */
public record SendAcceptChallenge(long clientId, int challengeId) implements GenericEvent {} public record SendAcceptChallenge(long clientId, int challengeId)
implements GenericEvent {}
/** Request to forfeit a game. */ /** Requests to forfeit the current game. */
public record SendForfeit(long clientId) implements GenericEvent {} public record SendForfeit(long clientId)
implements GenericEvent {}
/** Request to send a message from a client. */ /** Sends a chat or informational message from a client. */
public record SendMessage(long clientId, String message) implements GenericEvent {} public record SendMessage(long clientId, String message)
implements GenericEvent {}
/** Request to display help to a client. */ /** Requests general help information from the server. */
public record SendHelp(long clientId) implements GenericEvent {} public record SendHelp(long clientId)
implements GenericEvent {}
/** Request to display help for a specific command. */ /** Requests help information specific to a given command. */
public record SendHelpForCommand(long clientId, String command) implements GenericEvent {} public record SendHelpForCommand(long clientId, String command)
implements GenericEvent {}
/** Request to close a specific client connection. */ /** Requests to close an active client connection. */
public record CloseClient(long clientId) implements GenericEvent {} 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.
* *
* <p>Carries IP, port, and a unique event ID for correlation with responses. * @param clientId The client ID to send the command from.
*
* @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 args The command arguments. * @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. */ /** Event fired when a message is received from the server. */
public record Reconnect(long clientId) implements GenericEvent {} 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.
* <p>
* The {@code identifier} is automatically assigned by {@link org.toop.framework.eventbus.EventFlow}
* to correlate with its corresponding {@link StartClientResponse}.
* </p>
* *
* @param clientId The connection ID that received the message. * @param networkingClient The client instance to start.
* @param message The message content. * @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.
* <p>
* The {@code identifier} value is automatically propagated from
* the original {@link StartClient} request by {@link org.toop.framework.eventbus.EventFlow}.
* </p>
* *
* @param clientId The client connection ID. * @param clientId The newly assigned client ID.
* @param ip The new server IP. * @param successful Whether the connection succeeded.
* @param port The new server port. * @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.
* <p>
* The {@code identifier} is automatically injected by {@link org.toop.framework.eventbus.EventFlow}.
* </p>
*/
public record Reconnect(
long clientId,
NetworkingClient networkingClient,
NetworkingConnector networkingConnector,
long identifier)
implements UniqueEvent {}
/** Event indicating a client connection was closed. */ /** Response to a {@link Reconnect} event, carrying the success result. */
public record ClosedConnection(long clientId) implements GenericEvent {} public record ReconnectResponse(boolean successful, long identifier)
implements ResponseToUniqueEvent {}
/**
* Requests to change the connection target (host/port) for a client.
* <p>
* The {@code identifier} is automatically injected by {@link org.toop.framework.eventbus.EventFlow}.
* </p>
*/
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 {}
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking; package org.toop.framework.networking.exceptions;
public class NetworkingInitializationException extends RuntimeException { public class NetworkingInitializationException extends RuntimeException {
public NetworkingInitializationException(String message, Throwable cause) { public NetworkingInitializationException(String message, Throwable cause) {

View File

@@ -1,4 +1,4 @@
package org.toop.framework.networking; package org.toop.framework.networking.handlers;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInboundHandlerAdapter;

View File

@@ -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);
//
//
//}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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) {}

View File

@@ -0,0 +1,3 @@
package org.toop.framework.networking.types;
public record ServerCommand(long clientId, String command) {}

View File

@@ -0,0 +1,3 @@
package org.toop.framework.networking.types;
public record ServerMessage(String message) {}

View File

@@ -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<MockAudioResource> 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<ResourceMeta<MockAudioResource>> 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<MockAudioResource> 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<ResourceMeta<MockAudioResource>> list = new ArrayList<>();
list.add(new ResourceMeta<>("dup", oldRes));
list.add(new ResourceMeta<>("dup", newRes)); // duplicate key
SoundEffectManager<MockAudioResource> 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");
}
}