diff --git a/apps/server/eslint.config.mjs b/apps/server/eslint.config.mjs index e50ff7f..fd5e1d4 100644 --- a/apps/server/eslint.config.mjs +++ b/apps/server/eslint.config.mjs @@ -46,4 +46,22 @@ 'no-restricted-imports': 'off', }, }, + { + files: ['src/domains/**/*.ts'], + rules: { + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@repo/shared', + importNames: ['protocol'], + message: + 'domains 配下では protocol を直接 import せず、network でイベント名を解決してください。', + }, + ], + }, + ], + }, + }, ]; \ No newline at end of file diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index 56ae47e..bbeb4f2 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -1,4 +1,5 @@ import type { TickData } from "../../GameLoop"; +import type { playerTypes } from "@repo/shared"; export interface StartGamePort { addPlayer(id: string): void; @@ -12,7 +13,7 @@ } export interface ReadyForGamePort { - getAllPlayers(): unknown[]; + getAllPlayers(): playerTypes.PlayerData[]; getRoomStartTime(roomId: string): number | undefined; } diff --git a/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts b/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts index 327a343..ce9ce89 100644 --- a/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts @@ -1,22 +1,19 @@ -import { protocol } from "@repo/shared"; import type { DisconnectPlayerPort } from "../ports/gameUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; -type EmitToAll = (event: string, payload?: unknown) => void; - type DisconnectUseCaseParams = { gameManager: DisconnectPlayerPort; playerId: string; - emitToAll: EmitToAll; + publishPlayerRemoved: (playerId: string) => void; }; export const disconnectUseCase = ({ gameManager, playerId, - emitToAll, + publishPlayerRemoved, }: DisconnectUseCaseParams) => { gameManager.removePlayer(playerId); - emitToAll(protocol.SocketEvents.REMOVE_PLAYER, playerId); + publishPlayerRemoved(playerId); logEvent("GameUseCase", { event: "DISCONNECT", result: "player_removed", diff --git a/apps/server/src/domains/game/application/useCases/pingUseCase.ts b/apps/server/src/domains/game/application/useCases/pingUseCase.ts index 0ef9f54..d71fb20 100644 --- a/apps/server/src/domains/game/application/useCases/pingUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/pingUseCase.ts @@ -1,17 +1,13 @@ -import { protocol } from "@repo/shared"; - -type EmitToSocket = (event: string, payload?: unknown) => void; - type PingUseCaseParams = { clientTime: number; - emitToSocket: EmitToSocket; + publishPong: (payload: { clientTime: number; serverTime: number }) => void; }; export const pingUseCase = ({ clientTime, - emitToSocket, + publishPong, }: PingUseCaseParams) => { - emitToSocket(protocol.SocketEvents.PONG, { + publishPong({ clientTime, serverTime: Date.now(), }); diff --git a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts index dd57ff0..dfae3fd 100644 --- a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts @@ -1,24 +1,24 @@ -import { protocol } from "@repo/shared"; import type { ReadyForGamePort } from "../ports/gameUseCasePorts"; +import type { playerTypes } from "@repo/shared"; import { logEvent } from "@server/logging/logEvent"; -type EmitToSocket = (event: string, payload?: unknown) => void; - type ReadyForGameUseCaseParams = { socketId: string; roomId?: string; gameManager: ReadyForGamePort; - emitToSocket: EmitToSocket; + publishCurrentPlayers: (players: playerTypes.PlayerData[]) => void; + publishGameStart: (payload: { startTime: number }) => void; }; export const readyForGameUseCase = ({ socketId, roomId, gameManager, - emitToSocket, + publishCurrentPlayers, + publishGameStart, }: ReadyForGameUseCaseParams) => { const allPlayers = gameManager.getAllPlayers(); - emitToSocket(protocol.SocketEvents.CURRENT_PLAYERS, allPlayers); + publishCurrentPlayers(allPlayers); logEvent("GameUseCase", { event: "READY_FOR_GAME", @@ -42,7 +42,7 @@ return; } - emitToSocket(protocol.SocketEvents.GAME_START, { startTime }); + publishGameStart({ startTime }); logEvent("GameUseCase", { event: "GAME_START", result: "emitted", diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index 1e3d1d8..aa45777 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -1,22 +1,27 @@ -import { protocol, roomConsts } from "@repo/shared"; +import { roomConsts } from "@repo/shared"; +import type { gridMapTypes, playerTypes } from "@repo/shared"; import { RoomManager } from "@server/domains/room/RoomManager"; import type { StartGamePort } from "../ports/gameUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; -type EmitToRoom = (roomId: string, event: string, payload?: unknown) => void; - type StartGameUseCaseParams = { ownerId: string; gameManager: StartGamePort; roomManager: RoomManager; - emitToRoom: EmitToRoom; + publishUpdatePlayer: (roomId: string, playerData: playerTypes.PlayerData) => void; + publishMapCellUpdates: (roomId: string, cellUpdates: gridMapTypes.CellUpdate[]) => void; + publishGameEnd: (roomId: string) => void; + publishGameStart: (roomId: string, payload: { startTime: number }) => void; }; export const startGameUseCase = ({ ownerId, gameManager, roomManager, - emitToRoom, + publishUpdatePlayer, + publishMapCellUpdates, + publishGameEnd, + publishGameStart, }: StartGameUseCaseParams) => { const room = roomManager.getRoomByOwnerId(ownerId); if (!room) { @@ -59,11 +64,11 @@ playerIds, (tickData) => { tickData.players.forEach((playerData) => { - emitToRoom(room.roomId, protocol.SocketEvents.UPDATE_PLAYER, playerData); + publishUpdatePlayer(room.roomId, playerData); }); if (tickData.cellUpdates.length > 0) { - emitToRoom(room.roomId, protocol.SocketEvents.UPDATE_MAP_CELLS, tickData.cellUpdates); + publishMapCellUpdates(room.roomId, tickData.cellUpdates); } }, () => { @@ -73,11 +78,11 @@ roomId: room.roomId, reason: "duration_elapsed", }); - emitToRoom(room.roomId, protocol.SocketEvents.GAME_END); + publishGameEnd(room.roomId); room.status = roomConsts.RoomPhase.WAITING; } ); const startTime = gameManager.getRoomStartTime(room.roomId) || Date.now(); - emitToRoom(room.roomId, protocol.SocketEvents.GAME_START, { startTime }); + publishGameStart(room.roomId, { startTime }); }; diff --git a/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts b/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts index 1123299..c65239f 100644 --- a/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts +++ b/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts @@ -1,22 +1,19 @@ -import { protocol } from "@repo/shared"; import type { roomTypes } from "@repo/shared"; import type { JoinRoomPort } from "../ports/roomUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; -type EmitToRoom = (roomId: string, event: string, payload?: unknown) => void; - type JoinRoomUseCaseParams = { roomManager: JoinRoomPort; socketId: string; data: roomTypes.JoinRoomPayload; - emitToRoom: EmitToRoom; + publishRoomUpdate: (roomId: roomTypes.Room["roomId"], room: roomTypes.Room) => void; }; export const joinRoomUseCase = ({ roomManager, socketId, data, - emitToRoom, + publishRoomUpdate, }: JoinRoomUseCaseParams) => { const { roomId, playerName } = data; logEvent("RoomUseCase", { @@ -29,7 +26,7 @@ const room = roomManager.addPlayerToRoom(roomId, socketId, playerName); - emitToRoom(roomId, protocol.SocketEvents.ROOM_UPDATE, room); + publishRoomUpdate(roomId, room); logEvent("RoomUseCase", { event: "ROOM_UPDATE", result: "emitted", diff --git a/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts b/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts index e3c4a14..2e54090 100644 --- a/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts +++ b/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts @@ -1,19 +1,17 @@ -import { protocol } from "@repo/shared"; import type { DisconnectRoomPort } from "../ports/roomUseCasePorts"; +import type { roomTypes } from "@repo/shared"; import { logEvent } from "@server/logging/logEvent"; -type EmitToRoom = (roomId: string, event: string, payload?: unknown) => void; - type RoomDisconnectUseCaseParams = { roomManager: DisconnectRoomPort; socketId: string; - emitToRoom: EmitToRoom; + publishRoomUpdate: (roomId: roomTypes.Room["roomId"], room: roomTypes.Room) => void; }; export const roomDisconnectUseCase = ({ roomManager, socketId, - emitToRoom, + publishRoomUpdate, }: RoomDisconnectUseCaseParams) => { const updatedRooms = roomManager.removePlayer(socketId); logEvent("RoomUseCase", { @@ -24,7 +22,7 @@ }); updatedRooms.forEach((room) => { - emitToRoom(room.roomId, protocol.SocketEvents.ROOM_UPDATE, room); + publishRoomUpdate(room.roomId, room); logEvent("RoomUseCase", { event: "ROOM_UPDATE", result: "emitted", diff --git a/apps/server/src/network/SocketManager.ts b/apps/server/src/network/SocketManager.ts index 6ab7ad5..39372ed 100644 --- a/apps/server/src/network/SocketManager.ts +++ b/apps/server/src/network/SocketManager.ts @@ -1,10 +1,13 @@ -import { Server, Socket } from "socket.io"; +/** + * SocketManager + * Socket.IO接続ハンドラの登録を初期化するマネージャ + */ +import { Server } from "socket.io"; import { GameManager } from "@server/domains/game/GameManager"; import { RoomManager } from "@server/domains/room/RoomManager"; -import { protocol } from "@repo/shared"; -import { registerRoomHandlers, handleRoomDisconnect } from "./handlers/RoomHandler"; -import { registerGameHandlers, handleGameDisconnect } from "./handlers/GameHandler"; +import { registerConnectionHandlers } from "./handlers/registerConnectionHandlers"; +/** Socket.IOの接続ハンドラ登録を統括する */ export class SocketManager { private io: Server; private gameManager: GameManager; @@ -17,22 +20,11 @@ } public initialize() { - this.io.on(protocol.SocketEvents.CONNECT, (socket: Socket) => { - console.log(`✅ User connected: ${socket.id}`); - - registerRoomHandlers(this.io, socket, this.roomManager); - registerGameHandlers(this.io, socket, this.gameManager, this.roomManager); - - socket.on(protocol.SocketEvents.DISCONNECT, () => { - console.log(`❌ User disconnected: ${socket.id}`); - - // 順番を厳守して実行 - // 1. まずゲーム世界から消す(データ参照ができなくなる前に実行) - handleGameDisconnect(this.io, this.gameManager, socket.id); - - // 2. 次にルームの枠組みから消す(オーナー移譲などのロジックを最後に実行) - handleRoomDisconnect(this.io, socket, this.roomManager); - }); + // 接続時に必要な各ドメインハンドラを登録する + registerConnectionHandlers({ + io: this.io, + gameManager: this.gameManager, + roomManager: this.roomManager, }); } } \ No newline at end of file diff --git a/apps/server/src/network/adapters/socketEmitters.ts b/apps/server/src/network/adapters/socketEmitters.ts index 32439b9..4895b90 100644 --- a/apps/server/src/network/adapters/socketEmitters.ts +++ b/apps/server/src/network/adapters/socketEmitters.ts @@ -1,3 +1,7 @@ +/** + * socketEmitters + * Socket.IOの送信処理を用途別に生成するアダプタ + */ import { Server, Socket } from "socket.io"; type EmitPayload = unknown; @@ -15,18 +19,21 @@ emit(event, payload); }; +/** ルーム単位の送信関数を生成する */ export const createEmitToRoom = (io: Server) => { return (roomId: string, event: string, payload?: EmitPayload) => { emitWithOptionalPayload((eventName, body) => io.to(roomId).emit(eventName, body), event, payload); }; }; +/** 単一ソケット向けの送信関数を生成する */ export const createEmitToSocket = (socket: Socket) => { return (event: string, payload?: EmitPayload) => { emitWithOptionalPayload((eventName, body) => socket.emit(eventName, body), event, payload); }; }; +/** 全接続向けの送信関数を生成する */ export const createEmitToAll = (io: Server) => { return (event: string, payload?: EmitPayload) => { emitWithOptionalPayload((eventName, body) => io.emit(eventName, body), event, payload); diff --git a/apps/server/src/network/bootstrap/boot.ts b/apps/server/src/network/bootstrap/boot.ts index 004c870..29ba1ff 100644 --- a/apps/server/src/network/bootstrap/boot.ts +++ b/apps/server/src/network/bootstrap/boot.ts @@ -1,10 +1,16 @@ +/** + * boot + * HTTPサーバにSocket.IOと各マネージャを接続して起動準備を行う + */ import type { Server as HttpServer } from "http"; import { GameManager } from "@server/domains/game/GameManager"; import { RoomManager } from "@server/domains/room/RoomManager"; import { SocketManager } from "../SocketManager"; import { createIo } from "./createIo"; +/** 通信基盤とドメインマネージャを初期化して接続ハンドラを有効化する */ export const boot = (httpServer: HttpServer) => { + // ネットワーク層とドメイン層の依存を構築する const io = createIo(httpServer); const gameManager = new GameManager(); const roomManager = new RoomManager(); diff --git a/apps/server/src/network/bootstrap/createHttpServer.ts b/apps/server/src/network/bootstrap/createHttpServer.ts index 3434920..f797925 100644 --- a/apps/server/src/network/bootstrap/createHttpServer.ts +++ b/apps/server/src/network/bootstrap/createHttpServer.ts @@ -1,13 +1,20 @@ +/** + * createHttpServer + * ヘルスチェック用途のHTTPサーバを生成する + */ import { createServer } from "http"; +/** ルートの疎通確認と未定義パス応答を提供するHTTPサーバを生成する */ export const createHttpServer = () => { return createServer((req, res) => { + // ヘルスチェック要求に成功応答を返す if (req.url === "/") { res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); res.end("ok"); return; } + // 未定義パスには404を返す res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" }); res.end("not found"); }); diff --git a/apps/server/src/network/bootstrap/createIo.ts b/apps/server/src/network/bootstrap/createIo.ts index e77fb86..c492f28 100644 --- a/apps/server/src/network/bootstrap/createIo.ts +++ b/apps/server/src/network/bootstrap/createIo.ts @@ -1,7 +1,12 @@ +/** + * createIo + * 共有設定を用いてSocket.IOサーバを生成する + */ import { Server } from "socket.io"; import { config } from "@repo/shared"; import type { Server as HttpServer } from "http"; +/** CORS設定を適用したSocket.IOサーバを生成する */ export const createIo = (httpServer: HttpServer) => { return new Server(httpServer, { cors: { diff --git a/apps/server/src/network/createIo.ts b/apps/server/src/network/createIo.ts deleted file mode 100644 index e77fb86..0000000 --- a/apps/server/src/network/createIo.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Server } from "socket.io"; -import { config } from "@repo/shared"; -import type { Server as HttpServer } from "http"; - -export const createIo = (httpServer: HttpServer) => { - return new Server(httpServer, { - cors: { - origin: config.NETWORK_CONFIG.CORS_ORIGIN, - methods: [...config.NETWORK_CONFIG.CORS_METHODS], - }, - }); -}; diff --git a/apps/server/src/network/handlers/CommonHandler.ts b/apps/server/src/network/handlers/CommonHandler.ts index ef0dc0e..cd6bb1e 100644 --- a/apps/server/src/network/handlers/CommonHandler.ts +++ b/apps/server/src/network/handlers/CommonHandler.ts @@ -1,3 +1,7 @@ +/** + * CommonHandler + * 各ハンドラで共有する送信関数群を組み立てる + */ import { Server, Socket } from "socket.io"; import { createEmitToAll, @@ -5,12 +9,14 @@ createEmitToSocket, } from "@server/network/adapters/socketEmitters"; +/** ハンドラで共通利用する送信コンテキスト */ export type CommonHandlerContext = { emitToAll: ReturnType; emitToRoom: ReturnType; emitToSocket: ReturnType; }; +/** 送信先別のエミッタをまとめた共通コンテキストを生成する */ export const createCommonHandlerContext = ( io: Server, socket: Socket diff --git a/apps/server/src/network/handlers/GameHandler.ts b/apps/server/src/network/handlers/GameHandler.ts index c6b1ef1..3cefc15 100644 --- a/apps/server/src/network/handlers/GameHandler.ts +++ b/apps/server/src/network/handlers/GameHandler.ts @@ -1,2 +1,8 @@ +/** + * GameHandler + * ゲーム関連ハンドラを外部公開する再エクスポート定義 + */ +/** ゲームイベント受信ハンドラ登録関数を再公開する */ export { registerGameHandlers } from "./game/registerGameHandlers"; +/** ゲーム切断処理ハンドラを再公開する */ export { handleGameDisconnect } from "./game/handleGameDisconnect"; diff --git a/apps/server/src/network/handlers/RoomHandler.ts b/apps/server/src/network/handlers/RoomHandler.ts index dcd3d5e..4ff4894 100644 --- a/apps/server/src/network/handlers/RoomHandler.ts +++ b/apps/server/src/network/handlers/RoomHandler.ts @@ -1,2 +1,8 @@ +/** + * RoomHandler + * ルーム関連ハンドラを外部公開する再エクスポート定義 + */ +/** ルームイベント受信ハンドラ登録関数を再公開する */ export { registerRoomHandlers } from "./room/registerRoomHandlers"; +/** ルーム切断処理ハンドラを再公開する */ export { handleRoomDisconnect } from "./room/handleRoomDisconnect"; diff --git a/apps/server/src/network/handlers/game/createGameEventPublisher.ts b/apps/server/src/network/handlers/game/createGameEventPublisher.ts new file mode 100644 index 0000000..c8b1927 --- /dev/null +++ b/apps/server/src/network/handlers/game/createGameEventPublisher.ts @@ -0,0 +1,71 @@ +/** + * createGameEventPublisher + * ゲーム系ユースケースから利用する送信関数群を生成する + */ +import { Server } from "socket.io"; +import { protocol } from "@repo/shared"; +import type { gridMapTypes, playerTypes, roomTypes } from "@repo/shared"; +import { createEmitToAll } from "@server/network/adapters/socketEmitters"; +import type { CommonHandlerContext } from "../CommonHandler"; + +type RoomId = roomTypes.Room["roomId"]; +type SocketId = playerTypes.PlayerData["id"]; +type PongPayload = { clientTime: number; serverTime: number }; +type GameStartPayload = { startTime: number }; +type CurrentPlayersPayload = playerTypes.PlayerData[]; +type UpdatePlayerPayload = playerTypes.PlayerData; +type MapCellUpdatesPayload = gridMapTypes.CellUpdate[]; + +/** ゲーム進行中イベントの送信インターフェース */ +export type GameEventPublisher = { + publishPongToSocket: (payload: PongPayload) => void; + publishUpdatePlayerToRoom: (roomId: RoomId, playerData: UpdatePlayerPayload) => void; + publishMapCellUpdatesToRoom: (roomId: RoomId, cellUpdates: MapCellUpdatesPayload) => void; + publishGameEndToRoom: (roomId: RoomId) => void; + publishGameStartToRoom: (roomId: RoomId, payload: GameStartPayload) => void; + publishCurrentPlayersToSocket: (players: CurrentPlayersPayload) => void; + publishGameStartToSocket: (payload: GameStartPayload) => void; +}; + +/** 切断時に配信するゲームイベントの送信インターフェース */ +export type GameDisconnectPublisher = { + publishPlayerRemovedToAll: (removedPlayerId: SocketId) => void; +}; + +/** 共通送信コンテキストからゲームイベント送信関数群を生成する */ +export const createGameEventPublisher = (common: CommonHandlerContext): GameEventPublisher => { + return { + publishPongToSocket: (payload: PongPayload) => { + common.emitToSocket(protocol.SocketEvents.PONG, payload); + }, + publishUpdatePlayerToRoom: (roomId: RoomId, playerData: UpdatePlayerPayload) => { + common.emitToRoom(roomId, protocol.SocketEvents.UPDATE_PLAYER, playerData); + }, + publishMapCellUpdatesToRoom: (roomId: RoomId, cellUpdates: MapCellUpdatesPayload) => { + common.emitToRoom(roomId, protocol.SocketEvents.UPDATE_MAP_CELLS, cellUpdates); + }, + publishGameEndToRoom: (roomId: RoomId) => { + common.emitToRoom(roomId, protocol.SocketEvents.GAME_END); + }, + publishGameStartToRoom: (roomId: RoomId, payload: GameStartPayload) => { + common.emitToRoom(roomId, protocol.SocketEvents.GAME_START, payload); + }, + publishCurrentPlayersToSocket: (players: CurrentPlayersPayload) => { + common.emitToSocket(protocol.SocketEvents.CURRENT_PLAYERS, players); + }, + publishGameStartToSocket: (payload: GameStartPayload) => { + common.emitToSocket(protocol.SocketEvents.GAME_START, payload); + }, + }; +}; + +/** ゲーム切断時の送信関数群を生成する */ +export const createGameDisconnectPublisher = (io: Server): GameDisconnectPublisher => { + const emitToAll = createEmitToAll(io); + + return { + publishPlayerRemovedToAll: (removedPlayerId: SocketId) => { + emitToAll(protocol.SocketEvents.REMOVE_PLAYER, removedPlayerId); + }, + }; +}; diff --git a/apps/server/src/network/handlers/game/handleGameDisconnect.ts b/apps/server/src/network/handlers/game/handleGameDisconnect.ts index dc9e33d..3cb2c11 100644 --- a/apps/server/src/network/handlers/game/handleGameDisconnect.ts +++ b/apps/server/src/network/handlers/game/handleGameDisconnect.ts @@ -1,16 +1,23 @@ +/** + * handleGameDisconnect + * ゲーム切断ユースケースを呼び出してプレイヤー離脱を配信する + */ import { Server } from "socket.io"; import { GameManager } from "@server/domains/game/GameManager"; import { disconnectUseCase } from "@server/domains/game/application/useCases/disconnectUseCase"; -import { createEmitToAll } from "@server/network/adapters/socketEmitters"; +import { createGameDisconnectPublisher } from "./createGameEventPublisher"; +/** 切断したプレイヤーをゲーム管理から除外し通知する */ export const handleGameDisconnect = ( io: Server, gameManager: GameManager, playerId: string ) => { + const gameDisconnectPublisher = createGameDisconnectPublisher(io); + disconnectUseCase({ gameManager, playerId, - emitToAll: createEmitToAll(io), + publishPlayerRemoved: gameDisconnectPublisher.publishPlayerRemovedToAll, }); }; diff --git a/apps/server/src/network/handlers/game/registerGameHandlers.ts b/apps/server/src/network/handlers/game/registerGameHandlers.ts index 61a8f65..29d3511 100644 --- a/apps/server/src/network/handlers/game/registerGameHandlers.ts +++ b/apps/server/src/network/handlers/game/registerGameHandlers.ts @@ -1,3 +1,7 @@ +/** + * registerGameHandlers + * ゲーム関連イベントの受信ハンドラを登録する + */ import { Server, Socket } from "socket.io"; import { GameManager } from "@server/domains/game/GameManager"; import { RoomManager } from "@server/domains/room/RoomManager"; @@ -8,7 +12,11 @@ import { readyForGameUseCase } from "@server/domains/game/application/useCases/readyForGameUseCase"; import { movePlayerUseCase } from "@server/domains/game/application/useCases/movePlayerUseCase"; import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; +import { createGameEventPublisher } from "./createGameEventPublisher"; +import { logEvent } from "@server/logging/logEvent"; +import { isMovePayload, isPingPayload } from "@server/network/validation/socketPayloadValidators"; +/** ゲームイベントの購読とユースケース呼び出しを設定する */ export const registerGameHandlers = ( io: Server, socket: Socket, @@ -16,23 +24,39 @@ roomManager: RoomManager ) => { const common = createCommonHandlerContext(io, socket); + const gamePublisher = createGameEventPublisher(common); - socket.on(protocol.SocketEvents.PING, (clientTime: number) => { + // 遅延計測用のPINGを検証しPONGを返す + socket.on(protocol.SocketEvents.PING, (clientTime: unknown) => { + if (!isPingPayload(clientTime)) { + logEvent("Network", { + event: "PING", + result: "ignored_invalid_payload", + socketId: socket.id, + }); + return; + } + pingUseCase({ clientTime, - emitToSocket: common.emitToSocket, + publishPong: gamePublisher.publishPongToSocket, }); }); + // オーナー開始要求に応じてゲーム進行ユースケースを起動する socket.on(protocol.SocketEvents.START_GAME, () => { startGameUseCase({ ownerId: socket.id, gameManager, roomManager, - emitToRoom: common.emitToRoom, + publishUpdatePlayer: gamePublisher.publishUpdatePlayerToRoom, + publishMapCellUpdates: gamePublisher.publishMapCellUpdatesToRoom, + publishGameEnd: gamePublisher.publishGameEndToRoom, + publishGameStart: gamePublisher.publishGameStartToRoom, }); }); + // 参加者の準備完了通知を受けて現在状態を返す socket.on(protocol.SocketEvents.READY_FOR_GAME, () => { const roomId = Array.from(socket.rooms).find((room) => room !== socket.id); @@ -40,11 +64,22 @@ socketId: socket.id, roomId, gameManager, - emitToSocket: common.emitToSocket, + publishCurrentPlayers: gamePublisher.publishCurrentPlayersToSocket, + publishGameStart: gamePublisher.publishGameStartToSocket, }); }); - socket.on(protocol.SocketEvents.MOVE, (data: playerTypes.MovePayload) => { + // 移動入力を検証しプレイヤー移動ユースケースへ連携する + socket.on(protocol.SocketEvents.MOVE, (data: unknown) => { + if (!isMovePayload(data)) { + logEvent("Network", { + event: "MOVE", + result: "ignored_invalid_payload", + socketId: socket.id, + }); + return; + } + movePlayerUseCase({ gameManager, playerId: socket.id, diff --git a/apps/server/src/network/handlers/registerConnectionHandlers.ts b/apps/server/src/network/handlers/registerConnectionHandlers.ts new file mode 100644 index 0000000..690c439 --- /dev/null +++ b/apps/server/src/network/handlers/registerConnectionHandlers.ts @@ -0,0 +1,48 @@ +/** + * registerConnectionHandlers + * 接続時にルームとゲームの各ハンドラを登録する + */ +import { Server, Socket } from "socket.io"; +import { GameManager } from "@server/domains/game/GameManager"; +import { RoomManager } from "@server/domains/room/RoomManager"; +import { protocol } from "@repo/shared"; +import { registerRoomHandlers, handleRoomDisconnect } from "./RoomHandler"; +import { registerGameHandlers, handleGameDisconnect } from "./GameHandler"; +import { logEvent } from "@server/logging/logEvent"; + +type RegisterConnectionHandlersParams = { + io: Server; + gameManager: GameManager; + roomManager: RoomManager; +}; + +/** ソケット接続と切断イベントに対する共通ハンドラを登録する */ +export const registerConnectionHandlers = ({ + io, + gameManager, + roomManager, +}: RegisterConnectionHandlersParams) => { + io.on(protocol.SocketEvents.CONNECT, (socket: Socket) => { + // 接続ログを記録してドメイン別ハンドラを登録する + logEvent("Network", { + event: "CONNECT", + result: "connected", + socketId: socket.id, + }); + + registerRoomHandlers(io, socket, roomManager); + registerGameHandlers(io, socket, gameManager, roomManager); + + socket.on(protocol.SocketEvents.DISCONNECT, () => { + // 切断ログ記録後にドメイン別の後処理を実行する + logEvent("Network", { + event: "DISCONNECT", + result: "disconnected", + socketId: socket.id, + }); + + handleGameDisconnect(io, gameManager, socket.id); + handleRoomDisconnect(io, socket, roomManager); + }); + }); +}; diff --git a/apps/server/src/network/handlers/room/createRoomEventPublisher.ts b/apps/server/src/network/handlers/room/createRoomEventPublisher.ts new file mode 100644 index 0000000..3184664 --- /dev/null +++ b/apps/server/src/network/handlers/room/createRoomEventPublisher.ts @@ -0,0 +1,39 @@ +/** + * createRoomEventPublisher + * ルーム系ユースケースから利用する送信関数を生成する + */ +import { Server } from "socket.io"; +import { protocol } from "@repo/shared"; +import { createEmitToRoom } from "@server/network/adapters/socketEmitters"; +import type { roomTypes } from "@repo/shared"; +import type { CommonHandlerContext } from "../CommonHandler"; + +type RoomId = roomTypes.Room["roomId"]; +type RoomUpdatePayload = roomTypes.Room; + +/** ルーム更新イベントの送信インターフェース */ +export type RoomEventPublisher = { + publishRoomUpdate: (roomId: RoomId, room: RoomUpdatePayload) => void; +}; + +/** 共通送信コンテキストからルームイベント送信関数を生成する */ +export const createRoomEventPublisher = ( + common: CommonHandlerContext +): RoomEventPublisher => { + return { + publishRoomUpdate: (roomId: RoomId, room: RoomUpdatePayload) => { + common.emitToRoom(roomId, protocol.SocketEvents.ROOM_UPDATE, room); + }, + }; +}; + +/** 切断時のルーム更新送信関数を生成する */ +export const createRoomDisconnectPublisher = (io: Server): RoomEventPublisher => { + const emitToRoom = createEmitToRoom(io); + + return { + publishRoomUpdate: (roomId: RoomId, room: RoomUpdatePayload) => { + emitToRoom(roomId, protocol.SocketEvents.ROOM_UPDATE, room); + }, + }; +}; diff --git a/apps/server/src/network/handlers/room/handleRoomDisconnect.ts b/apps/server/src/network/handlers/room/handleRoomDisconnect.ts index 03046af..c711d20 100644 --- a/apps/server/src/network/handlers/room/handleRoomDisconnect.ts +++ b/apps/server/src/network/handlers/room/handleRoomDisconnect.ts @@ -1,16 +1,23 @@ +/** + * handleRoomDisconnect + * ルーム切断ユースケースを呼び出してルーム状態更新を配信する + */ import { Server, Socket } from "socket.io"; import { RoomManager } from "@server/domains/room/RoomManager"; import { roomDisconnectUseCase } from "@server/domains/room/application/useCases/roomDisconnectUseCase"; -import { createEmitToRoom } from "@server/network/adapters/socketEmitters"; +import { createRoomDisconnectPublisher } from "./createRoomEventPublisher"; +/** 切断ソケットのルーム離脱処理を実行して更新通知する */ export const handleRoomDisconnect = ( io: Server, socket: Socket, roomManager: RoomManager ) => { + const roomDisconnectPublisher = createRoomDisconnectPublisher(io); + roomDisconnectUseCase({ roomManager, socketId: socket.id, - emitToRoom: createEmitToRoom(io), + publishRoomUpdate: roomDisconnectPublisher.publishRoomUpdate, }); }; diff --git a/apps/server/src/network/handlers/room/registerRoomHandlers.ts b/apps/server/src/network/handlers/room/registerRoomHandlers.ts index f5173b5..0c3dd03 100644 --- a/apps/server/src/network/handlers/room/registerRoomHandlers.ts +++ b/apps/server/src/network/handlers/room/registerRoomHandlers.ts @@ -1,18 +1,37 @@ +/** + * registerRoomHandlers + * ルーム参加イベントの受信ハンドラを登録する + */ import { Server, Socket } from "socket.io"; import { RoomManager } from "@server/domains/room/RoomManager"; import { protocol } from "@repo/shared"; import type { roomTypes } from "@repo/shared"; import { joinRoomUseCase } from "@server/domains/room/application/useCases/joinRoomUseCase"; import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; +import { createRoomEventPublisher } from "./createRoomEventPublisher"; +import { isJoinRoomPayload } from "@server/network/validation/socketPayloadValidators"; +import { logEvent } from "@server/logging/logEvent"; +/** ルーム参加イベントを検証して参加ユースケースへ連携する */ export const registerRoomHandlers = ( io: Server, socket: Socket, roomManager: RoomManager ) => { const common = createCommonHandlerContext(io, socket); + const roomPublisher = createRoomEventPublisher(common); - socket.on(protocol.SocketEvents.JOIN_ROOM, (data: roomTypes.JoinRoomPayload) => { + // 参加要求のペイロード検証と参加処理を実行する + socket.on(protocol.SocketEvents.JOIN_ROOM, (data: unknown) => { + if (!isJoinRoomPayload(data)) { + logEvent("Network", { + event: "JOIN_ROOM", + result: "ignored_invalid_payload", + socketId: socket.id, + }); + return; + } + const { roomId } = data; socket.join(roomId); @@ -21,7 +40,7 @@ roomManager, socketId: socket.id, data, - emitToRoom: common.emitToRoom, + publishRoomUpdate: roomPublisher.publishRoomUpdate, }); }); }; diff --git a/apps/server/src/network/validation/socketPayloadValidators.ts b/apps/server/src/network/validation/socketPayloadValidators.ts new file mode 100644 index 0000000..beba0ee --- /dev/null +++ b/apps/server/src/network/validation/socketPayloadValidators.ts @@ -0,0 +1,38 @@ +/** + * socketPayloadValidators + * ソケット受信ペイロードの型ガードを提供する + */ +import type { playerTypes, roomTypes } from "@repo/shared"; + +const isFiniteNumber = (value: unknown): value is number => { + return typeof value === "number" && Number.isFinite(value); +}; + +const isNonEmptyString = (value: unknown): value is string => { + return typeof value === "string" && value.trim().length > 0; +}; + +/** PINGイベントのペイロードが数値であるか判定する */ +export const isPingPayload = (value: unknown): value is number => { + return isFiniteNumber(value); +}; + +/** MOVEイベントのペイロードが移動座標であるか判定する */ +export const isMovePayload = (value: unknown): value is playerTypes.MovePayload => { + if (typeof value !== "object" || value === null) { + return false; + } + + const candidate = value as Record; + return isFiniteNumber(candidate.x) && isFiniteNumber(candidate.y); +}; + +/** JOIN_ROOMイベントのペイロードが参加情報であるか判定する */ +export const isJoinRoomPayload = (value: unknown): value is roomTypes.JoinRoomPayload => { + if (typeof value !== "object" || value === null) { + return false; + } + + const candidate = value as Record; + return isNonEmptyString(candidate.roomId) && isNonEmptyString(candidate.playerName); +}; diff --git "a/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" "b/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" index 3e34b9e..bb06683 100644 --- "a/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" +++ "b/docs/01_Env/ENV_01_\347\222\260\345\242\203\346\247\213\347\257\211\343\203\273\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.txt" @@ -101,13 +101,19 @@ │ │ ├── game/ │ │ │ ├── GameLoop.ts # 固定ループ │ │ │ ├── GameManager.ts # ゲーム状態管理 - │ │ │ ├── application/ # ユースケース・サービス + │ │ │ ├── application/ + │ │ │ │ ├── ports/ # ユースケース入出力境界 + │ │ │ │ ├── services/ # ゲームアプリケーションサービス + │ │ │ │ └── useCases/ # ゲームユースケース │ │ │ ├── entities/ │ │ │ │ └── Player.ts # サーバーPlayer │ │ │ └── states/ │ │ │ └── MapStore.ts # マップ状態保持 │ │ └── room/ - │ │ ├── application/ # ユースケース・サービス + │ │ ├── application/ + │ │ │ ├── ports/ # ユースケース入出力境界 + │ │ │ ├── services/ # ルームアプリケーションサービス + │ │ │ └── useCases/ # ルームユースケース │ │ └── RoomManager.ts # ルーム管理 │ └── network/ │ ├── SocketManager.ts # WS接続管理 @@ -117,13 +123,16 @@ │ │ ├── CommonHandler.ts # 共通WSハンドラ │ │ ├── GameHandler.ts # ゲームWSハンドラ(中間層) │ │ ├── RoomHandler.ts # ルームWSハンドラ(中間層) - │ │ ├── game/ # ゲーム中粒度配線 - │ │ └── room/ # ルーム中粒度配線 + │ │ ├── game/ + │ │ │ ├── handleGameDisconnect.ts # ゲーム切断処理配線 + │ │ │ └── registerGameHandlers.ts # ゲーム受信イベント配線 + │ │ └── room/ + │ │ ├── handleRoomDisconnect.ts # ルーム切断処理配線 + │ │ └── registerRoomHandlers.ts # ルーム受信イベント配線 │ ├── bootstrap/ │ │ ├── createHttpServer.ts # HTTP生成 │ │ ├── createIo.ts # Socket.IO生成 │ │ └── boot.ts # 起動配線 - │ └── createIo.ts # 互換用ファイル ├── docs/ │ ├── 01_Env/ │ │ ├── ENV_01_環境構築・技術スタック.txt # 環境・技術定義 @@ -131,11 +140,13 @@ │ │ ├── ENV_03_TypeScript概要.txt # TS基礎説明 │ │ ├── ENV_04_スマホ実機デバッグ手順.txt # 実機デバッグ手順 │ │ ├── ENV_05_Docker運用操作ガイド.txt # Docker運用手順 - │ │ └── ENV_06_管理者用環境構築手順.txt # 管理者向け手順 + │ │ ├── ENV_06_管理者用環境構築手順.txt # 管理者向け手順 + │ │ └── ENV_07_テスト操作手順.txt # テスト操作手順 │ ├── 02_Guide/ │ │ ├── GUIDE_01_ドキュメント作成ガイド.txt # 文書作成ルール │ │ ├── GUIDE_02_ファイル命名規則.txt # 命名規則 - │ │ └── GUIDE_03_Git運用ルール.txt # Git運用ルール + │ │ ├── GUIDE_03_Git運用ルール.txt # Git運用ルール + │ │ └── GUIDE_04_コードコメント規則.txt # コメント運用ルール │ ├── 03_Plan/ │ │ └── PLAN_01_移動テスト実装計画.txt # 実装計画 │ └── 04_Spec/ @@ -289,4 +300,9 @@ 5-3. 依存方向 - OK: Client -> Shared - OK: Server -> Shared - - NG: Client -> Server / Server -> Client (直接参照禁止) \ No newline at end of file + - NG: Client -> Server / Server -> Client (直接参照禁止) + +5-4. 通信境界の責務分離 + - SocketEvents の解決(イベント名の参照)は `apps/server/src/network` 配下に集約する. + - `apps/server/src/domains` 配下では `protocol` を直接 import せず,意味名の Publisher 関数を受け取って利用する. + - Socket の接続制御・受信イベント登録・切断順序制御は network 層が担当し,業務ロジック判断は domain 層が担当する. \ No newline at end of file