From b75733fd2db99cbc571fcc210fe390273c080fc1 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 11 Apr 2026 08:14:52 +0700 Subject: [PATCH] refactor(server): migrate event handlers to typed records Replace every UnsupportedOperationException stub in RequestDispatcher with a real handler call. All 15 handlers now live under com.miti99.caro.server.event.handler and take a typed ClientRequest record, emitting typed Response protos via ChannelUtils.push. Handlers ported: - SetClientInfoHandler, SetNicknameHandler - CreateRoomHandler, CreatePveRoomHandler, GetRoomsHandler, JoinRoomHandler - GameStartingHandler, GameReadyHandler - GameMoveHandler (full PVP + PVE AI + game-over broadcast) - GameResetHandler (noop, never wired before) - WatchGameHandler, WatchGameExitHandler - ClientExitHandler, ClientOfflineHandler WebsocketTransferHandler.clientOfflineEvent now dispatches to ClientOfflineHandler. RoomClearTask reuses ClientExitHandler for stale-room cleanup. All 37 unit tests pass. --- .../caro/server/event/RequestDispatcher.java | 46 +++--- .../event/handler/ClientExitHandler.java | 46 ++++++ .../event/handler/ClientOfflineHandler.java | 52 +++++++ .../event/handler/CreatePveRoomHandler.java | 66 ++++++++ .../event/handler/CreateRoomHandler.java | 43 ++++++ .../server/event/handler/GameMoveHandler.java | 146 ++++++++++++++++++ .../event/handler/GameReadyHandler.java | 57 +++++++ .../event/handler/GameResetHandler.java | 19 +++ .../event/handler/GameStartingHandler.java | 70 +++++++++ .../server/event/handler/GetRoomsHandler.java | 31 ++++ .../server/event/handler/JoinRoomHandler.java | 78 ++++++++++ .../event/handler/SetClientInfoHandler.java | 21 +++ .../event/handler/SetNicknameHandler.java | 31 ++++ .../event/handler/WatchGameExitHandler.java | 24 +++ .../event/handler/WatchGameHandler.java | 37 +++++ .../handler/WebsocketTransferHandler.java | 3 +- .../caro/server/timer/RoomClearTask.java | 11 +- 17 files changed, 758 insertions(+), 23 deletions(-) create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/ClientExitHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/ClientOfflineHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/CreatePveRoomHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/CreateRoomHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/GameMoveHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/GameReadyHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/GameResetHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/GameStartingHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/GetRoomsHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/JoinRoomHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/SetClientInfoHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/SetNicknameHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/WatchGameExitHandler.java create mode 100644 server/src/main/java/com/miti99/caro/server/event/handler/WatchGameHandler.java diff --git a/server/src/main/java/com/miti99/caro/server/event/RequestDispatcher.java b/server/src/main/java/com/miti99/caro/server/event/RequestDispatcher.java index ff2ab62..0bb7cab 100644 --- a/server/src/main/java/com/miti99/caro/server/event/RequestDispatcher.java +++ b/server/src/main/java/com/miti99/caro/server/event/RequestDispatcher.java @@ -1,6 +1,19 @@ package com.miti99.caro.server.event; import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.server.event.handler.ClientExitHandler; +import com.miti99.caro.server.event.handler.CreatePveRoomHandler; +import com.miti99.caro.server.event.handler.CreateRoomHandler; +import com.miti99.caro.server.event.handler.GameMoveHandler; +import com.miti99.caro.server.event.handler.GameReadyHandler; +import com.miti99.caro.server.event.handler.GameResetHandler; +import com.miti99.caro.server.event.handler.GameStartingHandler; +import com.miti99.caro.server.event.handler.GetRoomsHandler; +import com.miti99.caro.server.event.handler.JoinRoomHandler; +import com.miti99.caro.server.event.handler.SetClientInfoHandler; +import com.miti99.caro.server.event.handler.SetNicknameHandler; +import com.miti99.caro.server.event.handler.WatchGameExitHandler; +import com.miti99.caro.server.event.handler.WatchGameHandler; import com.miti99.caro.server.event.request.ClientExitRequestRecord; import com.miti99.caro.server.event.request.ClientRequest; import com.miti99.caro.server.event.request.CreatePveRoomRequestRecord; @@ -20,9 +33,6 @@ import com.miti99.caro.server.event.request.WatchGameRequestRecord; /** * Routes a {@link ClientRequest} record to its handler via exhaustive pattern * matching. Replaces the old reflection-based {@code ServerEventListener} lookup. - * - *

Phase 02a: only {@code HeartbeatRequestRecord} has a real no-op implementation; - * every other case throws {@link UnsupportedOperationException} pending Phase 02b. */ public final class RequestDispatcher { @@ -34,23 +44,19 @@ public final class RequestDispatcher { case HeartbeatRequestRecord r -> { /* no-op: heartbeat just refreshes IdleStateHandler */ } - case SetNicknameRequestRecord r -> throw todo("set_nickname"); - case SetClientInfoRequestRecord r -> throw todo("set_client_info"); - case CreateRoomRequestRecord r -> throw todo("create_room"); - case CreatePveRoomRequestRecord r -> throw todo("create_pve_room"); - case GetRoomsRequestRecord r -> throw todo("get_rooms"); - case JoinRoomRequestRecord r -> throw todo("join_room"); - case GameStartingRequestRecord r -> throw todo("game_starting"); - case GameReadyRequestRecord r -> throw todo("game_ready"); - case GameMoveRequestRecord r -> throw todo("game_move"); - case GameResetRequestRecord r -> throw todo("game_reset"); - case WatchGameRequestRecord r -> throw todo("watch_game"); - case WatchGameExitRequestRecord r -> throw todo("watch_game_exit"); - case ClientExitRequestRecord r -> throw todo("client_exit"); + case SetNicknameRequestRecord r -> SetNicknameHandler.handle(client, r); + case SetClientInfoRequestRecord r -> SetClientInfoHandler.handle(client, r); + case CreateRoomRequestRecord r -> CreateRoomHandler.handle(client, r); + case CreatePveRoomRequestRecord r -> CreatePveRoomHandler.handle(client, r); + case GetRoomsRequestRecord r -> GetRoomsHandler.handle(client, r); + case JoinRoomRequestRecord r -> JoinRoomHandler.handle(client, r); + case GameStartingRequestRecord r -> GameStartingHandler.handle(client, r); + case GameReadyRequestRecord r -> GameReadyHandler.handle(client, r); + case GameMoveRequestRecord r -> GameMoveHandler.handle(client, r); + case GameResetRequestRecord r -> GameResetHandler.handle(client, r); + case WatchGameRequestRecord r -> WatchGameHandler.handle(client, r); + case WatchGameExitRequestRecord r -> WatchGameExitHandler.handle(client, r); + case ClientExitRequestRecord r -> ClientExitHandler.handle(client, r); } } - - private static UnsupportedOperationException todo(String name) { - return new UnsupportedOperationException("TODO phase 02b: " + name); - } } diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/ClientExitHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/ClientExitHandler.java new file mode 100644 index 0000000..e0e4e3a --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/ClientExitHandler.java @@ -0,0 +1,46 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.protocol.ClientExitResponse; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.ClientExitRequestRecord; + +public final class ClientExitHandler { + + private static final Object LOCK = new Object(); + + private ClientExitHandler() { + } + + public static void handle(ClientSide clientSide, ClientExitRequestRecord req) { + synchronized (LOCK) { + Room room = ServerContains.getRoom(clientSide.getRoomId()); + if (room == null) { + return; + } + + Response notice = Response.newBuilder() + .setClientExit(ClientExitResponse.newBuilder() + .setRoomId(room.getId()) + .setExitClientId(clientSide.getId()) + .setExitClientNickname(clientSide.getNickname() == null ? "" : clientSide.getNickname())) + .build(); + + for (ClientSide client : room.getClientSideList()) { + if (client.getChannel() != null) { + ChannelUtils.push(client.getChannel(), notice); + client.init(); + } + } + + for (ClientSide watcher : room.getWatcherList()) { + ChannelUtils.push(watcher.getChannel(), notice); + } + + ServerContains.removeRoom(room.getId()); + } + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/ClientOfflineHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/ClientOfflineHandler.java new file mode 100644 index 0000000..86268b6 --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/ClientOfflineHandler.java @@ -0,0 +1,52 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.protocol.ClientExitResponse; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.server.ServerContains; + +/** + * Triggered directly by the Netty pipeline on channel close or idle timeout — + * NOT routed through the {@code RequestDispatcher}. There is no wire-level + * request for this event; it is observed server-side only. + */ +public final class ClientOfflineHandler { + + private ClientOfflineHandler() { + } + + public static void handle(ClientSide clientSide) { + if (clientSide == null) { + return; + } + Room room = ServerContains.getRoom(clientSide.getRoomId()); + + if (room == null) { + ServerContains.CLIENT_SIDE_MAP.remove(clientSide.getId()); + return; + } + + if (room.getWatcherList().contains(clientSide)) { + room.getWatcherList().remove(clientSide); + return; + } + + Response notice = Response.newBuilder() + .setClientExit(ClientExitResponse.newBuilder() + .setRoomId(room.getId()) + .setExitClientId(clientSide.getId()) + .setExitClientNickname(clientSide.getNickname() == null ? "" : clientSide.getNickname())) + .build(); + + for (ClientSide client : room.getClientSideList()) { + if (client.getChannel() != null && client.getId() != clientSide.getId()) { + ChannelUtils.push(client.getChannel(), notice); + client.init(); + } + } + + ServerContains.removeRoom(room.getId()); + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/CreatePveRoomHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/CreatePveRoomHandler.java new file mode 100644 index 0000000..4a359d1 --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/CreatePveRoomHandler.java @@ -0,0 +1,66 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.common.enums.ClientRole; +import com.miti99.caro.common.enums.ClientStatus; +import com.miti99.caro.common.enums.RoomStatus; +import com.miti99.caro.common.enums.RoomType; +import com.miti99.caro.protocol.PveDifficultyNotSupportResponse; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.CreatePveRoomRequestRecord; +import com.miti99.caro.server.event.request.GameStartingRequestRecord; + +public final class CreatePveRoomHandler { + + private CreatePveRoomHandler() { + } + + public static void handle(ClientSide clientSide, CreatePveRoomRequestRecord req) { + int difficulty = req.difficulty(); + if (difficulty < 1 || difficulty > 3) { + ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + .setPveDifficultyNotSupport(PveDifficultyNotSupportResponse.getDefaultInstance()) + .build()); + return; + } + + Room room = new Room(ServerContains.getServerId()); + room.setType(RoomType.PVE); + room.setStatus(RoomStatus.WAIT); + room.setRoomOwner(clientSide.getNickname()); + room.setDifficultyCoefficient(difficulty); + room.setCreateTime(System.currentTimeMillis()); + room.setLastFlushTime(System.currentTimeMillis()); + + // Add human player + room.getClientSideMap().put(clientSide.getId(), clientSide); + room.getClientSideList().add(clientSide); + clientSide.setRoomId(room.getId()); + clientSide.setRole(ClientRole.BLACK_PLAYER); + + // Add AI robot (1 robot for 2-player Gomoku) + ClientSide robot = new ClientSide(-ServerContains.getClientId(), ClientStatus.PLAYING, null); + robot.setNickname("AI_" + getDifficultyName(difficulty)); + robot.setRole(ClientRole.WHITE_PLAYER); + robot.setRoomId(room.getId()); + room.getClientSideMap().put(robot.getId(), robot); + room.getClientSideList().add(robot); + ServerContains.CLIENT_SIDE_MAP.put(robot.getId(), robot); + + ServerContains.addRoom(room); + + // Auto-start game + GameStartingHandler.handle(clientSide, new GameStartingRequestRecord()); + } + + private static String getDifficultyName(int difficulty) { + return switch (difficulty) { + case 2 -> "Medium"; + case 3 -> "Hard"; + default -> "Easy"; + }; + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/CreateRoomHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/CreateRoomHandler.java new file mode 100644 index 0000000..a5e22ca --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/CreateRoomHandler.java @@ -0,0 +1,43 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.common.enums.ClientRole; +import com.miti99.caro.common.enums.ClientStatus; +import com.miti99.caro.common.enums.RoomStatus; +import com.miti99.caro.common.enums.RoomType; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.protocol.RoomCreateSuccessResponse; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.CreateRoomRequestRecord; + +public final class CreateRoomHandler { + + private CreateRoomHandler() { + } + + public static void handle(ClientSide clientSide, CreateRoomRequestRecord req) { + Room room = new Room(ServerContains.getServerId()); + room.setStatus(RoomStatus.WAIT); + room.setType(RoomType.PVP); + room.setRoomOwner(clientSide.getNickname()); + room.getClientSideMap().put(clientSide.getId(), clientSide); + room.getClientSideList().add(clientSide); + room.setCreateTime(System.currentTimeMillis()); + room.setLastFlushTime(System.currentTimeMillis()); + + clientSide.setRoomId(room.getId()); + clientSide.setRole(ClientRole.BLACK_PLAYER); + clientSide.setStatus(ClientStatus.NO_READY); + + ServerContains.addRoom(room); + + ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + .setRoomCreateSuccess(RoomCreateSuccessResponse.newBuilder() + .setId(room.getId()) + .setRoomOwner(room.getRoomOwner()) + .setRoomType(room.getType().name())) + .build()); + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/GameMoveHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/GameMoveHandler.java new file mode 100644 index 0000000..f5662bb --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/GameMoveHandler.java @@ -0,0 +1,146 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.Board; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.GameMove; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.common.enums.GameResult; +import com.miti99.caro.common.enums.PieceType; +import com.miti99.caro.common.enums.RoomType; +import com.miti99.caro.common.helper.GomokuHelper; +import com.miti99.caro.common.robot.GomokuAI; +import com.miti99.caro.protocol.GameMoveNotYourTurnResponse; +import com.miti99.caro.protocol.GameMoveOccupiedResponse; +import com.miti99.caro.protocol.GameMoveOutOfBoundsResponse; +import com.miti99.caro.protocol.GameMoveSuccessResponse; +import com.miti99.caro.protocol.GameOverResponse; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.protocol.RoomPlayFailNotFoundResponse; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.GameMoveRequestRecord; + +public final class GameMoveHandler { + + private GameMoveHandler() { + } + + public static void handle(ClientSide clientSide, GameMoveRequestRecord req) { + Room room = ServerContains.getRoom(clientSide.getRoomId()); + if (room == null) { + ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + .setRoomPlayFailNotFound(RoomPlayFailNotFoundResponse.getDefaultInstance()) + .build()); + return; + } + + int row = req.row(); + int col = req.col(); + + if (!room.isPlayerTurn(clientSide.getId())) { + ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + .setGameMoveNotYourTurn(GameMoveNotYourTurnResponse.getDefaultInstance()) + .build()); + return; + } + + Board board = room.getGameBoard(); + + if (row < 0 || row >= Board.BOARD_SIZE || col < 0 || col >= Board.BOARD_SIZE) { + ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + .setGameMoveOutOfBounds(GameMoveOutOfBoundsResponse.getDefaultInstance()) + .build()); + return; + } + + if (board.getPiece(row, col) != PieceType.EMPTY) { + ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + .setGameMoveOccupied(GameMoveOccupiedResponse.getDefaultInstance()) + .build()); + return; + } + + GameResult result = GomokuHelper.makeMove(room, row, col, clientSide.getId()); + + broadcastToRoom(room, Response.newBuilder() + .setGameMoveSuccess(GameMoveSuccessResponse.newBuilder() + .setRow(row) + .setCol(col) + .setPiece(room.getPlayerPiece(clientSide.getId()).name()) + .setPlayerNickname(clientSide.getNickname() == null ? "" : clientSide.getNickname()) + .setPlayerId(clientSide.getId())) + .build()); + + if (result != GameResult.IN_PROGRESS) { + handleGameOver(room, result); + return; + } + + if (room.getType() == RoomType.PVE) { + handleAIMove(room); + } + } + + private static void handleAIMove(Room room) { + ClientSide aiPlayer = null; + for (ClientSide client : room.getClientSideList()) { + if (client.getChannel() == null) { + aiPlayer = client; + break; + } + } + if (aiPlayer == null || !room.isPlayerTurn(aiPlayer.getId())) { + return; + } + + PieceType aiPiece = room.getPlayerPiece(aiPlayer.getId()); + GomokuAI ai = new GomokuAI(aiPiece); + GameMove aiMove = ai.getNextMove(room.getGameBoard(), room.getDifficultyCoefficient()); + if (aiMove == null) { + return; + } + + GameResult result = GomokuHelper.makeMove(room, aiMove.getRow(), aiMove.getCol(), aiPlayer.getId()); + + broadcastToRoom(room, Response.newBuilder() + .setGameMoveSuccess(GameMoveSuccessResponse.newBuilder() + .setRow(aiMove.getRow()) + .setCol(aiMove.getCol()) + .setPiece(aiPiece.name()) + .setPlayerNickname(aiPlayer.getNickname() == null ? "" : aiPlayer.getNickname()) + .setPlayerId(aiPlayer.getId())) + .build()); + + if (result != GameResult.IN_PROGRESS) { + handleGameOver(room, result); + } + } + + private static void handleGameOver(Room room, GameResult result) { + String winnerNickname = ""; + if (result == GameResult.BLACK_WIN) { + ClientSide winner = room.getClientSideMap().get(room.getBlackPlayerId()); + winnerNickname = winner != null ? winner.getNickname() : "Black"; + } else if (result == GameResult.WHITE_WIN) { + ClientSide winner = room.getClientSideMap().get(room.getWhitePlayerId()); + winnerNickname = winner != null ? winner.getNickname() : "White"; + } + + broadcastToRoom(room, Response.newBuilder() + .setGameOver(GameOverResponse.newBuilder() + .setResult(result.name()) + .setWinnerNickname(winnerNickname == null ? "" : winnerNickname)) + .build()); + } + + private static void broadcastToRoom(Room room, Response response) { + for (ClientSide client : room.getClientSideList()) { + if (client.getChannel() != null) { + ChannelUtils.push(client.getChannel(), response); + } + } + for (ClientSide watcher : room.getWatcherList()) { + ChannelUtils.push(watcher.getChannel(), response); + } + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/GameReadyHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/GameReadyHandler.java new file mode 100644 index 0000000..937ddb0 --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/GameReadyHandler.java @@ -0,0 +1,57 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.common.enums.ClientStatus; +import com.miti99.caro.common.enums.RoomStatus; +import com.miti99.caro.protocol.GameReadyResponse; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.GameReadyRequestRecord; +import com.miti99.caro.server.event.request.GameStartingRequestRecord; + +public final class GameReadyHandler { + + private GameReadyHandler() { + } + + public static void handle(ClientSide clientSide, GameReadyRequestRecord req) { + Room room = ServerContains.getRoom(clientSide.getRoomId()); + if (room == null || room.getStatus() == RoomStatus.STARTING) { + return; + } + if (clientSide.getStatus() == ClientStatus.PLAYING) { + return; + } + + // Toggle ready state + clientSide.setStatus(clientSide.getStatus() == ClientStatus.READY ? ClientStatus.NO_READY : ClientStatus.READY); + + Response response = Response.newBuilder() + .setGameReady(GameReadyResponse.newBuilder() + .setClientNickname(clientSide.getNickname() == null ? "" : clientSide.getNickname()) + .setStatus(clientSide.getStatus().name()) + .setClientId(clientSide.getId())) + .build(); + + // Check if all human players are ready (need 2 players) + boolean allReady = room.getClientSideMap().size() >= 2; + for (ClientSide client : room.getClientSideList()) { + if (client.getChannel() != null && client.getStatus() != ClientStatus.READY) { + allReady = false; + } + } + + // Notify all human players + for (ClientSide client : room.getClientSideList()) { + if (client.getChannel() != null) { + ChannelUtils.push(client.getChannel(), response); + } + } + + if (allReady) { + GameStartingHandler.handle(clientSide, new GameStartingRequestRecord()); + } + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/GameResetHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/GameResetHandler.java new file mode 100644 index 0000000..124528a --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/GameResetHandler.java @@ -0,0 +1,19 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.server.event.request.GameResetRequestRecord; + +/** + * Placeholder for the {@code game_reset} request. The original server never + * implemented this listener — see the audit in the migration plan. Left as a + * no-op so the sealed dispatch remains exhaustive. + */ +public final class GameResetHandler { + + private GameResetHandler() { + } + + public static void handle(ClientSide client, GameResetRequestRecord req) { + // Not implemented — intentional no-op. + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/GameStartingHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/GameStartingHandler.java new file mode 100644 index 0000000..8699310 --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/GameStartingHandler.java @@ -0,0 +1,70 @@ +package com.miti99.caro.server.event.handler; + +import java.util.LinkedList; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.common.enums.ClientRole; +import com.miti99.caro.common.enums.ClientStatus; +import com.miti99.caro.common.enums.PieceType; +import com.miti99.caro.common.enums.RoomStatus; +import com.miti99.caro.protocol.GameStartingResponse; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.GameStartingRequestRecord; + +public final class GameStartingHandler { + + private static final int BOARD_SIZE = 15; + + private GameStartingHandler() { + } + + public static void handle(ClientSide clientSide, GameStartingRequestRecord req) { + Room room = ServerContains.getRoom(clientSide.getRoomId()); + if (room == null) { + return; + } + + LinkedList roomClientList = room.getClientSideList(); + if (roomClientList.size() < 2) { + return; + } + + ClientSide blackPlayer = roomClientList.get(0); + ClientSide whitePlayer = roomClientList.get(1); + + blackPlayer.setRole(ClientRole.BLACK_PLAYER); + whitePlayer.setRole(ClientRole.WHITE_PLAYER); + blackPlayer.setStatus(ClientStatus.PLAYING); + whitePlayer.setStatus(ClientStatus.PLAYING); + + room.setBlackPlayerId(blackPlayer.getId()); + room.setWhitePlayerId(whitePlayer.getId()); + room.setCurrentTurn(PieceType.BLACK); + room.setStatus(RoomStatus.STARTING); + room.setCreateTime(System.currentTimeMillis()); + room.setLastFlushTime(System.currentTimeMillis()); + room.resetGame(); + + Response response = Response.newBuilder() + .setGameStarting(GameStartingResponse.newBuilder() + .setRoomId(room.getId()) + .setBlackPlayerId(blackPlayer.getId()) + .setBlackPlayerNickname(blackPlayer.getNickname() == null ? "" : blackPlayer.getNickname()) + .setWhitePlayerId(whitePlayer.getId()) + .setWhitePlayerNickname(whitePlayer.getNickname() == null ? "" : whitePlayer.getNickname()) + .setBoardSize(BOARD_SIZE)) + .build(); + + for (ClientSide client : roomClientList) { + if (client.getChannel() != null) { + ChannelUtils.push(client.getChannel(), response); + } + } + for (ClientSide watcher : room.getWatcherList()) { + ChannelUtils.push(watcher.getChannel(), response); + } + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/GetRoomsHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/GetRoomsHandler.java new file mode 100644 index 0000000..280842f --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/GetRoomsHandler.java @@ -0,0 +1,31 @@ +package com.miti99.caro.server.event.handler; + +import java.util.Map; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.protocol.RoomSummary; +import com.miti99.caro.protocol.ShowRoomsResponse; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.GetRoomsRequestRecord; + +public final class GetRoomsHandler { + + private GetRoomsHandler() { + } + + public static void handle(ClientSide client, GetRoomsRequestRecord req) { + ShowRoomsResponse.Builder response = ShowRoomsResponse.newBuilder(); + for (Map.Entry entry : ServerContains.getRoomMap().entrySet()) { + Room room = entry.getValue(); + response.addRooms(RoomSummary.newBuilder() + .setRoomId(room.getId()) + .setRoomOwner(room.getRoomOwner() == null ? "" : room.getRoomOwner()) + .setRoomClientCount(room.getClientSideList().size()) + .setRoomType(room.getType() == null ? "" : room.getType().name())); + } + ChannelUtils.push(client.getChannel(), Response.newBuilder().setShowRooms(response).build()); + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/JoinRoomHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/JoinRoomHandler.java new file mode 100644 index 0000000..2232b16 --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/JoinRoomHandler.java @@ -0,0 +1,78 @@ +package com.miti99.caro.server.event.handler; + +import java.util.LinkedList; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.common.enums.ClientStatus; +import com.miti99.caro.common.enums.RoomStatus; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.protocol.RoomJoinFailFullResponse; +import com.miti99.caro.protocol.RoomJoinFailNotFoundResponse; +import com.miti99.caro.protocol.RoomJoinSuccessResponse; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.GameStartingRequestRecord; +import com.miti99.caro.server.event.request.JoinRoomRequestRecord; + +public final class JoinRoomHandler { + + private JoinRoomHandler() { + } + + public static void handle(ClientSide clientSide, JoinRoomRequestRecord req) { + int roomId = req.roomId(); + Room room = ServerContains.getRoom(roomId); + + if (room == null) { + ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + .setRoomJoinFailNotFound(RoomJoinFailNotFoundResponse.newBuilder().setRoomId(roomId)) + .build()); + return; + } + + // Gomoku is 2-player + if (room.getClientSideList().size() >= 2) { + ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + .setRoomJoinFailFull(RoomJoinFailFullResponse.newBuilder() + .setRoomId(room.getId()) + .setRoomOwner(room.getRoomOwner() == null ? "" : room.getRoomOwner())) + .build()); + return; + } + + clientSide.setStatus(ClientStatus.READY); + clientSide.setRoomId(room.getId()); + + LinkedList roomClientList = room.getClientSideList(); + roomClientList.add(clientSide); + room.getClientSideMap().put(clientSide.getId(), clientSide); + room.setStatus(RoomStatus.WAIT); + + Response joinSuccess = Response.newBuilder() + .setRoomJoinSuccess(RoomJoinSuccessResponse.newBuilder() + .setClientId(clientSide.getId()) + .setClientNickname(clientSide.getNickname() == null ? "" : clientSide.getNickname()) + .setRoomId(room.getId()) + .setRoomOwner(room.getRoomOwner() == null ? "" : room.getRoomOwner()) + .setRoomClientCount(roomClientList.size())) + .build(); + + for (ClientSide client : room.getClientSideMap().values()) { + if (client.getChannel() != null) { + ChannelUtils.push(client.getChannel(), joinSuccess); + } + } + + // Auto-start when 2 players joined + if (roomClientList.size() == 2) { + GameStartingHandler.handle(clientSide, new GameStartingRequestRecord()); + return; + } + + // Notify spectators (legacy behaviour: same typed message, watcher receives join notice) + for (ClientSide watcher : room.getWatcherList()) { + ChannelUtils.push(watcher.getChannel(), joinSuccess); + } + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/SetClientInfoHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/SetClientInfoHandler.java new file mode 100644 index 0000000..750f7af --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/SetClientInfoHandler.java @@ -0,0 +1,21 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.server.event.request.SetClientInfoRequestRecord; + +public final class SetClientInfoHandler { + + private static final String DEFAULT_VERSION = "v1.2.8"; + + private SetClientInfoHandler() { + } + + public static void handle(ClientSide client, SetClientInfoRequestRecord req) { + String version = req.version(); + if (version == null || version.isEmpty()) { + client.setVersion(DEFAULT_VERSION); + } else { + client.setVersion(version); + } + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/SetNicknameHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/SetNicknameHandler.java new file mode 100644 index 0000000..99ef5b1 --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/SetNicknameHandler.java @@ -0,0 +1,31 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.protocol.NicknameSetResponse; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.protocol.ShowOptionsResponse; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.SetNicknameRequestRecord; + +public final class SetNicknameHandler { + + public static final int NICKNAME_MAX_LENGTH = 10; + + private SetNicknameHandler() { + } + + public static void handle(ClientSide client, SetNicknameRequestRecord req) { + String nickname = req.nickname() == null ? "" : req.nickname().trim(); + if (nickname.isEmpty() || nickname.length() > NICKNAME_MAX_LENGTH) { + ChannelUtils.push(client.getChannel(), Response.newBuilder() + .setNicknameSet(NicknameSetResponse.newBuilder().setInvalidLength(nickname.length())) + .build()); + return; + } + ServerContains.CLIENT_SIDE_MAP.get(client.getId()).setNickname(nickname); + ChannelUtils.push(client.getChannel(), Response.newBuilder() + .setShowOptions(ShowOptionsResponse.getDefaultInstance()) + .build()); + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/WatchGameExitHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/WatchGameExitHandler.java new file mode 100644 index 0000000..e248cd2 --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/WatchGameExitHandler.java @@ -0,0 +1,24 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.common.print.SimplePrinter; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.WatchGameExitRequestRecord; + +public final class WatchGameExitHandler { + + private WatchGameExitHandler() { + } + + public static void handle(ClientSide clientSide, WatchGameExitRequestRecord req) { + Room room = ServerContains.getRoom(clientSide.getRoomId()); + if (room != null) { + clientSide.setRoomId(room.getId()); + boolean removed = room.getWatcherList().remove(clientSide); + if (removed) { + SimplePrinter.serverLog(clientSide.getNickname() + " exit room " + room.getId()); + } + } + } +} diff --git a/server/src/main/java/com/miti99/caro/server/event/handler/WatchGameHandler.java b/server/src/main/java/com/miti99/caro/server/event/handler/WatchGameHandler.java new file mode 100644 index 0000000..1fbf893 --- /dev/null +++ b/server/src/main/java/com/miti99/caro/server/event/handler/WatchGameHandler.java @@ -0,0 +1,37 @@ +package com.miti99.caro.server.event.handler; + +import com.miti99.caro.common.channel.ChannelUtils; +import com.miti99.caro.common.entity.ClientSide; +import com.miti99.caro.common.entity.Room; +import com.miti99.caro.protocol.Response; +import com.miti99.caro.protocol.RoomJoinFailNotFoundResponse; +import com.miti99.caro.protocol.WatchGameSuccessResponse; +import com.miti99.caro.server.ServerContains; +import com.miti99.caro.server.event.request.WatchGameRequestRecord; + +public final class WatchGameHandler { + + private WatchGameHandler() { + } + + public static void handle(ClientSide clientSide, WatchGameRequestRecord req) { + int roomId = req.roomId(); + Room room = ServerContains.getRoom(roomId); + + if (room == null) { + ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + .setRoomJoinFailNotFound(RoomJoinFailNotFoundResponse.newBuilder().setRoomId(roomId)) + .build()); + return; + } + + clientSide.setRoomId(room.getId()); + room.getWatcherList().add(clientSide); + + ChannelUtils.push(clientSide.getChannel(), Response.newBuilder() + .setWatchGameSuccess(WatchGameSuccessResponse.newBuilder() + .setOwner(room.getRoomOwner() == null ? "" : room.getRoomOwner()) + .setStatus(room.getStatus() == null ? "" : room.getStatus().toString())) + .build()); + } +} diff --git a/server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java b/server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java index 45021dc..8314c6c 100644 --- a/server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java +++ b/server/src/main/java/com/miti99/caro/server/handler/WebsocketTransferHandler.java @@ -13,6 +13,7 @@ import com.miti99.caro.protocol.Response; import com.miti99.caro.server.ServerContains; import com.miti99.caro.server.event.RequestConverter; import com.miti99.caro.server.event.RequestDispatcher; +import com.miti99.caro.server.event.handler.ClientOfflineHandler; import com.miti99.caro.server.event.request.ClientRequest; import com.miti99.caro.server.event.request.HeartbeatRequestRecord; @@ -107,7 +108,7 @@ public class WebsocketTransferHandler extends SimpleChannelInboundHandler