mirror of
https://github.com/tiennm99/caro.git
synced 2026-08-14 12:25:26 +00:00
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.
This commit is contained in:
@@ -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.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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<ClientSide> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Integer, Room> 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());
|
||||
}
|
||||
}
|
||||
@@ -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<ClientSide> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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<Binary
|
||||
ClientSide client = ServerContains.CLIENT_SIDE_MAP.get(clientId);
|
||||
if (client != null) {
|
||||
SimplePrinter.serverLog("Has client exit to the server: " + clientId + " | " + client.getNickname());
|
||||
// TODO phase 02b: wire to ClientOfflineHandler.handle(client)
|
||||
ClientOfflineHandler.handle(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package com.miti99.caro.server.timer;
|
||||
import java.util.Map;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import com.miti99.caro.common.entity.ClientSide;
|
||||
import com.miti99.caro.common.entity.Room;
|
||||
import com.miti99.caro.common.enums.RoomStatus;
|
||||
import com.miti99.caro.common.print.SimplePrinter;
|
||||
import com.miti99.caro.server.ServerContains;
|
||||
import com.miti99.caro.server.event.handler.ClientExitHandler;
|
||||
import com.miti99.caro.server.event.request.ClientExitRequestRecord;
|
||||
|
||||
/**
|
||||
* Periodically cleans up idle or expired rooms.
|
||||
@@ -52,7 +55,11 @@ public class RoomClearTask extends TimerTask {
|
||||
}
|
||||
|
||||
private void closeRoom(Room room) {
|
||||
// TODO phase 02b: notify remaining clients via ClientExitHandler before removal.
|
||||
ServerContains.removeRoom(room.getId());
|
||||
if (!room.getClientSideList().isEmpty()) {
|
||||
ClientSide first = room.getClientSideList().get(0);
|
||||
ClientExitHandler.handle(first, new ClientExitRequestRecord());
|
||||
} else {
|
||||
ServerContains.removeRoom(room.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user