Fixed unittests. Formatting

This commit is contained in:
lieght
2025-09-28 21:45:58 +02:00
committed by Bas Antonius de Jong
parent c76b7a800e
commit a94d83292e
25 changed files with 1506 additions and 945 deletions

View File

@@ -42,9 +42,14 @@ public class SnowflakeGenerator {
}
}
void setTime(long l) {
this.lastTimestamp.set(l);
}
public SnowflakeGenerator() {
if (machineId < 0 || machineId > MAX_MACHINE_ID) {
throw new IllegalArgumentException("Machine ID must be between 0 and " + MAX_MACHINE_ID);
throw new IllegalArgumentException(
"Machine ID must be between 0 and " + MAX_MACHINE_ID);
}
}
@@ -87,4 +92,4 @@ public class SnowflakeGenerator {
private long timestamp() {
return System.currentTimeMillis();
}
}
}

View File

@@ -1,9 +1,5 @@
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 java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
@@ -13,20 +9,21 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.toop.framework.SnowflakeGenerator;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.events.EventWithSnowflake;
/**
* EventFlow is a utility class for creating, posting, and optionally subscribing to events
* in a type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}.
* EventFlow is a utility class for creating, posting, and optionally subscribing to events in a
* type-safe and chainable manner. It is designed to work with the {@link GlobalEventBus}.
*
* <p>This class supports automatic UUID assignment for {@link EventWithSnowflake} events,
* and allows filtering subscribers so they only respond to events with a specific UUID.
* All subscription methods are chainable, and you can configure automatic unsubscription
* after an event has been successfully handled.</p>
* <p>This class supports automatic UUID assignment for {@link EventWithSnowflake} events, and
* allows filtering subscribers so they only respond to events with a specific UUID. All
* subscription methods are chainable, and you can configure automatic unsubscription after an event
* has been successfully handled.
*/
public class EventFlow {
/** Lookup object used for dynamically invoking constructors via MethodHandles. */
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
@@ -65,15 +62,20 @@ public class EventFlow {
try {
boolean isUuidEvent = EventWithSnowflake.class.isAssignableFrom(eventClass);
MethodHandle ctorHandle = CONSTRUCTOR_CACHE.computeIfAbsent(eventClass, cls -> {
try {
Class<?>[] paramTypes = cls.getDeclaredConstructors()[0].getParameterTypes();
MethodType mt = MethodType.methodType(void.class, paramTypes);
return LOOKUP.findConstructor(cls, mt);
} catch (Exception e) {
throw new RuntimeException("Failed to find constructor handle for " + cls, e);
}
});
MethodHandle ctorHandle =
CONSTRUCTOR_CACHE.computeIfAbsent(
eventClass,
cls -> {
try {
Class<?>[] paramTypes =
cls.getDeclaredConstructors()[0].getParameterTypes();
MethodType mt = MethodType.methodType(void.class, paramTypes);
return LOOKUP.findConstructor(cls, mt);
} catch (Exception e) {
throw new RuntimeException(
"Failed to find constructor handle for " + cls, e);
}
});
Object[] finalArgs;
int expectedParamCount = ctorHandle.type().parameterCount();
@@ -98,67 +100,69 @@ public class EventFlow {
}
}
// public EventFlow addSnowflake() {
// this.eventSnowflake = new SnowflakeGenerator(1).nextId();
// return this;
// }
// public EventFlow addSnowflake() {
// this.eventSnowflake = new SnowflakeGenerator(1).nextId();
// return this;
// }
/**
* Subscribe by ID: only fires if UUID matches this publisher's eventId.
*/
public <TT extends EventWithSnowflake> EventFlow onResponse(Class<TT> eventClass, Consumer<TT> action,
boolean unsubscribeAfterSuccess) {
/** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
public <TT extends EventWithSnowflake> EventFlow onResponse(
Class<TT> eventClass, Consumer<TT> action, boolean unsubscribeAfterSuccess) {
ListenerHandler[] listenerHolder = new ListenerHandler[1];
listenerHolder[0] = new ListenerHandler(
GlobalEventBus.subscribe(eventClass, event -> {
if (event.eventSnowflake() != this.eventSnowflake) return;
listenerHolder[0] =
new ListenerHandler(
GlobalEventBus.subscribe(
eventClass,
event -> {
if (event.eventSnowflake() != this.eventSnowflake) return;
action.accept(event);
action.accept(event);
if (unsubscribeAfterSuccess && listenerHolder[0] != null) {
GlobalEventBus.unsubscribe(listenerHolder[0]);
this.listeners.remove(listenerHolder[0]);
}
if (unsubscribeAfterSuccess && listenerHolder[0] != null) {
GlobalEventBus.unsubscribe(listenerHolder[0]);
this.listeners.remove(listenerHolder[0]);
}
this.result = event.result();
})
);
this.result = event.result();
}));
this.listeners.add(listenerHolder[0]);
return this;
}
/**
* Subscribe by ID: only fires if UUID matches this publisher's eventId.
*/
public <TT extends EventWithSnowflake> EventFlow onResponse(Class<TT> eventClass, Consumer<TT> action) {
/** Subscribe by ID: only fires if UUID matches this publisher's eventId. */
public <TT extends EventWithSnowflake> EventFlow onResponse(
Class<TT> eventClass, Consumer<TT> action) {
return this.onResponse(eventClass, action, true);
}
/**
* Subscribe by ID without explicit class.
*/
/** Subscribe by ID without explicit class. */
@SuppressWarnings("unchecked")
public <TT extends EventWithSnowflake> EventFlow onResponse(Consumer<TT> action, boolean unsubscribeAfterSuccess) {
public <TT extends EventWithSnowflake> EventFlow onResponse(
Consumer<TT> 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 && listenerHolder[0] != null) {
GlobalEventBus.unsubscribe(listenerHolder[0]);
this.listeners.remove(listenerHolder[0]);
}
this.result = typedEvent.result();
} catch (ClassCastException _) {
throw new ClassCastException("Cannot cast " + event.getClass().getName() +
" to EventWithSnowflake");
}
}
})
);
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
&& listenerHolder[0] != null) {
GlobalEventBus.unsubscribe(listenerHolder[0]);
this.listeners.remove(listenerHolder[0]);
}
this.result = typedEvent.result();
} catch (ClassCastException _) {
throw new ClassCastException(
"Cannot cast "
+ event.getClass().getName()
+ " to EventWithSnowflake");
}
}
}));
this.listeners.add(listenerHolder[0]);
return this;
}
@@ -167,19 +171,21 @@ public class EventFlow {
return this.onResponse(action, true);
}
public <TT extends EventType> EventFlow listen(Class<TT> eventClass, Consumer<TT> action,
boolean unsubscribeAfterSuccess) {
public <TT extends EventType> EventFlow listen(
Class<TT> eventClass, Consumer<TT> action, boolean unsubscribeAfterSuccess) {
ListenerHandler[] listenerHolder = new ListenerHandler[1];
listenerHolder[0] = new ListenerHandler(
GlobalEventBus.subscribe(eventClass, event -> {
action.accept(event);
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]);
}
})
);
if (unsubscribeAfterSuccess && listenerHolder[0] != null) {
GlobalEventBus.unsubscribe(listenerHolder[0]);
this.listeners.remove(listenerHolder[0]);
}
}));
this.listeners.add(listenerHolder[0]);
return this;
}
@@ -189,24 +195,28 @@ public class EventFlow {
}
@SuppressWarnings("unchecked")
public <TT extends EventType> EventFlow listen(Consumer<TT> action, boolean unsubscribeAfterSuccess) {
public <TT extends EventType> EventFlow listen(
Consumer<TT> 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");
}
})
);
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;
}

View File

@@ -3,26 +3,26 @@ package org.toop.framework.eventbus;
import com.lmax.disruptor.*;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.events.EventWithSnowflake;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Consumer;
import org.toop.framework.eventbus.events.EventType;
import org.toop.framework.eventbus.events.EventWithSnowflake;
/**
* GlobalEventBus backed by the LMAX Disruptor for ultra-low latency,
* high-throughput event publishing.
* GlobalEventBus backed by the LMAX Disruptor for ultra-low latency, high-throughput event
* publishing.
*/
public final class GlobalEventBus {
/** Map of event class to type-specific listeners. */
private static final Map<Class<?>, CopyOnWriteArrayList<Consumer<? super EventType>>> LISTENERS =
new ConcurrentHashMap<>();
private static final Map<Class<?>, CopyOnWriteArrayList<Consumer<? super EventType>>>
LISTENERS = new ConcurrentHashMap<>();
/** Map of event class to Snowflake-ID-specific listeners. */
private static final Map<Class<?>, ConcurrentHashMap<Long, Consumer<? extends EventWithSnowflake>>> UUID_LISTENERS =
new ConcurrentHashMap<>();
private static final Map<
Class<?>, ConcurrentHashMap<Long, Consumer<? extends EventWithSnowflake>>>
UUID_LISTENERS = new ConcurrentHashMap<>();
/** Disruptor ring buffer size (must be power of two). */
private static final int RING_BUFFER_SIZE = 1024 * 64;
@@ -34,27 +34,29 @@ public final class GlobalEventBus {
private static final RingBuffer<EventHolder> RING_BUFFER;
static {
ThreadFactory threadFactory = r -> {
Thread t = new Thread(r, "EventBus-Disruptor");
t.setDaemon(true);
return t;
};
ThreadFactory threadFactory =
r -> {
Thread t = new Thread(r, "EventBus-Disruptor");
t.setDaemon(true);
return t;
};
DISRUPTOR = new Disruptor<>(
EventHolder::new,
RING_BUFFER_SIZE,
threadFactory,
ProducerType.MULTI,
new BusySpinWaitStrategy()
);
DISRUPTOR =
new Disruptor<>(
EventHolder::new,
RING_BUFFER_SIZE,
threadFactory,
ProducerType.MULTI,
new BusySpinWaitStrategy());
// Single consumer that dispatches to subscribers
DISRUPTOR.handleEventsWith((holder, seq, endOfBatch) -> {
if (holder.event != null) {
dispatchEvent(holder.event);
holder.event = null;
}
});
DISRUPTOR.handleEventsWith(
(holder, seq, endOfBatch) -> {
if (holder.event != null) {
dispatchEvent(holder.event);
holder.event = null;
}
});
DISRUPTOR.start();
RING_BUFFER = DISRUPTOR.getRingBuffer();
@@ -71,17 +73,21 @@ public final class GlobalEventBus {
// ------------------------------------------------------------------------
// Subscription
// ------------------------------------------------------------------------
public static <T extends EventType> Consumer<T> subscribe(Class<T> eventClass, Consumer<T> listener) {
public static <T extends EventType> Consumer<? super EventType> subscribe(
Class<T> eventClass, Consumer<T> listener) {
CopyOnWriteArrayList<Consumer<? super EventType>> list =
LISTENERS.computeIfAbsent(eventClass, k -> new CopyOnWriteArrayList<>());
list.add(event -> listener.accept(eventClass.cast(event)));
return listener;
Consumer<? super EventType> wrapper = event -> listener.accept(eventClass.cast(event));
list.add(wrapper);
return wrapper;
}
public static Consumer<Object> subscribe(Consumer<Object> listener) {
LISTENERS.computeIfAbsent(Object.class, _ -> new CopyOnWriteArrayList<>())
.add(listener);
return listener;
public static Consumer<? super EventType> subscribe(Consumer<Object> listener) {
Consumer<? super EventType> wrapper = event -> listener.accept(event);
LISTENERS.computeIfAbsent(Object.class, _ -> new CopyOnWriteArrayList<>()).add(wrapper);
return wrapper;
}
public static <T extends EventWithSnowflake> void subscribeById(
@@ -95,7 +101,8 @@ public final class GlobalEventBus {
LISTENERS.values().forEach(list -> list.remove(listener));
}
public static <T extends EventWithSnowflake> void unsubscribeById(Class<T> eventClass, long eventId) {
public static <T extends EventWithSnowflake> void unsubscribeById(
Class<T> eventClass, long eventId) {
Map<Long, Consumer<? extends EventWithSnowflake>> map = UUID_LISTENERS.get(eventClass);
if (map != null) map.remove(eventId);
}
@@ -125,15 +132,22 @@ public final class GlobalEventBus {
CopyOnWriteArrayList<Consumer<? super EventType>> classListeners = LISTENERS.get(clazz);
if (classListeners != null) {
for (Consumer<? super EventType> listener : classListeners) {
try { listener.accept(event); } catch (Throwable ignored) {}
try {
listener.accept(event);
} catch (Throwable ignored) {
}
}
}
// generic listeners
CopyOnWriteArrayList<Consumer<? super EventType>> genericListeners = LISTENERS.get(Object.class);
CopyOnWriteArrayList<Consumer<? super EventType>> genericListeners =
LISTENERS.get(Object.class);
if (genericListeners != null) {
for (Consumer<? super EventType> listener : genericListeners) {
try { listener.accept(event); } catch (Throwable ignored) {}
try {
listener.accept(event);
} catch (Throwable ignored) {
}
}
}
@@ -144,7 +158,10 @@ public final class GlobalEventBus {
Consumer<EventWithSnowflake> listener =
(Consumer<EventWithSnowflake>) map.remove(snowflakeEvent.eventSnowflake());
if (listener != null) {
try { listener.accept(snowflakeEvent); } catch (Throwable ignored) {}
try {
listener.accept(snowflakeEvent);
} catch (Throwable ignored) {
}
}
}
}

View File

@@ -1,15 +1,14 @@
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;
// }
// private boolean unsubscribeAfterSuccess = true;
// public ListenerHandler(Object listener, boolean unsubAfterSuccess) {
// this.listener = listener;
// this.unsubscribeAfterSuccess = unsubAfterSuccess;
// }
public ListenerHandler(Object listener) {
this.listener = listener;
@@ -19,8 +18,8 @@ public class ListenerHandler {
return this.listener;
}
// public boolean isUnsubscribeAfterSuccess() {
// return this.unsubscribeAfterSuccess;
// }
// public boolean isUnsubscribeAfterSuccess() {
// return this.unsubscribeAfterSuccess;
// }
}

View File

@@ -9,13 +9,12 @@ import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import java.util.function.Supplier;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.framework.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
import java.util.function.Supplier;
public class NetworkingClient {
private static final Logger logger = LogManager.getLogger(NetworkingClient.class);
@@ -37,18 +36,20 @@ public class NetworkingClient {
bootstrap.group(workerGroup);
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
handler = handlerFactory.get();
bootstrap.handler(
new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
handler = handlerFactory.get();
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)); // bytes -> String
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(handler);
}
});
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(1024)); // split at \n
pipeline.addLast(
new StringDecoder(CharsetUtil.UTF_8)); // bytes -> String
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(handler);
}
});
ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
this.channel = channelFuture.channel();
this.host = host;
@@ -82,7 +83,8 @@ 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);
}
@@ -99,23 +101,30 @@ public class NetworkingClient {
public void closeConnection() {
if (this.channel != null && this.channel.isActive()) {
this.channel.close().addListener(future -> {
if (future.isSuccess()) {
logger.info("Connection {} closed successfully", this.channel.remoteAddress());
new EventFlow()
.addPostEvent(new NetworkEvents.ClosedConnection(this.connectionId))
.asyncPostEvent();
} else {
logger.error("Error closing connection {}. Error: {}",
this.channel.remoteAddress(),
future.cause().getMessage());
}
});
this.channel
.close()
.addListener(
future -> {
if (future.isSuccess()) {
logger.info(
"Connection {} closed successfully",
this.channel.remoteAddress());
new EventFlow()
.addPostEvent(
new NetworkEvents.ClosedConnection(
this.connectionId))
.asyncPostEvent();
} else {
logger.error(
"Error closing connection {}. Error: {}",
this.channel.remoteAddress(),
future.cause().getMessage());
}
});
}
}
public long getId() {
return this.connectionId;
}
}

View File

@@ -2,11 +2,10 @@ package org.toop.framework.networking;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
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.eventbus.EventFlow;
import org.toop.framework.networking.events.NetworkEvents;
public class NetworkingClientManager {
@@ -14,7 +13,7 @@ public class NetworkingClientManager {
private static final Logger logger = LogManager.getLogger(NetworkingClientManager.class);
/** Map of serverId -> Server instances */
private final Map<Long, NetworkingClient> networkClients = new ConcurrentHashMap<>();
final Map<Long, NetworkingClient> networkClients = new ConcurrentHashMap<>();
/** Starts a connection manager, to manage, connections. */
public NetworkingClientManager() throws NetworkingInitializationException {
@@ -39,20 +38,21 @@ public class NetworkingClientManager {
.listen(this::handleGetAllConnections)
.listen(this::handleShutdownAll);
logger.info("NetworkingClientManager initialized");
} catch (Exception e) {
} catch (Exception e) {
logger.error("Failed to initialize the client manager", e);
throw e;
}
}
private long startClientRequest(String ip, int port) {
long startClientRequest(String ip, int port) {
long connectionId = new SnowflakeGenerator().nextId(); // TODO: Maybe use the one generated
try { // With EventFlow
NetworkingClient client = new NetworkingClient(
() -> new NetworkingGameClientHandler(connectionId),
ip,
port,
connectionId);
try { // With EventFlow
NetworkingClient client =
new NetworkingClient(
() -> new NetworkingGameClientHandler(connectionId),
ip,
port,
connectionId);
client.setConnectionId(connectionId);
this.networkClients.put(connectionId, client);
logger.info("New client started successfully for {}:{}", ip, port);
@@ -63,15 +63,14 @@ public class NetworkingClientManager {
}
private long startClientRequest(String ip, int port, long clientId) {
try { // With EventFlow
NetworkingClient client = new NetworkingClient(
() -> new NetworkingGameClientHandler(clientId),
ip,
port,
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);
logger.info(
"New client started successfully for {}:{}, replaced: {}", ip, port, clientId);
} catch (Exception e) {
logger.error(e);
}
@@ -79,21 +78,26 @@ public class NetworkingClientManager {
return clientId;
}
private void handleStartClient(NetworkEvents.StartClient event) {
void handleStartClient(NetworkEvents.StartClient event) {
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();
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(
void handleCommand(
NetworkEvents.SendCommand
event) { // TODO: Move this to ServerConnection class, keep it internal.
NetworkingClient client = this.networkClients.get(event.clientId());
@@ -101,7 +105,7 @@ public class NetworkingClientManager {
sendCommand(client, args);
}
private void handleSendLogin(NetworkEvents.SendLogin event) {
void handleSendLogin(NetworkEvents.SendLogin event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, String.format("LOGIN %s", event.username()));
}
@@ -133,7 +137,9 @@ public class NetworkingClientManager {
private void handleSendChallenge(NetworkEvents.SendChallenge event) {
NetworkingClient client = this.networkClients.get(event.clientId());
sendCommand(client, String.format("CHALLENGE %s %s", event.usernameToChallenge(), event.gameType()));
sendCommand(
client,
String.format("CHALLENGE %s %s", event.usernameToChallenge(), event.gameType()));
}
private void handleSendAcceptChallenge(NetworkEvents.SendAcceptChallenge event) {
@@ -162,8 +168,12 @@ public class NetworkingClientManager {
}
private void sendCommand(NetworkingClient client, String command) {
logger.info("Preparing to send command: {} to server: {}:{}. clientId: {}",
command.trim(), client.getHost(), client.getPort(), client.getId());
logger.info(
"Preparing to send command: {} to server: {}:{}. clientId: {}",
command.trim(),
client.getHost(),
client.getPort(),
client.getId());
client.writeAndFlushnl(command);
}
@@ -173,14 +183,14 @@ public class NetworkingClientManager {
startClientRequest(event.ip(), event.port(), event.clientId());
}
private void handleCloseClient(NetworkEvents.CloseClient event) {
void handleCloseClient(NetworkEvents.CloseClient event) {
NetworkingClient client = this.networkClients.get(event.clientId());
client.closeConnection(); // TODO: Check if not blocking, what if error, mb not remove?
this.networkClients.remove(event.clientId());
logger.info("Client {} closed successfully.", event.clientId());
}
private void handleGetAllConnections(NetworkEvents.RequestsAllClients request) {
void handleGetAllConnections(NetworkEvents.RequestsAllClients request) {
List<NetworkingClient> a = new ArrayList<>(this.networkClients.values());
request.future().complete(a);
}

View File

@@ -2,14 +2,13 @@ package org.toop.framework.networking;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.regex.Matcher;
import java.util.regex.Pattern;
public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger(NetworkingGameClientHandler.class);
@@ -28,17 +27,27 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
return;
}
if (rec.equalsIgnoreCase("ok")) {
logger.info("Received OK message from server-{}, data: {}", ctx.channel().remoteAddress(), msg);
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();
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);
logger.info(
"Received unparsed message from server-{}, data: {}",
ctx.channel().remoteAddress(),
msg);
}
private void parseServerReturn(String rec) {
@@ -48,27 +57,43 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
Matcher gameMatch = gamePattern.matcher(recSrvRemoved);
if (gameMatch.find()) {
switch(gameMatch.group(1)) {
case "YOURTURN": gameYourTurnHandler(recSrvRemoved); return;
case "MOVE": gameMoveHandler(recSrvRemoved); return;
case "MATCH": gameMatchHandler(recSrvRemoved); return;
case "CHALLENGE": gameChallengeHandler(recSrvRemoved); return;
case "WIN",
"DRAW",
"LOSE": gameWinConditionHandler(recSrvRemoved); return;
default: return;
switch (gameMatch.group(1)) {
case "YOURTURN":
gameYourTurnHandler(recSrvRemoved);
return;
case "MOVE":
gameMoveHandler(recSrvRemoved);
return;
case "MATCH":
gameMatchHandler(recSrvRemoved);
return;
case "CHALLENGE":
gameChallengeHandler(recSrvRemoved);
return;
case "WIN", "DRAW", "LOSE":
gameWinConditionHandler(recSrvRemoved);
return;
default:
return;
}
} else {
Pattern getPattern = Pattern.compile("(\\w+)", Pattern.CASE_INSENSITIVE);
Matcher getMatch = getPattern.matcher(recSrvRemoved);
Pattern getPattern = Pattern.compile("(\\w+)", Pattern.CASE_INSENSITIVE);
Matcher getMatch = getPattern.matcher(recSrvRemoved);
if (getMatch.find()) {
switch(getMatch.group(1)) {
case "PLAYERLIST": playerlistHandler(recSrvRemoved); return;
case "GAMELIST": gamelistHandler(recSrvRemoved); return;
case "HELP": helpHandler(recSrvRemoved); return;
default: return;
switch (getMatch.group(1)) {
case "PLAYERLIST":
playerlistHandler(recSrvRemoved);
return;
case "GAMELIST":
gamelistHandler(recSrvRemoved);
return;
case "HELP":
helpHandler(recSrvRemoved);
return;
default:
return;
}
} else {
return; // TODO: Should be an error.
@@ -77,25 +102,29 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
}
private void gameMoveHandler(String rec) {
String[] msg = Pattern
.compile("(?:player|details|move):\\s*\"?([^\",}]+)\"?", Pattern.CASE_INSENSITIVE)
.matcher(rec)
.results()
.map(m -> m.group(1).trim())
.toArray(String[]::new);
String[] msg =
Pattern.compile(
"(?:player|details|move):\\s*\"?([^\",}]+)\"?",
Pattern.CASE_INSENSITIVE)
.matcher(rec)
.results()
.map(m -> m.group(1).trim())
.toArray(String[]::new);
new EventFlow()
.addPostEvent(new NetworkEvents.GameMoveResponse(this.connectionId, msg[0], msg[1], msg[2]))
.addPostEvent(
new NetworkEvents.GameMoveResponse(
this.connectionId, msg[0], msg[1], msg[2]))
.asyncPostEvent();
}
private void gameWinConditionHandler(String rec) {
String condition = Pattern
.compile("\\b(win|draw|lose)\\b", Pattern.CASE_INSENSITIVE)
.matcher(rec)
.results()
.toString()
.trim();
String condition =
Pattern.compile("\\b(win|draw|lose)\\b", Pattern.CASE_INSENSITIVE)
.matcher(rec)
.results()
.toString()
.trim();
new EventFlow()
.addPostEvent(new NetworkEvents.GameResultResponse(this.connectionId, condition))
@@ -105,19 +134,26 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
private void gameChallengeHandler(String rec) {
boolean isCancelled = rec.toLowerCase().startsWith("challenge accepted");
try {
String[] msg = Pattern
.compile("(?:CHALLENGER|GAMETYPE|CHALLENGENUMBER):\\s*\"?(.*?)\"?\\s*(?:,|})")
.matcher(rec)
.results()
.map(m -> m.group().trim())
.toArray(String[]::new);
String[] msg =
Pattern.compile(
"(?:CHALLENGER|GAMETYPE|CHALLENGENUMBER):\\s*\"?(.*?)\"?\\s*(?:,|})")
.matcher(rec)
.results()
.map(m -> m.group().trim())
.toArray(String[]::new);
if (isCancelled) new EventFlow()
.addPostEvent(new NetworkEvents.ChallengeCancelledResponse(this.connectionId, msg[0]))
.asyncPostEvent();
else new EventFlow()
.addPostEvent(new NetworkEvents.ChallengeResponse(this.connectionId, msg[0], msg[1], msg[2]))
.asyncPostEvent();
if (isCancelled)
new EventFlow()
.addPostEvent(
new NetworkEvents.ChallengeCancelledResponse(
this.connectionId, msg[0]))
.asyncPostEvent();
else
new EventFlow()
.addPostEvent(
new NetworkEvents.ChallengeResponse(
this.connectionId, msg[0], msg[1], msg[2]))
.asyncPostEvent();
} catch (ArrayIndexOutOfBoundsException e) {
logger.error("Array out of bounds for: {}", rec, e);
}
@@ -125,30 +161,31 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
private void gameMatchHandler(String rec) {
try {
String[] msg = Pattern
.compile("\"([^\"]*)\"")
.matcher(rec)
.results()
.map(m -> m.group(1).trim())
.toArray(String[]::new);
String[] msg =
Pattern.compile("\"([^\"]*)\"")
.matcher(rec)
.results()
.map(m -> m.group(1).trim())
.toArray(String[]::new);
// [0] playerToMove, [1] gameType, [2] opponent
new EventFlow()
.addPostEvent(new NetworkEvents.GameMatchResponse(this.connectionId, msg[0], msg[1], msg[2]))
.addPostEvent(
new NetworkEvents.GameMatchResponse(
this.connectionId, msg[0], msg[1], msg[2]))
.asyncPostEvent();
} catch (ArrayIndexOutOfBoundsException e) {
logger.error("Array out of bounds for: {}", rec, e);
}
}
private void gameYourTurnHandler(String rec) {
String msg = Pattern
.compile("TURNMESSAGE:\\s*\"([^\"]*)\"")
.matcher(rec)
.results()
.toString()
.trim();
String msg =
Pattern.compile("TURNMESSAGE:\\s*\"([^\"]*)\"")
.matcher(rec)
.results()
.toString()
.trim();
new EventFlow()
.addPostEvent(new NetworkEvents.YourTurnResponse(this.connectionId, msg))
@@ -156,12 +193,12 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
}
private void playerlistHandler(String rec) {
String[] players = Pattern
.compile("\"([^\"]+)\"")
.matcher(rec)
.results()
.map(m -> m.group(1).trim())
.toArray(String[]::new);
String[] players =
Pattern.compile("\"([^\"]+)\"")
.matcher(rec)
.results()
.map(m -> m.group(1).trim())
.toArray(String[]::new);
new EventFlow()
.addPostEvent(new NetworkEvents.PlayerlistResponse(this.connectionId, players))
@@ -169,16 +206,16 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
}
private void gamelistHandler(String rec) {
String[] gameTypes = Pattern
.compile("\"([^\"]+)\"")
.matcher(rec)
.results()
.map(m -> m.group(1).trim())
.toArray(String[]::new);
String[] gameTypes =
Pattern.compile("\"([^\"]+)\"")
.matcher(rec)
.results()
.map(m -> m.group(1).trim())
.toArray(String[]::new);
new EventFlow()
.addPostEvent(new NetworkEvents.GamelistResponse(this.connectionId, gameTypes))
.asyncPostEvent();
.addPostEvent(new NetworkEvents.GamelistResponse(this.connectionId, gameTypes))
.asyncPostEvent();
}
private void helpHandler(String rec) {
@@ -190,5 +227,4 @@ public class NetworkingGameClientHandler extends ChannelInboundHandlerAdapter {
logger.error(cause.getMessage(), cause);
ctx.close();
}
}

View File

@@ -1,66 +1,73 @@
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;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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;
/**
* A collection of networking-related event records for use with the {@link org.toop.framework.eventbus.GlobalEventBus}.
* <p>
* This class defines all the events that can be posted or listened to in the networking subsystem.
* Events are separated into those with unique IDs (EventWithSnowflake) and those without (EventWithoutSnowflake).
* </p>
* A collection of networking-related event records for use with the {@link
* org.toop.framework.eventbus.GlobalEventBus}.
*
* <p>This class defines all the events that can be posted or listened to in the networking
* subsystem. Events are separated into those with unique IDs (EventWithSnowflake) and those without
* (EventWithoutSnowflake).
*/
public class NetworkEvents extends EventsBase {
/**
* Requests all active client connections.
* <p>
* This is a blocking event. The result will be delivered via the provided {@link CompletableFuture}.
* </p>
*
* @param future CompletableFuture to receive the list of active {@link NetworkingClient} instances.
* <p>This is a blocking event. The result will be delivered via the provided {@link
* CompletableFuture}.
*
* @param future CompletableFuture to receive the list of active {@link NetworkingClient}
* instances.
*/
public record RequestsAllClients(CompletableFuture<List<NetworkingClient>> future) implements EventWithoutSnowflake {}
public record RequestsAllClients(CompletableFuture<List<NetworkingClient>> future)
implements EventWithoutSnowflake {}
/** Forces all active client connections to close immediately. */
public record ForceCloseAllClients() implements EventWithoutSnowflake {}
/** Response indicating a challenge was cancelled. */
public record ChallengeCancelledResponse(long clientId, String challengeId) implements EventWithoutSnowflake {}
public record ChallengeCancelledResponse(long clientId, String challengeId)
implements EventWithoutSnowflake {}
/** Response indicating a challenge was received. */
public record ChallengeResponse(long clientId, String challengerName, String gameType, String challengeId)
public record ChallengeResponse(
long clientId, String challengerName, String gameType, String challengeId)
implements EventWithoutSnowflake {}
/** Response containing a list of players for a client. */
public record PlayerlistResponse(long clientId, String[] playerlist) implements EventWithoutSnowflake {}
public record PlayerlistResponse(long clientId, String[] playerlist)
implements EventWithoutSnowflake {}
/** Response containing a list of games for a client. */
public record GamelistResponse(long clientId, String[] gamelist) implements EventWithoutSnowflake {}
public record GamelistResponse(long clientId, String[] gamelist)
implements EventWithoutSnowflake {}
/** Response indicating a game match information for a client. */
public record GameMatchResponse(long clientId, String playerToMove, String gameType, String opponent)
public record GameMatchResponse(
long clientId, String playerToMove, String gameType, String opponent)
implements EventWithoutSnowflake {}
/** Response indicating the result of a game. */
public record GameResultResponse(long clientId, String condition) implements EventWithoutSnowflake {}
public record GameResultResponse(long clientId, String condition)
implements EventWithoutSnowflake {}
/** Response indicating a game move occurred. */
public record GameMoveResponse(long clientId, String player, String details, String move)
implements EventWithoutSnowflake {}
/** Response indicating it is the player's turn. */
public record YourTurnResponse(long clientId, String message) implements EventWithoutSnowflake {}
public record YourTurnResponse(long clientId, String message)
implements EventWithoutSnowflake {}
/** Request to send login credentials for a client. */
public record SendLogin(long clientId, String username) implements EventWithoutSnowflake {}
@@ -85,7 +92,8 @@ public class NetworkEvents extends EventsBase {
implements EventWithoutSnowflake {}
/** Request to accept a challenge. */
public record SendAcceptChallenge(long clientId, int challengeId) implements EventWithoutSnowflake {}
public record SendAcceptChallenge(long clientId, int challengeId)
implements EventWithoutSnowflake {}
/** Request to forfeit a game. */
public record SendForfeit(long clientId) implements EventWithoutSnowflake {}
@@ -97,36 +105,37 @@ public class NetworkEvents extends EventsBase {
public record SendHelp(long clientId) implements EventWithoutSnowflake {}
/** Request to display help for a specific command. */
public record SendHelpForCommand(long clientId, String command) implements EventWithoutSnowflake {}
public record SendHelpForCommand(long clientId, String command)
implements EventWithoutSnowflake {}
/** Request to close a specific client connection. */
public record CloseClient(long clientId) implements EventWithoutSnowflake {}
/**
* Event to start a new client connection.
* <p>
* Carries IP, port, and a unique event ID for correlation with responses.
* </p>
*
* <p>Carries IP, port, and a unique event ID for correlation with responses.
*
* @param ip Server IP address.
* @param port Server port.
* @param eventSnowflake Unique event identifier for correlation.
*/
public record StartClient(String ip, int port, long eventSnowflake) implements EventWithSnowflake {
public record StartClient(String ip, int port, long eventSnowflake)
implements EventWithSnowflake {
@Override
public Map<String, Object> result() {
return Stream.of(this.getClass().getRecordComponents())
.collect(Collectors.toMap(
RecordComponent::getName,
rc -> {
try {
return rc.getAccessor().invoke(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
));
.collect(
Collectors.toMap(
RecordComponent::getName,
rc -> {
try {
return rc.getAccessor().invoke(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}));
}
@Override
@@ -141,20 +150,21 @@ public class NetworkEvents extends EventsBase {
* @param clientId The client ID assigned to the new connection.
* @param eventSnowflake Event ID used for correlation.
*/
public record StartClientResponse(long clientId, long eventSnowflake) implements EventWithSnowflake {
public record StartClientResponse(long clientId, long eventSnowflake)
implements EventWithSnowflake {
@Override
public Map<String, Object> result() {
return Stream.of(this.getClass().getRecordComponents())
.collect(Collectors.toMap(
RecordComponent::getName,
rc -> {
try {
return rc.getAccessor().invoke(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
));
.collect(
Collectors.toMap(
RecordComponent::getName,
rc -> {
try {
return rc.getAccessor().invoke(this);
} catch (Exception e) {
throw new RuntimeException(e);
}
}));
}
@Override
@@ -192,7 +202,8 @@ public class NetworkEvents extends EventsBase {
* @param ip The new server IP.
* @param port The new server port.
*/
public record ChangeClientHost(long clientId, String ip, int port) implements EventWithoutSnowflake {}
public record ChangeClientHost(long clientId, String ip, int port)
implements EventWithoutSnowflake {}
/** WIP (Not working) Response indicating that the client could not connect. */
public record CouldNotConnect(long clientId) implements EventWithoutSnowflake {}