瀏覽代碼

Basic implementation of a multiplayer experience, added Networking, Packets and Events

jan 1 月之前
父節點
當前提交
5b144506a5
共有 29 個文件被更改,包括 521 次插入83 次删除
  1. 1 1
      core/build.gradle
  2. 5 4
      core/src/main/java/me/lethunderhawk/Main.java
  3. 53 0
      core/src/main/java/me/lethunderhawk/network/ClientPacketListener.java
  4. 19 19
      core/src/main/java/me/lethunderhawk/network/GameClient.java
  5. 0 32
      core/src/main/java/me/lethunderhawk/network/Packets.java
  6. 29 10
      core/src/main/java/me/lethunderhawk/screen/GameScreen.java
  7. 4 1
      core/src/main/java/me/lethunderhawk/screen/MenuScreen.java
  8. 1 2
      server/build.gradle
  9. 49 0
      server/src/main/java/me/lethunderhawk/server/GameListener.java
  10. 14 9
      server/src/main/java/me/lethunderhawk/server/GameServer.java
  11. 2 1
      shared/build.gradle
  12. 0 1
      shared/src/main/java/me/lethunderhawk/entity/Player.java
  13. 4 0
      shared/src/main/java/me/lethunderhawk/event/Event.java
  14. 93 0
      shared/src/main/java/me/lethunderhawk/event/EventDispatcher.java
  15. 11 0
      shared/src/main/java/me/lethunderhawk/event/EventHandler.java
  16. 4 0
      shared/src/main/java/me/lethunderhawk/event/EventListener.java
  17. 11 0
      shared/src/main/java/me/lethunderhawk/event/ExampleEventListener.java
  18. 35 0
      shared/src/main/java/me/lethunderhawk/event/impl/PlayerMoveEvent.java
  19. 0 3
      shared/src/main/java/me/lethunderhawk/network/NetworkRegister.java
  20. 4 0
      shared/src/main/java/me/lethunderhawk/network/packet/Packet.java
  21. 79 0
      shared/src/main/java/me/lethunderhawk/network/packet/PacketDispatcher.java
  22. 12 0
      shared/src/main/java/me/lethunderhawk/network/packet/PacketHandler.java
  23. 4 0
      shared/src/main/java/me/lethunderhawk/network/packet/PacketListener.java
  24. 33 0
      shared/src/main/java/me/lethunderhawk/network/packet/PacketRegistry.java
  25. 9 0
      shared/src/main/java/me/lethunderhawk/network/packet/impl/PlayerLoginPacket.java
  26. 9 0
      shared/src/main/java/me/lethunderhawk/network/packet/impl/PlayerLoginSuccessPacket.java
  27. 8 0
      shared/src/main/java/me/lethunderhawk/network/packet/impl/PlayerLogoutPacket.java
  28. 10 0
      shared/src/main/java/me/lethunderhawk/network/packet/impl/PlayerMovePacket.java
  29. 18 0
      shared/src/main/java/me/lethunderhawk/network/packet/impl/PlayerPositionPacket.java

+ 1 - 1
core/build.gradle

@@ -2,12 +2,12 @@
 eclipse.project.name = appName + '-core'
 
 dependencies {
-  implementation "com.esotericsoftware:kryonet:2.22.0-RC1"
   api "com.badlogicgames.ashley:ashley:$ashleyVersion"
   api "com.badlogicgames.box2dlights:box2dlights:$box2dlightsVersion"
   api "com.badlogicgames.gdx:gdx-ai:$aiVersion"
   api "com.badlogicgames.gdx:gdx-box2d:$gdxVersion"
   api "com.badlogicgames.gdx:gdx:$gdxVersion"
+  implementation project(":shared")
   api project(':shared')
 
   if(enableGraalNative == 'true') {

+ 5 - 4
core/src/main/java/me/lethunderhawk/Main.java

@@ -1,15 +1,16 @@
 package me.lethunderhawk;
 
-import com.badlogic.gdx.ApplicationListener;
 import com.badlogic.gdx.Game;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.InputAdapter;
+import me.lethunderhawk.event.EventDispatcher;
+import me.lethunderhawk.event.ExampleEventListener;
+import me.lethunderhawk.network.packet.PacketRegistry;
 import me.lethunderhawk.screen.MenuScreen;
 
-/** {@link com.badlogic.gdx.ApplicationListener} implementation shared by all platforms. Listens to user input. */
 public class Main extends Game {
     @Override
     public void create() {
         setScreen(new MenuScreen(this));
+        EventDispatcher.addListener(new ExampleEventListener());
     }
+
 }

+ 53 - 0
core/src/main/java/me/lethunderhawk/network/ClientPacketListener.java

@@ -0,0 +1,53 @@
+package me.lethunderhawk.network;
+
+import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.network.packet.PacketHandler;
+import me.lethunderhawk.network.packet.PacketListener;
+import me.lethunderhawk.network.packet.impl.PlayerLoginPacket;
+import me.lethunderhawk.network.packet.impl.PlayerLogoutPacket;
+import me.lethunderhawk.network.packet.impl.PlayerMovePacket;
+import me.lethunderhawk.network.packet.impl.PlayerPositionPacket;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class ClientPacketListener implements PacketListener {
+    private final Map<UUID, Player> otherPlayers = new ConcurrentHashMap<>();
+
+    @PacketHandler
+    private void playerLogin(PlayerLoginPacket packet) {
+        otherPlayers.put(packet.player.uuid, packet.player);
+    }
+
+    @PacketHandler
+    private void playerLogout(PlayerLogoutPacket packet) {
+        otherPlayers.remove(packet.player.uuid);
+    }
+
+    @PacketHandler
+    private void handlePlayerPosition(PlayerPositionPacket packet) {
+        Player player = otherPlayers.computeIfAbsent(packet.player.uuid, uuid -> {
+            Player p = new Player();
+            p.uuid = uuid;
+            return p;
+        });
+        player.x = packet.x;
+        player.y = packet.y;
+    }
+
+    @PacketHandler
+    private void handlePlayerMove(PlayerMovePacket packet) {
+        Player player = otherPlayers.computeIfAbsent(packet.player.uuid, uuid -> {
+            Player p = new Player();
+            p.uuid = uuid;
+            return p;
+        });
+        player.x = packet.x;
+        player.y = packet.y;
+    }
+
+    public Map<UUID, Player> getOtherPlayers() {
+        return otherPlayers;
+    }
+}

+ 19 - 19
core/src/main/java/me/lethunderhawk/network/GameClient.java

@@ -4,28 +4,34 @@ import com.esotericsoftware.kryonet.Client;
 import com.esotericsoftware.kryonet.Connection;
 import com.esotericsoftware.kryonet.Listener;
 import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.network.packet.*;
+import me.lethunderhawk.network.packet.impl.PlayerLoginPacket;
+import me.lethunderhawk.network.packet.impl.PlayerMovePacket;
 
 import java.io.IOException;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
-public class GameClient extends Listener {
+public class GameClient extends Listener implements EventListener {
 
     private final Client client;
-    private final Map<UUID, Player> otherPlayers = new ConcurrentHashMap<>();
+
     private final Player player;
+    private final ClientPacketListener clientPacketListener;
 
     public GameClient() {
         this.player = new Player();
         client = new Client();
 
         client.start();
+        this.clientPacketListener = new ClientPacketListener();
+        PacketDispatcher.registerListener(clientPacketListener);
 
         registerPackets();
     }
 
     private void registerPackets() {
-        NetworkRegister.register(client.getKryo());
+        PacketRegistry.register(client.getKryo());
     }
 
     public void connect(String ip) throws IOException {
@@ -37,32 +43,26 @@ public class GameClient extends Listener {
             54777
         );
         client.addListener(this);
+        PlayerLoginPacket loginPacket = new PlayerLoginPacket();
+        loginPacket.player = player;
+        send(loginPacket);
     }
 
     public void send(Object packet) {
-
         client.sendUDP(packet);
     }
 
     @Override
     public void received (Connection connection, Object object) {
-        if(object instanceof Packets.PlayerLoginPacket packet) {
-            otherPlayers.put(packet.player.uuid, packet.player);
-        }
-
-        if(object instanceof Packets.PlayerPositionPacket packet) {
-            handlePlayerPosition(packet);
+        if(object instanceof Packet packet){
+            PacketDispatcher.dispatchPacket(packet, connection);
         }
     }
 
-    private void handlePlayerPosition(Packets.PlayerPositionPacket packet) {
-        Player player = otherPlayers.computeIfAbsent(packet.player.uuid, uuid -> {
-            Player p = new Player();
-            p.uuid = uuid;
-            return p;
-        });
-        player.x = packet.x;
-        player.y = packet.y;
+
+
+    public void disconnect(){
+        client.stop();
     }
 
     public Player getPlayer() {
@@ -70,6 +70,6 @@ public class GameClient extends Listener {
     }
 
     public Map<UUID, Player> getOtherPlayers() {
-        return otherPlayers;
+        return clientPacketListener.getOtherPlayers();
     }
 }

+ 0 - 32
core/src/main/java/me/lethunderhawk/network/Packets.java

@@ -1,32 +0,0 @@
-package me.lethunderhawk.network;
-
-import me.lethunderhawk.entity.Player;
-
-import java.awt.*;
-
-public class Packets {
-
-    /*public abstract static class Packet{
-        abstract void onReceive();
-
-    }*/
-
-    public static class PlayerPositionPacket {
-        public Player player;
-        public float x;
-        public float y;
-
-        public PlayerPositionPacket() {}
-
-        public PlayerPositionPacket(Player player, float x, float y) {
-            this.player = player;
-            this.x = x;
-            this.y = y;
-        }
-    }
-
-    public static class PlayerLoginPacket {
-        public Player player;
-
-    }
-}

+ 29 - 10
core/src/main/java/me/lethunderhawk/screen/GameScreen.java

@@ -7,22 +7,26 @@ import com.badlogic.gdx.graphics.GL20;
 import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
 import me.lethunderhawk.Main;
 import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.event.EventDispatcher;
+import me.lethunderhawk.event.EventListener;
+import me.lethunderhawk.event.impl.PlayerMoveEvent;
 import me.lethunderhawk.network.GameClient;
-import me.lethunderhawk.network.Packets;
+import me.lethunderhawk.network.packet.PacketRegistry;
+import me.lethunderhawk.network.packet.impl.PlayerLogoutPacket;
+import me.lethunderhawk.network.packet.impl.PlayerPositionPacket;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-public class GameScreen implements Screen {
+public class GameScreen implements Screen, EventListener {
 
     private final ShapeRenderer shapeRenderer;
     private final GameClient client;
+    private final Main mainGame;
 
     public GameScreen(Main game, String ip) {
-
+        this.mainGame = game;
         shapeRenderer = new ShapeRenderer();
 
         client = new GameClient();
@@ -59,7 +63,9 @@ public class GameScreen implements Screen {
 
         boolean moved = false;
         Player currentPlayer = client.getPlayer();
-
+        if(Gdx.input.isKeyPressed(Input.Keys.ESCAPE)) {
+            mainGame.setScreen(new MenuScreen(mainGame));
+        }
         if(Gdx.input.isKeyPressed(Input.Keys.W)) {
             currentPlayer.y += speed * delta;
             moved = true;
@@ -82,8 +88,10 @@ public class GameScreen implements Screen {
 
         if(moved) {
 
-            Packets.PlayerPositionPacket packet =
-                new Packets.PlayerPositionPacket(currentPlayer, currentPlayer.x, currentPlayer.y);
+            PlayerPositionPacket packet =
+                new PlayerPositionPacket(currentPlayer, currentPlayer.x, currentPlayer.y);
+            PlayerMoveEvent event = new PlayerMoveEvent(currentPlayer, currentPlayer.x, currentPlayer.y);
+            EventDispatcher.getInstance().fire(event);
 
             client.send(packet);
         }
@@ -99,7 +107,18 @@ public class GameScreen implements Screen {
     @Override
     public void resume() {}
     @Override
-    public void hide() {}
+    public void hide() {
+        logout();
+    }
     @Override
-    public void dispose() {}
+    public void dispose() {
+        logout();
+    }
+
+    private void logout(){
+        PlayerLogoutPacket packet = new PlayerLogoutPacket();
+        packet.player = client.getPlayer();
+        client.send(packet);
+        client.disconnect();
+    }
 }

+ 4 - 1
core/src/main/java/me/lethunderhawk/screen/MenuScreen.java

@@ -15,6 +15,7 @@ import com.badlogic.gdx.utils.viewport.ScreenViewport;
 import com.badlogic.gdx.InputMultiplexer;
 import com.badlogic.gdx.scenes.scene2d.InputEvent;
 import me.lethunderhawk.Main;
+import me.lethunderhawk.event.EventDispatcher;
 
 public class MenuScreen implements Screen {
 
@@ -67,7 +68,9 @@ public class MenuScreen implements Screen {
                 if (ip == null || ip.trim().isEmpty()) {
                     ip = "localhost";
                 }
-                game.setScreen(new GameScreen(game, ip));
+                GameScreen gameScreen = new GameScreen(game, ip);
+                game.setScreen(gameScreen);
+                EventDispatcher.addListener(gameScreen);
             }
         });
 

+ 1 - 2
server/build.gradle

@@ -13,8 +13,7 @@ eclipse.project.name = appName + '-server'
 
 dependencies {
   implementation project(':shared')
-  implementation project(":core")
-  implementation "com.esotericsoftware:kryonet:2.22.0-RC1"
+  api project(':shared')
 }
 
 jar {

+ 49 - 0
server/src/main/java/me/lethunderhawk/server/GameListener.java

@@ -0,0 +1,49 @@
+package me.lethunderhawk.server;
+
+import com.esotericsoftware.kryonet.Connection;
+import com.esotericsoftware.kryonet.Server;
+import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.network.packet.PacketHandler;
+import me.lethunderhawk.network.packet.PacketListener;
+import me.lethunderhawk.network.packet.impl.PlayerLoginPacket;
+import me.lethunderhawk.network.packet.impl.PlayerLoginSuccessPacket;
+import me.lethunderhawk.network.packet.impl.PlayerLogoutPacket;
+import me.lethunderhawk.network.packet.impl.PlayerPositionPacket;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class GameListener implements PacketListener {
+    private final Map<Integer, Player> players = new HashMap<>();
+    public Server server;
+
+    public GameListener(Server server) {
+        this.server = server;
+    }
+    @PacketHandler
+    public void onPlayerPosition(PlayerPositionPacket packet, Connection connection) {
+        server.sendToAllExceptUDP(connection.getID(), packet);
+        Player p = players.get(connection.getID());
+        p.x = packet.x;
+        p.y = packet.y;
+    }
+
+    @PacketHandler
+    public void onLogout(PlayerLogoutPacket packet, Connection connection){
+        players.remove(connection.getID());
+        server.sendToAllExceptUDP(connection.getID(), packet);
+    }
+
+    @PacketHandler
+    public void onLogin(PlayerLoginPacket packet, Connection connection){
+        players.putIfAbsent(connection.getID(),  packet.player);
+
+        server.sendToAllExceptUDP(connection.getID(), packet);
+        server.sendToUDP(connection.getID(), new PlayerLoginSuccessPacket());
+
+        for(Player player : players.values()){
+            if(player.equals(packet.player)) return;
+            server.sendToUDP(connection.getID(), new PlayerPositionPacket(player, player.x, player.y));
+        }
+    }
+}

+ 14 - 9
server/src/main/java/me/lethunderhawk/server/GameServer.java

@@ -3,40 +3,45 @@ package me.lethunderhawk.server;
 import com.esotericsoftware.kryonet.Connection;
 import com.esotericsoftware.kryonet.Listener;
 import com.esotericsoftware.kryonet.Server;
+import me.lethunderhawk.entity.Player;
 import me.lethunderhawk.network.NetworkRegister;
-import me.lethunderhawk.network.Packets;
+import me.lethunderhawk.network.packet.*;
+import me.lethunderhawk.network.packet.impl.PlayerLoginPacket;
+import me.lethunderhawk.network.packet.impl.PlayerLoginSuccessPacket;
+import me.lethunderhawk.network.packet.impl.PlayerLogoutPacket;
+import me.lethunderhawk.network.packet.impl.PlayerPositionPacket;
 
 import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
 
 public class GameServer {
 
     private final Server server;
 
+
     public GameServer() throws IOException {
 
         server = new Server();
 
         registerPackets();
+        PacketDispatcher.registerListener(new GameListener(server));
 
         server.start();
 
         server.bind(54555, 54777);
-
         server.addListener(new Listener() {
-
             @Override
             public void received(Connection connection, Object object) {
-                if(object instanceof Packets.PlayerLoginPacket packet) {
-                    server.sendToAllExceptUDP(connection.getID(), packet);
-                }
-                if(object instanceof Packets.PlayerPositionPacket packet) {
-                    server.sendToAllExceptUDP(connection.getID(), packet);
+                if(object instanceof Packet packet) {
+                    PacketDispatcher.dispatchPacket(packet, connection);
                 }
             }
         });
     }
 
+
     private void registerPackets() {
-        NetworkRegister.register(server.getKryo());
+        PacketRegistry.register(server.getKryo());
     }
 }

+ 2 - 1
shared/build.gradle

@@ -1,5 +1,6 @@
 eclipse.project.name = appName + '-shared'
 
 dependencies {
-
+  implementation "com.esotericsoftware:kryonet:2.22.0-RC1"
+  api "com.esotericsoftware:kryonet:2.22.0-RC1"
 }

+ 0 - 1
core/src/main/java/me/lethunderhawk/entity/Player.java → shared/src/main/java/me/lethunderhawk/entity/Player.java

@@ -10,5 +10,4 @@ public class Player {
     public Player(){
         this.uuid = UUID.randomUUID();
     }
-
 }

+ 4 - 0
shared/src/main/java/me/lethunderhawk/event/Event.java

@@ -0,0 +1,4 @@
+package me.lethunderhawk.event;
+
+public abstract class Event {
+}

+ 93 - 0
shared/src/main/java/me/lethunderhawk/event/EventDispatcher.java

@@ -0,0 +1,93 @@
+package me.lethunderhawk.event;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class EventDispatcher {
+    private EventDispatcher() {}
+
+    private static EventDispatcher instance;
+
+    public static EventDispatcher getInstance(){
+        if(instance == null) instance = new EventDispatcher();
+        return instance;
+    }
+
+    private final Map<Class<? extends Event>, List<RegisteredListener>>
+        listeners = new ConcurrentHashMap<>();
+
+    public void registerListener(EventListener eventListener) {
+
+        for(Method method : eventListener.getClass().getDeclaredMethods()) {
+
+            if(!method.isAnnotationPresent(EventHandler.class))
+                continue;
+
+            Class<?>[] parameters = method.getParameterTypes();
+
+            if(parameters.length != 1)
+                continue;
+
+            Class<?> param = parameters[0];
+
+            if(!Event.class.isAssignableFrom(param))
+                continue;
+
+            method.setAccessible(true);
+
+            RegisteredListener registered =
+                new RegisteredListener(eventListener, method);
+
+            listeners
+                .computeIfAbsent(
+                    (Class<? extends Event>) param,
+                    c -> new CopyOnWriteArrayList<>()
+                )
+                .add(registered);
+        }
+    }
+
+    public void fire(Event event) {
+
+        List<RegisteredListener> eventListeners =
+            listeners.get(event.getClass());
+
+        if(eventListeners == null)
+            return;
+
+        for(RegisteredListener listener : eventListeners) {
+
+            try {
+
+                listener.method.invoke(
+                    listener.eventListener,
+                    event
+                );
+
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    private static class RegisteredListener {
+
+        private final EventListener eventListener;
+        private final Method method;
+
+        public RegisteredListener(
+            EventListener eventListener,
+            Method method
+        ) {
+            this.eventListener = eventListener;
+            this.method = method;
+        }
+    }
+
+    public static void addListener(EventListener eventListener) {
+        EventDispatcher.getInstance().registerListener(eventListener);
+    }
+}

+ 11 - 0
shared/src/main/java/me/lethunderhawk/event/EventHandler.java

@@ -0,0 +1,11 @@
+package me.lethunderhawk.event;
+
+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.METHOD)
+public @interface EventHandler {
+}

+ 4 - 0
shared/src/main/java/me/lethunderhawk/event/EventListener.java

@@ -0,0 +1,4 @@
+package me.lethunderhawk.event;
+
+public interface EventListener {
+}

+ 11 - 0
shared/src/main/java/me/lethunderhawk/event/ExampleEventListener.java

@@ -0,0 +1,11 @@
+package me.lethunderhawk.event;
+
+import me.lethunderhawk.event.impl.PlayerMoveEvent;
+
+public class ExampleEventListener implements EventListener {
+
+    @EventHandler
+    public void irgendeinname(PlayerMoveEvent event) {
+
+    }
+}

+ 35 - 0
shared/src/main/java/me/lethunderhawk/event/impl/PlayerMoveEvent.java

@@ -0,0 +1,35 @@
+package me.lethunderhawk.event.impl;
+
+import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.event.Event;
+
+public class PlayerMoveEvent extends Event {
+
+    private final Player player;
+
+    private final float x;
+    private final float y;
+
+    public PlayerMoveEvent(
+        Player player,
+        float x,
+        float y
+    ) {
+
+        this.player = player;
+        this.x = x;
+        this.y = y;
+    }
+
+    public Player getPlayer() {
+        return player;
+    }
+
+    public float getX() {
+        return x;
+    }
+
+    public float getY() {
+        return y;
+    }
+}

+ 0 - 3
core/src/main/java/me/lethunderhawk/network/NetworkRegister.java → shared/src/main/java/me/lethunderhawk/network/NetworkRegister.java

@@ -4,7 +4,6 @@ import com.esotericsoftware.kryo.Kryo;
 import com.esotericsoftware.kryo.Serializer;
 import com.esotericsoftware.kryo.io.Input;
 import com.esotericsoftware.kryo.io.Output;
-import com.esotericsoftware.kryo.serializers.DefaultSerializers;
 import me.lethunderhawk.entity.Player;
 
 import java.util.UUID;
@@ -12,8 +11,6 @@ import java.util.UUID;
 public class NetworkRegister {
 
     public static void register(Kryo kryo) {
-        kryo.register(Packets.PlayerPositionPacket.class);
-        kryo.register(Packets.PlayerLoginPacket.class);
         kryo.register(Player.class);
         kryo.register(UUID.class, new Serializer<UUID>() {
             public void write(Kryo kryo, Output out, UUID uuid) {

+ 4 - 0
shared/src/main/java/me/lethunderhawk/network/packet/Packet.java

@@ -0,0 +1,4 @@
+package me.lethunderhawk.network.packet;
+
+public interface Packet {
+}

+ 79 - 0
shared/src/main/java/me/lethunderhawk/network/packet/PacketDispatcher.java

@@ -0,0 +1,79 @@
+
+package me.lethunderhawk.network.packet;
+
+import com.esotericsoftware.kryonet.Connection;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class PacketDispatcher {
+
+    private static final Map<Class<? extends Packet>, List<RegisteredListener>> listeners = new ConcurrentHashMap<>();
+
+    public static void registerListener(PacketListener listener) {
+        for (Method method : listener.getClass().getDeclaredMethods()) {
+            if (!method.isAnnotationPresent(PacketHandler.class)) {
+                continue;
+            }
+
+            Class<?>[] params = method.getParameterTypes();
+            if (params.length != 1 && params.length != 2) {
+                continue;
+            }
+
+            if (!Packet.class.isAssignableFrom(params[0])) {
+                continue;
+            }
+            if (params.length == 2 && !Connection.class.isAssignableFrom(params[1])) {
+                continue;
+            }
+
+            method.setAccessible(true);
+
+            listeners.computeIfAbsent((Class<? extends Packet>) params[0], c -> new CopyOnWriteArrayList<>())
+                .add(new RegisteredListener(listener, method, params.length));
+        }
+    }
+
+    public static void dispatchPacket(Packet packet, Connection connection) {
+        List<RegisteredListener> packetListeners = listeners.get(packet.getClass());
+        if (packetListeners == null || packetListeners.isEmpty()) {
+            return;
+        }
+
+        for (RegisteredListener entry : packetListeners) {
+            try {
+                if (entry.argCount == 2) {
+                    entry.method.invoke(entry.listener, packet, connection);
+                } else {
+                    entry.method.invoke(entry.listener, packet);
+                }
+            } catch (InvocationTargetException e) {
+                // Unwrap the actual listener exception for clearer stack traces
+                throw new RuntimeException("Packet listener failed for " + packet.getClass().getSimpleName(),
+                    e.getCause() != null ? e.getCause() : e);
+            } catch (Exception e) {
+                throw new RuntimeException("Failed to invoke listener for " + packet.getClass().getSimpleName(), e);
+            }
+        }
+    }
+
+    public static void dispatchPacket(Packet packet) {
+        dispatchPacket(packet, null);
+    }
+
+    private static class RegisteredListener {
+        private final PacketListener listener;
+        private final Method method;
+        private final int argCount;
+
+        public RegisteredListener(PacketListener listener, Method method, int argCount) {
+            this.listener = listener;
+            this.method = method;
+            this.argCount = argCount;
+        }
+    }
+}

+ 12 - 0
shared/src/main/java/me/lethunderhawk/network/packet/PacketHandler.java

@@ -0,0 +1,12 @@
+package me.lethunderhawk.network.packet;
+
+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.METHOD)
+public @interface PacketHandler {
+}
+

+ 4 - 0
shared/src/main/java/me/lethunderhawk/network/packet/PacketListener.java

@@ -0,0 +1,4 @@
+package me.lethunderhawk.network.packet;
+
+public interface PacketListener {
+}

+ 33 - 0
shared/src/main/java/me/lethunderhawk/network/packet/PacketRegistry.java

@@ -0,0 +1,33 @@
+package me.lethunderhawk.network.packet;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.Serializer;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.io.Output;
+import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.event.impl.PlayerMoveEvent;
+import me.lethunderhawk.network.packet.impl.*;
+
+import java.util.UUID;
+
+public class PacketRegistry {
+
+    public static void register(Kryo kryo) {
+        kryo.register(Player.class);
+        kryo.register(PlayerLoginPacket.class);
+        kryo.register(PlayerLoginSuccessPacket.class);
+        kryo.register(PlayerLogoutPacket.class);
+        kryo.register(PlayerMovePacket.class);
+        kryo.register(PlayerPositionPacket.class);
+        kryo.register(UUID.class, new Serializer<UUID>() {
+            public void write(Kryo kryo, Output out, UUID uuid) {
+                out.writeLong(uuid.getMostSignificantBits());
+                out.writeLong(uuid.getLeastSignificantBits());
+            }
+
+            public UUID read(Kryo kryo, Input in, Class<UUID> type) {
+                return new UUID(in.readLong(), in.readLong());
+            }
+        });
+    }
+}

+ 9 - 0
shared/src/main/java/me/lethunderhawk/network/packet/impl/PlayerLoginPacket.java

@@ -0,0 +1,9 @@
+package me.lethunderhawk.network.packet.impl;
+
+import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.network.packet.Packet;
+
+public class PlayerLoginPacket implements Packet {
+    public Player player;
+
+}

+ 9 - 0
shared/src/main/java/me/lethunderhawk/network/packet/impl/PlayerLoginSuccessPacket.java

@@ -0,0 +1,9 @@
+package me.lethunderhawk.network.packet.impl;
+
+import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.network.packet.Packet;
+
+public class PlayerLoginSuccessPacket implements Packet {
+    public Player player;
+
+}

+ 8 - 0
shared/src/main/java/me/lethunderhawk/network/packet/impl/PlayerLogoutPacket.java

@@ -0,0 +1,8 @@
+package me.lethunderhawk.network.packet.impl;
+
+import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.network.packet.Packet;
+
+public class PlayerLogoutPacket implements Packet {
+    public Player player;
+}

+ 10 - 0
shared/src/main/java/me/lethunderhawk/network/packet/impl/PlayerMovePacket.java

@@ -0,0 +1,10 @@
+package me.lethunderhawk.network.packet.impl;
+
+import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.network.packet.Packet;
+
+public class PlayerMovePacket implements Packet {
+    public Player player;
+    public float x;
+    public float y;
+}

+ 18 - 0
shared/src/main/java/me/lethunderhawk/network/packet/impl/PlayerPositionPacket.java

@@ -0,0 +1,18 @@
+package me.lethunderhawk.network.packet.impl;
+
+import me.lethunderhawk.entity.Player;
+import me.lethunderhawk.network.packet.Packet;
+
+public class PlayerPositionPacket implements Packet {
+    public Player player;
+    public float x;
+    public float y;
+
+    public PlayerPositionPacket() {}
+
+    public PlayerPositionPacket(Player player, float x, float y) {
+        this.player = player;
+        this.x = x;
+        this.y = y;
+    }
+}