From ef14edffde36a5992b630be9edbd0c2bb43f87b4 Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Sat, 31 Jan 2026 17:02:24 +0100 Subject: [PATCH] feat: Started basic implementation of socket comm --- .idea/FuzzierSettings.xml | 14 +++ .idea/compiler.xml | 1 + .idea/misc.xml | 2 +- common/pom.xml | 7 ++ .../jzitnik/common/socket/SocketMessage.java | 6 ++ .../jzitnik/common/socket/messages/Test.java | 6 ++ game/.idea/encodings.xml | 6 ++ game/.idea/misc.xml | 16 +--- game/pom.xml | 6 ++ game/src/main/java/cz/jzitnik/client/Cli.java | 8 +- .../src/main/java/cz/jzitnik/client/Game.java | 20 +++- .../annotations/SocketEventHandler.java | 14 +++ .../client/events/SendSocketMessageEvent.java | 7 ++ .../SendSocketMessageEventHandler.java | 24 +++++ .../socket/AbstractSocketEventHandler.java | 7 ++ .../java/cz/jzitnik/client/socket/Client.java | 50 ++++++++++ .../client/socket/SocketEventManager.java | 94 +++++++++++++++++++ .../client/socket/events/TestHandler.java | 15 +++ .../utils/events/AbstractEventHandler.java | 2 +- .../client/utils/events/EventManager.java | 4 +- server/pom.xml | 44 +++++++++ .../src/main/java/cz/jzitnik/server/Main.java | 15 ++- .../java/cz/jzitnik/server/WebSocket.java | 53 +++++++++++ 23 files changed, 394 insertions(+), 27 deletions(-) create mode 100644 .idea/FuzzierSettings.xml create mode 100644 common/src/main/java/cz/jzitnik/common/socket/SocketMessage.java create mode 100644 common/src/main/java/cz/jzitnik/common/socket/messages/Test.java create mode 100644 game/src/main/java/cz/jzitnik/client/annotations/SocketEventHandler.java create mode 100644 game/src/main/java/cz/jzitnik/client/events/SendSocketMessageEvent.java create mode 100644 game/src/main/java/cz/jzitnik/client/events/handlers/SendSocketMessageEventHandler.java create mode 100644 game/src/main/java/cz/jzitnik/client/socket/AbstractSocketEventHandler.java create mode 100644 game/src/main/java/cz/jzitnik/client/socket/Client.java create mode 100644 game/src/main/java/cz/jzitnik/client/socket/SocketEventManager.java create mode 100644 game/src/main/java/cz/jzitnik/client/socket/events/TestHandler.java create mode 100644 server/src/main/java/cz/jzitnik/server/WebSocket.java diff --git a/.idea/FuzzierSettings.xml b/.idea/FuzzierSettings.xml new file mode 100644 index 0000000..4426b5e --- /dev/null +++ b/.idea/FuzzierSettings.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 5c84731..8fa91a7 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -2,6 +2,7 @@ + diff --git a/.idea/misc.xml b/.idea/misc.xml index 86993b2..1044c37 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,5 +8,5 @@ - + \ No newline at end of file diff --git a/common/pom.xml b/common/pom.xml index 7303d3b..6c1d95c 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -24,5 +24,12 @@ jackson-dataformat-yaml 3.0.4 + + + org.projectlombok + lombok + 1.18.42 + provided + diff --git a/common/src/main/java/cz/jzitnik/common/socket/SocketMessage.java b/common/src/main/java/cz/jzitnik/common/socket/SocketMessage.java new file mode 100644 index 0000000..3134c92 --- /dev/null +++ b/common/src/main/java/cz/jzitnik/common/socket/SocketMessage.java @@ -0,0 +1,6 @@ +package cz.jzitnik.common.socket; + +import java.io.Serializable; + +public interface SocketMessage extends Serializable { +} diff --git a/common/src/main/java/cz/jzitnik/common/socket/messages/Test.java b/common/src/main/java/cz/jzitnik/common/socket/messages/Test.java new file mode 100644 index 0000000..00d3703 --- /dev/null +++ b/common/src/main/java/cz/jzitnik/common/socket/messages/Test.java @@ -0,0 +1,6 @@ +package cz.jzitnik.common.socket.messages; + +import cz.jzitnik.common.socket.SocketMessage; + +public class Test implements SocketMessage { +} diff --git a/game/.idea/encodings.xml b/game/.idea/encodings.xml index aa00ffa..fd66ed6 100644 --- a/game/.idea/encodings.xml +++ b/game/.idea/encodings.xml @@ -1,6 +1,12 @@ + + + + + + diff --git a/game/.idea/misc.xml b/game/.idea/misc.xml index 3202223..1044c37 100644 --- a/game/.idea/misc.xml +++ b/game/.idea/misc.xml @@ -1,17 +1,5 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/game/pom.xml b/game/pom.xml index 759b5da..6056ef3 100644 --- a/game/pom.xml +++ b/game/pom.xml @@ -179,5 +179,11 @@ jvm 2.5 + + + org.glassfish.tyrus.bundles + tyrus-standalone-client + 2.2.2 + diff --git a/game/src/main/java/cz/jzitnik/client/Cli.java b/game/src/main/java/cz/jzitnik/client/Cli.java index 897412f..073d3a8 100644 --- a/game/src/main/java/cz/jzitnik/client/Cli.java +++ b/game/src/main/java/cz/jzitnik/client/Cli.java @@ -10,6 +10,7 @@ import cz.jzitnik.client.annotations.injectors.InjectState; import cz.jzitnik.client.events.KeyboardPressEvent; import cz.jzitnik.client.events.MouseAction; import cz.jzitnik.client.events.TerminalResizeEvent; +import cz.jzitnik.client.socket.SocketEventManager; import cz.jzitnik.client.states.RunningState; import cz.jzitnik.client.states.TerminalState; import cz.jzitnik.client.utils.events.EventManager; @@ -23,6 +24,9 @@ public class Cli implements Runnable { @InjectDependency private EventManager eventManager; + @InjectDependency + private SocketEventManager socketEventManager; + @InjectState private TerminalState terminalState; @@ -31,7 +35,9 @@ public class Cli implements Runnable { @Override public void run() { - eventManager.start(); // Start event manager thread + // Start event manager thread + eventManager.start(); + socketEventManager.start(); try (TerminalScreen terminal = new DefaultTerminalFactory() .setMouseCaptureMode(MouseCaptureMode.CLICK_RELEASE_DRAG_MOVE) diff --git a/game/src/main/java/cz/jzitnik/client/Game.java b/game/src/main/java/cz/jzitnik/client/Game.java index d6601e5..b7c5496 100644 --- a/game/src/main/java/cz/jzitnik/client/Game.java +++ b/game/src/main/java/cz/jzitnik/client/Game.java @@ -2,10 +2,12 @@ package cz.jzitnik.client; import cz.jzitnik.client.annotations.injectors.InjectDependency; import cz.jzitnik.client.game.setup.GameSetup; +import cz.jzitnik.client.socket.Client; import cz.jzitnik.client.utils.DependencyManager; import cz.jzitnik.client.utils.GlobalIOHandlerRepository; import cz.jzitnik.client.utils.ScheduledTaskManager; import cz.jzitnik.client.utils.ThreadManager; +import jakarta.websocket.DeploymentException; import org.reflections.Reflections; import java.io.IOException; @@ -23,15 +25,23 @@ public class Game { private ScheduledTaskManager scheduledTaskManager; @InjectDependency private GlobalIOHandlerRepository globalIOHandlerRepository; + @InjectDependency + private Client client; public void start() throws IOException { dependencyManager.inject(this); - gameSetup.setup(); - threadManager.startAll(); - scheduledTaskManager.startAll(); - globalIOHandlerRepository.setup(); + try { + client.connect(); - cli.run(); + gameSetup.setup(); + threadManager.startAll(); + scheduledTaskManager.startAll(); + globalIOHandlerRepository.setup(); + + cli.run(); + } catch (DeploymentException e) { + throw new RuntimeException(e); + } } } diff --git a/game/src/main/java/cz/jzitnik/client/annotations/SocketEventHandler.java b/game/src/main/java/cz/jzitnik/client/annotations/SocketEventHandler.java new file mode 100644 index 0000000..bf6fdd9 --- /dev/null +++ b/game/src/main/java/cz/jzitnik/client/annotations/SocketEventHandler.java @@ -0,0 +1,14 @@ +package cz.jzitnik.client.annotations; + +import cz.jzitnik.common.socket.SocketMessage; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface SocketEventHandler { + Class value(); +} diff --git a/game/src/main/java/cz/jzitnik/client/events/SendSocketMessageEvent.java b/game/src/main/java/cz/jzitnik/client/events/SendSocketMessageEvent.java new file mode 100644 index 0000000..291299a --- /dev/null +++ b/game/src/main/java/cz/jzitnik/client/events/SendSocketMessageEvent.java @@ -0,0 +1,7 @@ +package cz.jzitnik.client.events; + +import cz.jzitnik.client.utils.events.Event; +import cz.jzitnik.common.socket.SocketMessage; + +public record SendSocketMessageEvent(SocketMessage message) implements Event { +} diff --git a/game/src/main/java/cz/jzitnik/client/events/handlers/SendSocketMessageEventHandler.java b/game/src/main/java/cz/jzitnik/client/events/handlers/SendSocketMessageEventHandler.java new file mode 100644 index 0000000..d5be9c4 --- /dev/null +++ b/game/src/main/java/cz/jzitnik/client/events/handlers/SendSocketMessageEventHandler.java @@ -0,0 +1,24 @@ +package cz.jzitnik.client.events.handlers; + +import cz.jzitnik.client.annotations.EventHandler; +import cz.jzitnik.client.annotations.injectors.InjectDependency; +import cz.jzitnik.client.events.SendSocketMessageEvent; +import cz.jzitnik.client.socket.Client; +import cz.jzitnik.client.utils.events.AbstractEventHandler; + +import java.io.IOException; + +@EventHandler(SendSocketMessageEvent.class) +public class SendSocketMessageEventHandler extends AbstractEventHandler { + @InjectDependency + private Client client; + + @Override + public void handle(SendSocketMessageEvent event) { + try { + client.send(event.message()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/game/src/main/java/cz/jzitnik/client/socket/AbstractSocketEventHandler.java b/game/src/main/java/cz/jzitnik/client/socket/AbstractSocketEventHandler.java new file mode 100644 index 0000000..223136f --- /dev/null +++ b/game/src/main/java/cz/jzitnik/client/socket/AbstractSocketEventHandler.java @@ -0,0 +1,7 @@ +package cz.jzitnik.client.socket; + +import cz.jzitnik.common.socket.SocketMessage; + +public abstract class AbstractSocketEventHandler { + public abstract void handle(T event); +} diff --git a/game/src/main/java/cz/jzitnik/client/socket/Client.java b/game/src/main/java/cz/jzitnik/client/socket/Client.java new file mode 100644 index 0000000..36a16a9 --- /dev/null +++ b/game/src/main/java/cz/jzitnik/client/socket/Client.java @@ -0,0 +1,50 @@ +package cz.jzitnik.client.socket; + +import cz.jzitnik.client.annotations.Dependency; +import cz.jzitnik.client.annotations.injectors.InjectDependency; +import cz.jzitnik.common.socket.SocketMessage; +import jakarta.websocket.*; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.net.URI; +import java.nio.ByteBuffer; + +@Slf4j +@Dependency +@ClientEndpoint +public class Client { + private Session session; + + @InjectDependency + private SocketEventManager socketEventManager; + + @OnOpen + public void onOpen(Session session) { + this.session = session; + } + + @OnMessage + public void onMessage(ByteBuffer buffer) { + try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buffer.array()))) { + SocketMessage message = (SocketMessage) ois.readObject(); + socketEventManager.emitEvent(message); + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + } + + public void send(SocketMessage message) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(message); + oos.flush(); + + session.getBasicRemote().sendBinary(ByteBuffer.wrap(baos.toByteArray())); + } + + public void connect() throws DeploymentException, IOException { + WebSocketContainer container = ContainerProvider.getWebSocketContainer(); + container.connectToServer(this, URI.create("ws://localhost:8025/ws")); + } +} diff --git a/game/src/main/java/cz/jzitnik/client/socket/SocketEventManager.java b/game/src/main/java/cz/jzitnik/client/socket/SocketEventManager.java new file mode 100644 index 0000000..154802f --- /dev/null +++ b/game/src/main/java/cz/jzitnik/client/socket/SocketEventManager.java @@ -0,0 +1,94 @@ +package cz.jzitnik.client.socket; + +import cz.jzitnik.client.annotations.Dependency; +import cz.jzitnik.client.annotations.SocketEventHandler; +import cz.jzitnik.client.annotations.injectors.InjectConfig; +import cz.jzitnik.client.annotations.injectors.InjectState; +import cz.jzitnik.client.config.ThreadPoolConfig; +import cz.jzitnik.client.states.RunningState; +import cz.jzitnik.client.utils.DependencyManager; +import cz.jzitnik.common.socket.SocketMessage; +import lombok.extern.slf4j.Slf4j; +import org.reflections.Reflections; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; + +@Slf4j +@Dependency +public class SocketEventManager extends Thread { + private final DependencyManager dependencyManager; + + private ExecutorService eventExecutor; + private final HashMap, AbstractSocketEventHandler> handlers = new HashMap<>(); + private final BlockingQueue eventQueue = new LinkedBlockingQueue<>(); + + @InjectConfig + private ThreadPoolConfig threadPoolConfig; + + @InjectState + private RunningState runningState; + + public void emitEvent(SocketMessage event) { + eventQueue.add(event); + } + + public SocketEventManager(Reflections reflections, DependencyManager dependencyManager) { + this.dependencyManager = dependencyManager; + setDaemon(true); + + var classes = reflections.getTypesAnnotatedWith(SocketEventHandler.class); + + for (var clazz : classes) { + SocketEventHandler eventHandler = clazz.getAnnotation(SocketEventHandler.class); + try { + var instance = (AbstractSocketEventHandler) clazz.getDeclaredConstructor().newInstance(); + handlers.put(eventHandler.value(), instance); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + log.error("Failed to instantiate socket event handler: {}", clazz.getName(), e); + } + } + } + + @Override + public void run() { + for (Object instance : handlers.values()) { + dependencyManager.inject(instance); + } + + eventExecutor = Executors.newFixedThreadPool(threadPoolConfig.eventThreadCount()); + while (runningState.isRunning()) { + try { + SocketMessage event = eventQueue.take(); + handleEvent(event); + } catch (InterruptedException e) { + // The game is shutting down. + eventExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + eventExecutor.shutdown(); + } + + @SuppressWarnings("unchecked") + private AbstractSocketEventHandler getHandler(Class type) { + return (AbstractSocketEventHandler) handlers.get(type); + } + + @SuppressWarnings("unchecked") + private void handleEvent(SocketMessage event) { + eventExecutor.submit(() -> { + try { + AbstractSocketEventHandler handler = getHandler((Class) event.getClass()); + handler.handle(event); + } catch (Exception e) { + log.error("Error", e); + } + }); + } +} diff --git a/game/src/main/java/cz/jzitnik/client/socket/events/TestHandler.java b/game/src/main/java/cz/jzitnik/client/socket/events/TestHandler.java new file mode 100644 index 0000000..14e181d --- /dev/null +++ b/game/src/main/java/cz/jzitnik/client/socket/events/TestHandler.java @@ -0,0 +1,15 @@ +package cz.jzitnik.client.socket.events; + +import cz.jzitnik.client.annotations.SocketEventHandler; +import cz.jzitnik.client.socket.AbstractSocketEventHandler; +import cz.jzitnik.common.socket.messages.Test; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@SocketEventHandler(Test.class) +public class TestHandler extends AbstractSocketEventHandler { + @Override + public void handle(Test event) { + log.debug("Got test: {}", event); + } +} diff --git a/game/src/main/java/cz/jzitnik/client/utils/events/AbstractEventHandler.java b/game/src/main/java/cz/jzitnik/client/utils/events/AbstractEventHandler.java index 51dd639..e5924d3 100644 --- a/game/src/main/java/cz/jzitnik/client/utils/events/AbstractEventHandler.java +++ b/game/src/main/java/cz/jzitnik/client/utils/events/AbstractEventHandler.java @@ -1,5 +1,5 @@ package cz.jzitnik.client.utils.events; -public abstract class AbstractEventHandler { +public abstract class AbstractEventHandler { public abstract void handle(T event); } diff --git a/game/src/main/java/cz/jzitnik/client/utils/events/EventManager.java b/game/src/main/java/cz/jzitnik/client/utils/events/EventManager.java index d1a7b03..ddcf8d9 100644 --- a/game/src/main/java/cz/jzitnik/client/utils/events/EventManager.java +++ b/game/src/main/java/cz/jzitnik/client/utils/events/EventManager.java @@ -24,7 +24,7 @@ public class EventManager extends Thread { private ThreadPoolConfig threadPoolConfig; private ExecutorService eventExecutor; - private final HashMap, AbstractEventHandler> handlers = new HashMap<>(); + private final HashMap, AbstractEventHandler> handlers = new HashMap<>(); private final BlockingQueue eventQueue = new LinkedBlockingQueue<>(); private final DependencyManager dependencyManager; @@ -74,7 +74,7 @@ public class EventManager extends Thread { for (var clazz : classes) { EventHandler eventHandler = clazz.getAnnotation(EventHandler.class); try { - var instance = (AbstractEventHandler) clazz.getDeclaredConstructor().newInstance(); + var instance = (AbstractEventHandler) clazz.getDeclaredConstructor().newInstance(); handlers.put(eventHandler.value(), instance); } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { diff --git a/server/pom.xml b/server/pom.xml index b335cca..6b9d574 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -18,5 +18,49 @@ common 1.0-SNAPSHOT + + + org.projectlombok + lombok + 1.18.42 + provided + + + + jakarta.websocket + jakarta.websocket-api + 2.3.0-M2 + + + + org.glassfish.tyrus + tyrus-server + 2.2.2 + compile + + + + org.glassfish.tyrus + tyrus-container-grizzly-server + 2.2.2 + + + + org.glassfish.tyrus + tyrus-client + 2.2.2 + + + + org.slf4j + slf4j-api + 2.0.17 + + + + ch.qos.logback + logback-classic + 1.5.25 + diff --git a/server/src/main/java/cz/jzitnik/server/Main.java b/server/src/main/java/cz/jzitnik/server/Main.java index 64fd88d..a752d0a 100644 --- a/server/src/main/java/cz/jzitnik/server/Main.java +++ b/server/src/main/java/cz/jzitnik/server/Main.java @@ -1,7 +1,18 @@ package cz.jzitnik.server; -public class Main { - static void main() { +import jakarta.websocket.DeploymentException; +import org.glassfish.tyrus.server.Server; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +public class Main { + static void main() throws DeploymentException { + Map properties = new HashMap<>(); + Server server = new Server("localhost", 8025, "/", properties, WebSocket.class); + + server.start(); + new Scanner(System.in).nextLine(); } } diff --git a/server/src/main/java/cz/jzitnik/server/WebSocket.java b/server/src/main/java/cz/jzitnik/server/WebSocket.java new file mode 100644 index 0000000..5fa6558 --- /dev/null +++ b/server/src/main/java/cz/jzitnik/server/WebSocket.java @@ -0,0 +1,53 @@ +package cz.jzitnik.server; + +import cz.jzitnik.common.socket.SocketMessage; +import cz.jzitnik.common.socket.messages.Test; +import jakarta.websocket.*; +import jakarta.websocket.server.ServerEndpoint; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; + +@Slf4j +@ServerEndpoint("/ws") +public class WebSocket { + + @OnOpen + public void onOpen(Session session) { + log.debug("Client connected: " + session.getId()); + + try { + SocketMessage response = new Test(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(response); + oos.flush(); + session.getBasicRemote().sendBinary(java.nio.ByteBuffer.wrap(baos.toByteArray())); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // Receive binary data from client + @OnMessage + public void onMessage(byte[] bytes, Session session) { + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + + SocketMessage socketMessage = (SocketMessage) ois.readObject(); + System.out.println("Received: " + socketMessage); + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + } + + @OnClose + public void onClose(Session session, CloseReason reason) { + System.out.println("Connection closed: " + reason); + } + + @OnError + public void onError(Session session, Throwable throwable) { + throwable.printStackTrace(); + } +}