diff --git a/.gitignore b/.gitignore index 323576f..12c0ee3 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,10 @@ nb-configuration.xml # Ignore Gradle build output directory build + +############################## +## Hanze +############################## +newgamesver-release-V1.jar +server.properties +gameserver.log \ No newline at end of file diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml index e5bfff7..5b1b09b 100644 --- a/.idea/dictionaries/project.xml +++ b/.idea/dictionaries/project.xml @@ -2,6 +2,7 @@ aosp + clid dcompile errorprone gamelist diff --git a/.idea/misc.xml b/.idea/misc.xml index 97dd9e8..72be14a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -13,7 +13,7 @@ - + \ No newline at end of file diff --git a/app/src/main/java/org/toop/Main.java b/app/src/main/java/org/toop/Main.java index f2f618e..a79e324 100644 --- a/app/src/main/java/org/toop/Main.java +++ b/app/src/main/java/org/toop/Main.java @@ -1,13 +1,54 @@ package org.toop; -import org.toop.app.gui.LocalServerSelector; +import org.toop.framework.eventbus.EventFlow; +import org.toop.framework.networking.events.NetworkEvents; import org.toop.framework.networking.NetworkingClientManager; import org.toop.framework.networking.NetworkingInitializationException; +import org.toop.app.gui.LocalServerSelector; + +import java.util.Arrays; public class Main { static void main(String[] args) { initSystems(); - javax.swing.SwingUtilities.invokeLater(LocalServerSelector::new); + + EventFlow a = new EventFlow() + .addPostEvent( + NetworkEvents.StartClient.class, + "127.0.0.1", + 7789) + .onResponse(Main::login) +// .onResponse(Main::sendCommand) +// .onResponse(Main::closeClient) + .asyncPostEvent(); + + new Thread(() -> { + while (a.getResult() == null) { + try { + Thread.sleep(2000); + } catch (InterruptedException e) {} + } + long clid = (Long) a.getResult().get("clientId"); + new EventFlow() + .addPostEvent(new NetworkEvents.SendCommand(clid, "get playerlist")) + .listen(NetworkEvents.PlayerListResponse.class, response -> { + if (response.clientId() == clid) System.out.println(Arrays.toString(response.playerlist())); + }) + .asyncPostEvent(); + }).start(); + + new Thread(() -> javax.swing.SwingUtilities.invokeLater(LocalServerSelector::new)).start(); + } + + private static void login(NetworkEvents.StartClientResponse event) { + new Thread(() -> { + try { + Thread.sleep(1000); + new EventFlow() + .addPostEvent(new NetworkEvents.SendCommand(event.clientId(), "login bas")) + .asyncPostEvent(); + } catch (InterruptedException e) {} + }).start(); } private static void initSystems() throws NetworkingInitializationException { diff --git a/app/src/main/java/org/toop/app/gui/RemoteGameSelector.java b/app/src/main/java/org/toop/app/gui/RemoteGameSelector.java index 0d13a98..8a62644 100644 --- a/app/src/main/java/org/toop/app/gui/RemoteGameSelector.java +++ b/app/src/main/java/org/toop/app/gui/RemoteGameSelector.java @@ -54,14 +54,15 @@ public class RemoteGameSelector { && !ipTextField.getText().isEmpty() && !portTextField.getText().isEmpty()) { - AtomicReference clientId = new AtomicReference<>(); + AtomicReference clientId = new AtomicReference<>(); new EventFlow().addPostEvent( NetworkEvents.StartClient.class, - (Supplier) NetworkingGameClientHandler::new, + (Supplier) + new NetworkingGameClientHandler(clientId.get()), "127.0.0.1", 5001 ).onResponse( - NetworkEvents.StartClientSuccess.class, + NetworkEvents.StartClientResponse.class, (response) -> { clientId.set(response.clientId()); } diff --git a/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java b/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java index f361d8c..d390ebd 100644 --- a/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java +++ b/app/src/main/java/org/toop/tictactoe/LocalTicTacToe.java @@ -33,7 +33,7 @@ public class LocalTicTacToe { // TODO: Implement runnable private boolean isLocal; private String gameId; - private String connectionId = null; + private long connectionId = -1; private String serverId = null; private boolean[] isAiPlayer = new boolean[2]; @@ -74,7 +74,7 @@ public class LocalTicTacToe { // TODO: Implement runnable // this.receivedMessageListener = // GlobalEventBus.subscribe(this::receiveMessageAction); // GlobalEventBus.subscribe(this.receivedMessageListener); - this.connectionId = this.createConnection(ip, port); +// this.connectionId = this.createConnection(ip, port); TODO: Refactor this this.createGame("X", "O"); this.isLocal = false; //this.executor.submit(this::remoteGameThread); @@ -101,19 +101,6 @@ public class LocalTicTacToe { // TODO: Implement runnable return new LocalTicTacToe(ip, port); } - private String createConnection(String ip, int port) { - CompletableFuture connectionIdFuture = new CompletableFuture<>(); - new EventFlow().addPostEvent(NetworkEvents.StartClientRequest.class, - (Supplier) NetworkingGameClientHandler::new, - ip, port, connectionIdFuture).asyncPostEvent(); // TODO: what if server couldn't be started with port. - try { - return connectionIdFuture.get(); - } catch (InterruptedException | ExecutionException e) { - logger.error("Error getting connection ID", e); - } - return null; - } - private void createGame(String nameA, String nameB) { nameA = nameA.trim().replace(" ", "-"); nameB = nameB.trim().replace(" ", "-"); @@ -223,7 +210,7 @@ public class LocalTicTacToe { // TODO: Implement runnable } private void receiveMessageAction(NetworkEvents.ReceivedMessage receivedMessage) { - if (!receivedMessage.ConnectionUuid().equals(this.connectionId)) { + if (receivedMessage.ConnectionId() != this.connectionId) { return; } diff --git a/framework/src/main/java/org/toop/framework/eventbus/SnowflakeGenerator.java b/framework/src/main/java/org/toop/framework/SnowflakeGenerator.java similarity index 62% rename from framework/src/main/java/org/toop/framework/eventbus/SnowflakeGenerator.java rename to framework/src/main/java/org/toop/framework/SnowflakeGenerator.java index c75acba..3f6830d 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/SnowflakeGenerator.java +++ b/framework/src/main/java/org/toop/framework/SnowflakeGenerator.java @@ -1,10 +1,12 @@ -package org.toop.framework.eventbus; +package org.toop.framework; +import java.net.NetworkInterface; +import java.time.Instant; +import java.util.Collections; import java.util.concurrent.atomic.AtomicLong; public class SnowflakeGenerator { - // Epoch start (choose your custom epoch to reduce bits wasted on old time) - private static final long EPOCH = 1700000000000L; // ~2023-11-15 + private static final long EPOCH = Instant.parse("2025-01-01T00:00:00Z").toEpochMilli(); // Bit allocations private static final long TIMESTAMP_BITS = 41; @@ -14,20 +16,36 @@ public class SnowflakeGenerator { // Max values private static final long MAX_MACHINE_ID = (1L << MACHINE_BITS) - 1; private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1; + private static final long MAX_TIMESTAMP = (1L << TIMESTAMP_BITS) - 1; // Bit shifts private static final long MACHINE_SHIFT = SEQUENCE_BITS; private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_BITS; - private final long machineId; + private static final long machineId = SnowflakeGenerator.genMachineId(); private final AtomicLong lastTimestamp = new AtomicLong(-1L); private long sequence = 0L; - public SnowflakeGenerator(long machineId) { + private static long genMachineId() { + try { + StringBuilder sb = new StringBuilder(); + for (NetworkInterface ni : Collections.list(NetworkInterface.getNetworkInterfaces())) { + byte[] mac = ni.getHardwareAddress(); + if (mac != null) { + for (byte b : mac) sb.append(String.format("%02X", b)); + } + } + // limit to 10 bits (0–1023) + return sb.toString().hashCode() & 0x3FF; + } catch (Exception e) { + return (long) (Math.random() * 1024); // fallback + } + } + + public SnowflakeGenerator() { if (machineId < 0 || machineId > MAX_MACHINE_ID) { throw new IllegalArgumentException("Machine ID must be between 0 and " + MAX_MACHINE_ID); } - this.machineId = machineId; } public synchronized long nextId() { @@ -37,6 +55,10 @@ public class SnowflakeGenerator { throw new IllegalStateException("Clock moved backwards. Refusing to generate id."); } + if (currentTimestamp > MAX_TIMESTAMP) { + throw new IllegalStateException("Timestamp bits overflow, Snowflake expired."); + } + if (currentTimestamp == lastTimestamp.get()) { sequence = (sequence + 1) & MAX_SEQUENCE; if (sequence == 0) { diff --git a/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java b/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java index 5d71f61..6092fb8 100644 --- a/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java +++ b/framework/src/main/java/org/toop/framework/eventbus/EventFlow.java @@ -1,15 +1,18 @@ package org.toop.framework.eventbus; +import org.toop.framework.SnowflakeGenerator; import org.toop.framework.eventbus.events.EventType; import org.toop.framework.eventbus.events.EventWithSnowflake; -import org.toop.framework.eventbus.SnowflakeGenerator; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import java.util.function.Supplier; /** * EventFlow is a utility class for creating, posting, and optionally subscribing to events @@ -22,6 +25,8 @@ import java.util.function.Consumer; */ public class EventFlow { + + /** Lookup object used for dynamically invoking constructors via MethodHandles. */ private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); @@ -35,10 +40,7 @@ public class EventFlow { private EventType event = null; /** The listener returned by GlobalEventBus subscription. Used for unsubscription. */ - private Object listener; - - /** Flag indicating whether to automatically unsubscribe the listener after success. */ - private boolean unsubscribeAfterSuccess = false; + private final List listeners = new ArrayList<>(); /** Holds the results returned from the subscribed event, if any. */ private Map result = null; @@ -46,9 +48,19 @@ public class EventFlow { /** Empty constructor (event must be added via {@link #addPostEvent(Class, Object...)}). */ public EventFlow() {} - /** - * Instantiate an event of the given class and store it in this publisher. - */ + // New: accept an event instance directly + public EventFlow addPostEvent(EventType event) { + this.event = event; + return this; + } + + // Optional: accept a Supplier to defer construction + public EventFlow addPostEvent(Supplier eventSupplier) { + this.event = eventSupplier.get(); + return this; + } + + // Keep the old class+args version if needed public EventFlow addPostEvent(Class eventClass, Object... args) { try { boolean isUuidEvent = EventWithSnowflake.class.isAssignableFrom(eventClass); @@ -67,7 +79,7 @@ public class EventFlow { int expectedParamCount = ctorHandle.type().parameterCount(); if (isUuidEvent && args.length < expectedParamCount) { - this.eventSnowflake = new SnowflakeGenerator(1).nextId(); + this.eventSnowflake = new SnowflakeGenerator().nextId(); finalArgs = new Object[args.length + 1]; System.arraycopy(args, 0, finalArgs, 0, args.length); finalArgs[args.length] = this.eventSnowflake; @@ -86,124 +98,132 @@ public class EventFlow { } } +// public EventFlow addSnowflake() { +// this.eventSnowflake = new SnowflakeGenerator(1).nextId(); +// return this; +// } + /** - * Start listening for a response event type, chainable with perform(). + * Subscribe by ID: only fires if UUID matches this publisher's eventId. */ - public ResponseBuilder onResponse(Class eventClass) { - return new ResponseBuilder<>(this, eventClass); - } + public EventFlow onResponse(Class eventClass, Consumer action, + boolean unsubscribeAfterSuccess) { + ListenerHandler[] listenerHolder = new ListenerHandler[1]; + listenerHolder[0] = new ListenerHandler( + GlobalEventBus.subscribe(eventClass, event -> { + if (event.eventSnowflake() != this.eventSnowflake) return; - public static class ResponseBuilder { - private final EventFlow parent; - private final Class responseClass; + action.accept(event); - ResponseBuilder(EventFlow parent, Class responseClass) { - this.parent = parent; - this.responseClass = responseClass; - } + if (unsubscribeAfterSuccess && listenerHolder[0] != null) { + GlobalEventBus.unsubscribe(listenerHolder[0]); + this.listeners.remove(listenerHolder[0]); + } - /** Finalize the subscription */ - public EventFlow perform(Consumer action) { - parent.listener = GlobalEventBus.subscribe(responseClass, event -> { - action.accept(responseClass.cast(event)); - if (parent.unsubscribeAfterSuccess && parent.listener != null) { - GlobalEventBus.unsubscribe(parent.listener); - } - }); - return parent; - } + this.result = event.result(); + }) + ); + this.listeners.add(listenerHolder[0]); + return this; } /** * Subscribe by ID: only fires if UUID matches this publisher's eventId. */ public EventFlow onResponse(Class eventClass, Consumer action) { - this.listener = GlobalEventBus.subscribe(eventClass, event -> { - if (event.eventSnowflake() == this.eventSnowflake) { - action.accept(event); - if (unsubscribeAfterSuccess && listener != null) { - GlobalEventBus.unsubscribe(listener); - } - this.result = event.result(); - } - }); - return this; + return this.onResponse(eventClass, action, true); } /** * Subscribe by ID without explicit class. */ @SuppressWarnings("unchecked") - public EventFlow onResponse(Consumer action) { - this.listener = GlobalEventBus.subscribe(event -> { - if (event instanceof EventWithSnowflake uuidEvent) { + public EventFlow onResponse(Consumer action, boolean unsubscribeAfterSuccess) { + ListenerHandler[] listenerHolder = new ListenerHandler[1]; + listenerHolder[0] = new ListenerHandler( + GlobalEventBus.subscribe(event -> { + if (!(event instanceof EventWithSnowflake uuidEvent)) return; if (uuidEvent.eventSnowflake() == this.eventSnowflake) { try { TT typedEvent = (TT) uuidEvent; action.accept(typedEvent); - if (unsubscribeAfterSuccess && listener != null) { - GlobalEventBus.unsubscribe(listener); + if (unsubscribeAfterSuccess && listenerHolder[0] != null) { + GlobalEventBus.unsubscribe(listenerHolder[0]); + this.listeners.remove(listenerHolder[0]); } this.result = typedEvent.result(); - } catch (ClassCastException ignored) {} + } catch (ClassCastException _) { + throw new ClassCastException("Cannot cast " + event.getClass().getName() + + " to EventWithSnowflake"); + } } - } - }); + }) + ); + this.listeners.add(listenerHolder[0]); return this; } - // choose event type - public EventSubscriberBuilder onEvent(Class eventClass) { - return new EventSubscriberBuilder<>(this, eventClass); + public EventFlow onResponse(Consumer action) { + return this.onResponse(action, true); + } + + public EventFlow listen(Class eventClass, Consumer action, + boolean unsubscribeAfterSuccess) { + ListenerHandler[] listenerHolder = new ListenerHandler[1]; + listenerHolder[0] = new ListenerHandler( + GlobalEventBus.subscribe(eventClass, event -> { + action.accept(event); + + if (unsubscribeAfterSuccess && listenerHolder[0] != null) { + GlobalEventBus.unsubscribe(listenerHolder[0]); + this.listeners.remove(listenerHolder[0]); + } + }) + ); + this.listeners.add(listenerHolder[0]); + return this; } - // One-liner shorthand public EventFlow listen(Class eventClass, Consumer action) { - return this.onEvent(eventClass).perform(action); + return this.listen(eventClass, action, true); } - // Builder for chaining .onEvent(...).perform(...) - public static class EventSubscriberBuilder { - private final EventFlow publisher; - private final Class eventClass; + @SuppressWarnings("unchecked") + public EventFlow listen(Consumer action, boolean unsubscribeAfterSuccess) { + ListenerHandler[] listenerHolder = new ListenerHandler[1]; + listenerHolder[0] = new ListenerHandler( + GlobalEventBus.subscribe(event -> { + if (!(event instanceof EventType nonUuidEvent)) return; + try { + TT typedEvent = (TT) nonUuidEvent; + action.accept(typedEvent); + if (unsubscribeAfterSuccess && listenerHolder[0] != null) { + GlobalEventBus.unsubscribe(listenerHolder[0]); + this.listeners.remove(listenerHolder[0]); + } + } catch (ClassCastException _) { + throw new ClassCastException("Cannot cast " + event.getClass().getName() + + " to EventWithSnowflake"); + } + }) + ); + this.listeners.add(listenerHolder[0]); + return this; + } - EventSubscriberBuilder(EventFlow publisher, Class eventClass) { - this.publisher = publisher; - this.eventClass = eventClass; - } - - public EventFlow perform(Consumer action) { - publisher.listener = GlobalEventBus.subscribe(eventClass, event -> { - action.accept(eventClass.cast(event)); - if (publisher.unsubscribeAfterSuccess && publisher.listener != null) { - GlobalEventBus.unsubscribe(publisher.listener); - } - }); - return publisher; - } + public EventFlow listen(Consumer action) { + return this.listen(action, true); } /** Post synchronously */ public EventFlow postEvent() { - GlobalEventBus.post(event); + GlobalEventBus.post(this.event); return this; } /** Post asynchronously */ public EventFlow asyncPostEvent() { - GlobalEventBus.postAsync(event); - return this; - } - - public EventFlow unsubscribeAfterSuccess() { - this.unsubscribeAfterSuccess = true; - return this; - } - - public EventFlow unsubscribeNow() { - if (unsubscribeAfterSuccess && listener != null) { - GlobalEventBus.unsubscribe(listener); - } + GlobalEventBus.postAsync(this.event); return this; } @@ -215,7 +235,11 @@ public class EventFlow { return event; } - public long getEventId() { + public ListenerHandler[] getListeners() { + return listeners.toArray(new ListenerHandler[0]); + } + + public long getEventSnowflake() { return eventSnowflake; } } diff --git a/framework/src/main/java/org/toop/framework/eventbus/ListenerHandler.java b/framework/src/main/java/org/toop/framework/eventbus/ListenerHandler.java new file mode 100644 index 0000000..05eadb4 --- /dev/null +++ b/framework/src/main/java/org/toop/framework/eventbus/ListenerHandler.java @@ -0,0 +1,26 @@ +package org.toop.framework.eventbus; + +import org.toop.framework.eventbus.events.EventType; + +public class ListenerHandler { + private Object listener = null; +// private boolean unsubscribeAfterSuccess = true; + +// public ListenerHandler(Object listener, boolean unsubAfterSuccess) { +// this.listener = listener; +// this.unsubscribeAfterSuccess = unsubAfterSuccess; +// } + + public ListenerHandler(Object listener) { + this.listener = listener; + } + + public Object getListener() { + return this.listener; + } + +// public boolean isUnsubscribeAfterSuccess() { +// return this.unsubscribeAfterSuccess; +// } + +} diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingClient.java b/framework/src/main/java/org/toop/framework/networking/NetworkingClient.java index 91143ec..7b88374 100644 --- a/framework/src/main/java/org/toop/framework/networking/NetworkingClient.java +++ b/framework/src/main/java/org/toop/framework/networking/NetworkingClient.java @@ -7,6 +7,8 @@ import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; +import io.netty.handler.codec.string.StringEncoder; +import io.netty.util.CharsetUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -15,33 +17,35 @@ import java.util.function.Supplier; public class NetworkingClient { private static final Logger logger = LogManager.getLogger(NetworkingClient.class); - final Bootstrap bootstrap = new Bootstrap(); - final EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); - - private String connectionUuid; + private long connectionId; private Channel channel; private NetworkingGameClientHandler handler; public NetworkingClient( - Supplier handlerFactory, + Supplier handlerFactory, String host, - int port) { + int port, + long connectionId) { + this.connectionId = connectionId; try { - this.bootstrap.group(this.workerGroup); - this.bootstrap.channel(NioSocketChannel.class); - this.bootstrap.option(ChannelOption.SO_KEEPALIVE, true); - this.bootstrap.handler(new ChannelInitializer() { + Bootstrap bootstrap = new Bootstrap(); + EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); + bootstrap.group(workerGroup); + bootstrap.channel(NioSocketChannel.class); + bootstrap.option(ChannelOption.SO_KEEPALIVE, true); + bootstrap.handler(new ChannelInitializer() { @Override public void initChannel(SocketChannel ch) { handler = handlerFactory.get(); ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n - pipeline.addLast(new StringDecoder()); // bytes -> String + pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)); // bytes -> String + pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8)); pipeline.addLast(handler); } }); - ChannelFuture channelFuture = this.bootstrap.connect(host, port).sync(); + ChannelFuture channelFuture = bootstrap.connect(host, port).sync(); this.channel = channelFuture.channel(); } catch (Exception e) { logger.error("Failed to create networking client instance", e); @@ -52,8 +56,8 @@ public class NetworkingClient { return handler; } - public void setConnectionUuid(String connectionUuid) { - this.connectionUuid = connectionUuid; + public void setConnectionId(long connectionId) { + this.connectionId = connectionId; } public boolean isChannelActive() { @@ -64,18 +68,18 @@ public class NetworkingClient { String literalMsg = msg.replace("\n", "\\n").replace("\r", "\\r"); if (isChannelActive()) { 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); + logger.warn("Cannot send message: '{}', connection inactive.", literalMsg); } } public void writeAndFlushnl(String msg) { if (isChannelActive()) { - this.channel.writeAndFlush(msg + "\n"); - logger.info("Connection {} sent message: {}", this.channel.remoteAddress(), msg); + this.channel.writeAndFlush(msg + "\r\n"); + logger.info("Connection {} sent message: '{}'", this.channel.remoteAddress(), msg); } else { - logger.warn("Cannot send message: {}, connection inactive.", msg); + logger.warn("Cannot send message: '{}', connection inactive.", msg); } } @@ -137,4 +141,8 @@ public class NetworkingClient { } } + public long getId() { + return this.connectionId; + } + } 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 346d5c8..bbd0bb4 100644 --- a/framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java +++ b/framework/src/main/java/org/toop/framework/networking/NetworkingClientManager.java @@ -7,6 +7,7 @@ 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.SnowflakeGenerator; import org.toop.framework.networking.events.NetworkEvents; public class NetworkingClientManager { @@ -14,67 +15,62 @@ public class NetworkingClientManager { private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class); /** Map of serverId -> Server instances */ - private final Map networkClients = new ConcurrentHashMap<>(); + private final Map networkClients = new ConcurrentHashMap<>(); /** Starts a connection manager, to manage, connections. */ public NetworkingClientManager() throws NetworkingInitializationException { try { - new EventFlow().listen(NetworkEvents.StartClientRequest.class, this::handleStartClientRequest); - new EventFlow().listen(NetworkEvents.StartClient.class, this::handleStartClient); - new EventFlow().listen(NetworkEvents.SendCommand.class, this::handleCommand); - new EventFlow().listen(NetworkEvents.CloseClient.class, this::handleCloseClient); - new EventFlow().listen(NetworkEvents.RequestsAllClients.class, this::getAllConnections); - new EventFlow().listen(NetworkEvents.ForceCloseAllClients.class, this::shutdownAll); + new EventFlow() + .listen(this::handleStartClient) + .listen(this::handleCommand) + .listen(this::handleCloseClient) + .listen(this::getAllConnections) + .listen(this::shutdownAll); + logger.info("NetworkingClientManager initialized"); } catch (Exception e) { logger.error("Failed to initialize the client manager", e); throw e; } } - private String startClientRequest(Supplier handlerFactory, - String ip, - int port) { - String connectionUuid = UUID.randomUUID().toString(); - try { + private long startClientRequest(String ip, int port) { + long connectionId = new SnowflakeGenerator().nextId(); // TODO: Maybe use the one generated + try { // With EventFlow NetworkingClient client = new NetworkingClient( - handlerFactory, + () -> new NetworkingGameClientHandler(connectionId), ip, - port); - this.networkClients.put(connectionUuid, client); + port, + connectionId); + client.setConnectionId(connectionId); + this.networkClients.put(connectionId, client); } catch (Exception e) { logger.error(e); } - logger.info("Client {} started", connectionUuid); - return connectionUuid; - } - - private void handleStartClientRequest(NetworkEvents.StartClientRequest request) { - request.future() - .complete( - this.startClientRequest( - request.handlerFactory(), - request.ip(), - request.port())); // TODO: Maybe post ConnectionEstablished event. + logger.info("Client {} started", connectionId); + return connectionId; } private void handleStartClient(NetworkEvents.StartClient event) { - String uuid = this.startClientRequest(event.handlerFactory(), event.ip(), event.port()); - new EventFlow().addPostEvent(NetworkEvents.StartClientSuccess.class, - uuid, event.eventSnowflake() - ).asyncPostEvent(); + long id = this.startClientRequest(event.ip(), event.port()); + new Thread(() -> { + try { + Thread.sleep(100); // TODO: Is this a good idea? + new EventFlow().addPostEvent(NetworkEvents.StartClientResponse.class, + id, event.eventSnowflake() + ).asyncPostEvent(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }).start(); } private void handleCommand( NetworkEvents.SendCommand event) { // TODO: Move this to ServerConnection class, keep it internal. NetworkingClient client = this.networkClients.get(event.connectionId()); - logger.info("Preparing to send command: {} to server: {}", event.args(), client); - if (client != null) { - String args = String.join(" ", event.args()) + "\n"; - client.writeAndFlush(args); - } else { - logger.warn("Server {} not found for command '{}'", event.connectionId(), event.args()); - } + logger.info("Preparing to send command: {} to server: {}", event.args(), client.getId()); + String args = String.join(" ", event.args()); + client.writeAndFlushnl(args); } private void handleCloseClient(NetworkEvents.CloseClient event) { @@ -115,7 +111,7 @@ public class NetworkingClientManager { private void getAllConnections(NetworkEvents.RequestsAllClients request) { List a = new ArrayList<>(this.networkClients.values()); - request.future().complete(a.toString()); + request.future().complete(a); } public void shutdownAll(NetworkEvents.ForceCloseAllClients request) { diff --git a/framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java b/framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java index 0b7ca49..a88686c 100644 --- a/framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java +++ b/framework/src/main/java/org/toop/framework/networking/NetworkingGameClientHandler.java @@ -2,19 +2,65 @@ package org.toop.framework.networking; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import jdk.jfr.Event; 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 java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter { private static final Logger logger = LogManager.getLogger(NetworkingGameClientHandler.class); - public NetworkingGameClientHandler() {} + private final long connectionId; + + public NetworkingGameClientHandler(long connectionId) { + this.connectionId = connectionId; + } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { - logger.debug("Received message from server-{}, data: {}", ctx.channel().remoteAddress(), msg); + String rec = msg.toString().trim(); - // TODO: Handle server messages + if (rec.equalsIgnoreCase("err")) { + logger.error("server-{} send back error, data: {}", ctx.channel().remoteAddress(), msg); + return; + } + if (rec.equalsIgnoreCase("ok")) { + logger.info("Received OK message from server-{}, data: {}", ctx.channel().remoteAddress(), msg); + return; + } + if (rec.toLowerCase().startsWith("svr")) { + logger.info("Received SVR message from server-{}, data: {}", ctx.channel().remoteAddress(), msg); + new EventFlow().addPostEvent(new NetworkEvents.ServerResponse(this.connectionId)).asyncPostEvent(); + parseServerReturn(rec); + return; + } + logger.info("Received unparsed message from server-{}, data: {}", ctx.channel().remoteAddress(), msg); + + } + + private void parseServerReturn(String rec) { + if (rec.toLowerCase().contains("playerlist")) { + playerListHandler(rec); + } else if (rec.toLowerCase().contains("close")) { + + } else {} + } + + private void playerListHandler(String rec) { + Pattern pattern = Pattern.compile("\"([^\"]+)\""); + String[] players = pattern.matcher(rec) + .results() + .map(m -> m.group(1)) + .toArray(String[]::new); + + new EventFlow() + .addPostEvent(new NetworkEvents.PlayerListResponse(this.connectionId, players)) + .asyncPostEvent(); } @Override 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 18fbd26..9596014 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,6 +3,7 @@ package org.toop.framework.networking.events; import org.toop.framework.eventbus.events.EventWithSnowflake; import org.toop.framework.eventbus.events.EventWithoutSnowflake; import org.toop.framework.eventbus.events.EventsBase; +import org.toop.framework.networking.NetworkingClient; import org.toop.framework.networking.NetworkingGameClientHandler; import java.lang.reflect.RecordComponent; @@ -20,14 +21,14 @@ public class NetworkEvents extends EventsBase { * * @param future List of all connections in string form. */ - public record RequestsAllClients(CompletableFuture future) implements EventWithoutSnowflake {} + public record RequestsAllClients(CompletableFuture> future) implements EventWithoutSnowflake {} /** Forces closing all active connections immediately. */ public record ForceCloseAllClients() implements EventWithoutSnowflake {} - public record CloseClientRequest(CompletableFuture future) implements EventWithoutSnowflake {} + public record PlayerListResponse(long clientId, String[] playerlist) implements EventWithoutSnowflake {} - public record CloseClient(String connectionId) implements EventWithoutSnowflake {} + public record CloseClient(long connectionId) implements EventWithoutSnowflake {} /** * Event to start a new client connection to a server. @@ -48,14 +49,12 @@ public class NetworkEvents extends EventsBase { * or {@code StartClientFailure} event may carry the same {@code eventId}. *

* - * @param handlerFactory Factory for constructing a {@link NetworkingGameClientHandler}. * @param ip The IP address of the server to connect to. * @param port The port number of the server to connect to. * @param eventSnowflake A unique identifier for this event, typically injected * automatically by the {@link org.toop.framework.eventbus.EventFlow}. */ public record StartClient( - Supplier handlerFactory, String ip, int port, long eventSnowflake @@ -94,24 +93,12 @@ public class NetworkEvents extends EventsBase { } } - /** - * TODO: Update docs new input. - * BLOCKING Triggers starting a server connection and returns a future. - * - * @param ip The IP address of the server to connect to. - * @param port The port of the server to connect to. - * @param future Returns the UUID of the connection, when connection is established. - */ - public record StartClientRequest( - Supplier handlerFactory, - String ip, int port, CompletableFuture future) implements EventWithoutSnowflake {} - /** * * @param clientId The ID of the client to be used in requests. * @param eventSnowflake The eventID used in checking if event is for you. */ - public record StartClientSuccess(String clientId, long eventSnowflake) + public record StartClientResponse(long clientId, long eventSnowflake) implements EventWithSnowflake { @Override public Map result() { @@ -134,28 +121,34 @@ public class NetworkEvents extends EventsBase { } } + /** + * + * @param clientId The ID of the client that received the response. + */ + public record ServerResponse(long clientId) implements EventWithoutSnowflake {} + /** * Triggers sending a command to a server. * * @param connectionId The UUID of the connection to send the command on. * @param args The command arguments. */ - public record SendCommand(String connectionId, String... args) implements EventWithoutSnowflake {} + public record SendCommand(long connectionId, String... args) implements EventWithoutSnowflake {} /** * Triggers reconnecting to a previous address. * * @param connectionId The identifier of the connection being reconnected. */ - public record Reconnect(Object connectionId) implements EventWithoutSnowflake {} + public record Reconnect(long connectionId) implements EventWithoutSnowflake {} /** * Triggers when the server client receives a message. * - * @param ConnectionUuid The UUID of the connection that received the message. + * @param ConnectionId The snowflake id of the connection that received the message. * @param message The message received. */ - public record ReceivedMessage(String ConnectionUuid, String message) implements EventWithoutSnowflake {} + public record ReceivedMessage(long ConnectionId, String message) implements EventWithoutSnowflake {} /** * Triggers changing connection to a new address. @@ -164,7 +157,7 @@ public class NetworkEvents extends EventsBase { * @param ip The new IP address. * @param port The new port. */ - public record ChangeClient(Object connectionId, String ip, int port) implements EventWithoutSnowflake {} + public record ChangeClient(long connectionId, String ip, int port) implements EventWithoutSnowflake {} /** @@ -172,7 +165,7 @@ public class NetworkEvents extends EventsBase { * * @param connectionId The identifier of the connection that failed. */ - public record CouldNotConnect(Object connectionId) implements EventWithoutSnowflake {} + public record CouldNotConnect(long connectionId) implements EventWithoutSnowflake {} /** WIP Triggers when a connection closes. */ public record ClosedConnection() implements EventWithoutSnowflake {} diff --git a/framework/src/test/java/org/toop/framework/eventbus/EventPublisherStressTest.java b/framework/src/test/java/org/toop/framework/eventbus/EventPublisherStressTest.java index efd9ce9..a9704a0 100644 --- a/framework/src/test/java/org/toop/framework/eventbus/EventPublisherStressTest.java +++ b/framework/src/test/java/org/toop/framework/eventbus/EventPublisherStressTest.java @@ -163,10 +163,9 @@ class EventPublisherStressTest { for (int i = 0; i < EVENTS_PER_THREAD; i++) { var a = new EventFlow().addPostEvent(HeavyEvent.class, "payload-" + i) .onResponse(HeavyEventSuccess.class, _ -> counter.increment()) - .unsubscribeAfterSuccess() .postEvent(); - new EventFlow().addPostEvent(HeavyEventSuccess.class, "payload-" + i, a.getEventId()) + new EventFlow().addPostEvent(HeavyEventSuccess.class, "payload-" + i, a.getEventSnowflake()) .postEvent(); } }); diff --git a/framework/src/test/java/org/toop/framework/eventbus/EventPublisherTest.java b/framework/src/test/java/org/toop/framework/eventbus/EventPublisherTest.java index 2a29dd0..6303a1a 100644 --- a/framework/src/test/java/org/toop/framework/eventbus/EventPublisherTest.java +++ b/framework/src/test/java/org/toop/framework/eventbus/EventPublisherTest.java @@ -1,6 +1,7 @@ package org.toop.framework.eventbus; import org.junit.jupiter.api.Test; +import org.toop.framework.SnowflakeGenerator; import org.toop.framework.eventbus.events.EventWithSnowflake; import java.util.HashSet; @@ -13,7 +14,7 @@ class EventFlowTest { @Test void testSnowflakeStructure() { - long id = new SnowflakeGenerator(1).nextId(); + long id = new SnowflakeGenerator().nextId(); long timestampPart = id >>> 22; long randomPart = id & ((1L << 22) - 1); @@ -55,7 +56,7 @@ class EventFlowTest { EventFlow flow = new EventFlow(); flow.addPostEvent(DummySnowflakeEvent.class); // no args, should auto-generate - long id = flow.getEventId(); + long id = flow.getEventSnowflake(); assertNotEquals(-1, id, "Snowflake should be auto-generated"); assertTrue(flow.getEvent() instanceof DummySnowflakeEvent); assertEquals(id, ((DummySnowflakeEvent) flow.getEvent()).eventSnowflake()); @@ -74,7 +75,7 @@ class EventFlowTest { assertFalse(handlerCalled.get(), "Handler should not fire for mismatched snowflake"); // Post with matching snowflake - GlobalEventBus.post(new DummySnowflakeEvent(flow.getEventId())); + GlobalEventBus.post(new DummySnowflakeEvent(flow.getEventSnowflake())); assertTrue(handlerCalled.get(), "Handler should fire for matching snowflake"); } } \ No newline at end of file