diff --git a/apps/client/src/hooks/useAppFlow.ts b/apps/client/src/hooks/useAppFlow.ts index ed9d840..bb6bafc 100644 --- a/apps/client/src/hooks/useAppFlow.ts +++ b/apps/client/src/hooks/useAppFlow.ts @@ -19,6 +19,16 @@ const [joinErrorMessage, setJoinErrorMessage] = useState(null); const [isJoining, setIsJoining] = useState(false); const joinTimeoutRef = useRef | null>(null); + const joinRejectedHandlerRef = useRef<((payload: roomTypes.JoinRoomRejectedPayload) => void) | null>(null); + + const clearJoinRejectedHandler = () => { + if (!joinRejectedHandlerRef.current) { + return; + } + + socketManager.title.offJoinRejected(joinRejectedHandlerRef.current); + joinRejectedHandlerRef.current = null; + }; const clearJoinTimeout = () => { if (!joinTimeoutRef.current) { @@ -35,30 +45,15 @@ } clearJoinTimeout(); + clearJoinRejectedHandler(); setJoinErrorMessage(null); setIsJoining(true); - joinTimeoutRef.current = setTimeout(() => { - setIsJoining(false); - setJoinErrorMessage("参加要求がタイムアウトしました,もう一度お試しください"); - joinTimeoutRef.current = null; - }, config.GAME_CONFIG.JOIN_REQUEST_TIMEOUT_MS); - socketManager.title.joinRoom(payload); - }; - useEffect(() => { - const handleConnect = (id: string) => { - setMyId(id); - }; - const handleRoomUpdate = (updatedRoom: roomTypes.Room) => { - clearJoinTimeout(); - setRoom(updatedRoom); - setIsJoining(false); - setJoinErrorMessage(null); - setScenePhase(appConsts.ScenePhase.LOBBY); - }; const handleJoinRejected = (payload: roomTypes.JoinRoomRejectedPayload) => { clearJoinTimeout(); setIsJoining(false); + joinRejectedHandlerRef.current = null; + if (payload.reason === "full") { setJoinErrorMessage(`ルーム ${payload.roomId} は満員です`); return; @@ -68,21 +63,45 @@ setJoinErrorMessage(`ルーム ${payload.roomId} への参加要求が重複しました`); } }; + + joinRejectedHandlerRef.current = handleJoinRejected; + socketManager.title.onceJoinRejected(handleJoinRejected); + + joinTimeoutRef.current = setTimeout(() => { + clearJoinRejectedHandler(); + setIsJoining(false); + setJoinErrorMessage("参加要求がタイムアウトしました,もう一度お試しください"); + joinTimeoutRef.current = null; + }, config.GAME_CONFIG.JOIN_REQUEST_TIMEOUT_MS); + + socketManager.title.joinRoom(payload); + }; + + useEffect(() => { + const handleConnect = (id: string) => { + setMyId(id); + }; + const handleRoomUpdate = (updatedRoom: roomTypes.Room) => { + clearJoinTimeout(); + clearJoinRejectedHandler(); + setRoom(updatedRoom); + setIsJoining(false); + setJoinErrorMessage(null); + setScenePhase(appConsts.ScenePhase.LOBBY); + }; const handleGameStart = () => { setScenePhase(appConsts.ScenePhase.PLAYING); }; socketManager.common.onConnect(handleConnect); - socketManager.title.onJoinRejected(handleJoinRejected); socketManager.lobby.onRoomUpdate(handleRoomUpdate); - socketManager.game.onGameStart(handleGameStart); + socketManager.game.onceGameStart(handleGameStart); return () => { clearJoinTimeout(); + clearJoinRejectedHandler(); socketManager.common.offConnect(handleConnect); - socketManager.title.offJoinRejected(handleJoinRejected); socketManager.lobby.offRoomUpdate(handleRoomUpdate); - socketManager.game.offGameStart(handleGameStart); }; }, []); diff --git a/apps/client/src/network/handlers/CommonHandler.ts b/apps/client/src/network/handlers/CommonHandler.ts index 0c6d10b..76b798a 100644 --- a/apps/client/src/network/handlers/CommonHandler.ts +++ b/apps/client/src/network/handlers/CommonHandler.ts @@ -1,5 +1,7 @@ import type { Socket } from "socket.io-client"; import { protocol } from "@repo/shared"; +import type { PayloadOf } from "@repo/shared"; +import { createClientSocketEventBridge } from "./socketEventBridge"; type CommonHandler = { onConnect: (callback: (id: string) => void) => void; @@ -7,7 +9,12 @@ }; export const createCommonHandler = (socket: Socket): CommonHandler => { - const connectListenerMap = new Map<(id: string) => void, () => void>(); + const connectListenerMap = new Map< + (id: string) => void, + (payload: PayloadOf) => void + >(); + + const { onEvent, offEvent } = createClientSocketEventBridge(socket); return { onConnect: (callback: (id: string) => void) => { @@ -15,18 +22,18 @@ callback(socket.id || ""); } - const listener = () => { + const listener = (_payload: PayloadOf) => { callback(socket.id || ""); }; connectListenerMap.set(callback, listener); - socket.on(protocol.SocketEvents.CONNECT, listener); + onEvent(protocol.SocketEvents.CONNECT, listener); }, offConnect: (callback: (id: string) => void) => { const listener = connectListenerMap.get(callback); if (!listener) return; - socket.off(protocol.SocketEvents.CONNECT, listener); + offEvent(protocol.SocketEvents.CONNECT, listener); connectListenerMap.delete(callback); } }; diff --git a/apps/client/src/network/handlers/GameHandler.ts b/apps/client/src/network/handlers/GameHandler.ts index 0360d91..e4b307d 100644 --- a/apps/client/src/network/handlers/GameHandler.ts +++ b/apps/client/src/network/handlers/GameHandler.ts @@ -5,71 +5,86 @@ */ import type { Socket } from "socket.io-client"; import { protocol } from "@repo/shared"; -import type { playerTypes, gridMapTypes, UpdatePlayersPayload } from "@repo/shared"; +import type { + CurrentPlayersPayload, + GameStartPayload, + MovePayload, + NewPlayerPayload, + RemovePlayerPayload, + UpdateMapCellsPayload, + UpdatePlayersPayload, +} from "@repo/shared"; +import { createClientSocketEventBridge } from "./socketEventBridge"; /** ゲームシーンが利用するソケット操作の契約 */ export type GameHandler = { - onCurrentPlayers: (callback: (players: playerTypes.PlayerData[] | Record) => void) => void; - offCurrentPlayers: (callback: (players: playerTypes.PlayerData[] | Record) => void) => void; - onNewPlayer: (callback: (player: playerTypes.PlayerData) => void) => void; - offNewPlayer: (callback: (player: playerTypes.PlayerData) => void) => void; + onCurrentPlayers: (callback: (players: CurrentPlayersPayload) => void) => void; + offCurrentPlayers: (callback: (players: CurrentPlayersPayload) => void) => void; + onNewPlayer: (callback: (player: NewPlayerPayload) => void) => void; + offNewPlayer: (callback: (player: NewPlayerPayload) => void) => void; onUpdatePlayers: (callback: (players: UpdatePlayersPayload) => void) => void; offUpdatePlayers: (callback: (players: UpdatePlayersPayload) => void) => void; - onRemovePlayer: (callback: (id: string) => void) => void; - offRemovePlayer: (callback: (id: string) => void) => void; - onUpdateMapCells: (callback: (updates: gridMapTypes.CellUpdate[]) => void) => void; - offUpdateMapCells: (callback: (updates: gridMapTypes.CellUpdate[]) => void) => void; - onGameStart: (callback: (data: { startTime: number }) => void) => void; - offGameStart: (callback: (data: { startTime: number }) => void) => void; + onRemovePlayer: (callback: (id: RemovePlayerPayload) => void) => void; + offRemovePlayer: (callback: (id: RemovePlayerPayload) => void) => void; + onUpdateMapCells: (callback: (updates: UpdateMapCellsPayload) => void) => void; + offUpdateMapCells: (callback: (updates: UpdateMapCellsPayload) => void) => void; + onGameStart: (callback: (data: GameStartPayload) => void) => void; + onceGameStart: (callback: (data: GameStartPayload) => void) => void; + offGameStart: (callback: (data: GameStartPayload) => void) => void; sendMove: (x: number, y: number) => void; readyForGame: () => void; }; /** ソケットインスタンスからゲーム向けハンドラを生成する */ export const createGameHandler = (socket: Socket): GameHandler => { + const { onEvent, onceEvent, offEvent, emitEvent } = createClientSocketEventBridge(socket); + return { onCurrentPlayers: (callback) => { - socket.on(protocol.SocketEvents.CURRENT_PLAYERS, callback); + onEvent(protocol.SocketEvents.CURRENT_PLAYERS, callback); }, offCurrentPlayers: (callback) => { - socket.off(protocol.SocketEvents.CURRENT_PLAYERS, callback); + offEvent(protocol.SocketEvents.CURRENT_PLAYERS, callback); }, onNewPlayer: (callback) => { - socket.on(protocol.SocketEvents.NEW_PLAYER, callback); + onEvent(protocol.SocketEvents.NEW_PLAYER, callback); }, offNewPlayer: (callback) => { - socket.off(protocol.SocketEvents.NEW_PLAYER, callback); + offEvent(protocol.SocketEvents.NEW_PLAYER, callback); }, onUpdatePlayers: (callback) => { - socket.on(protocol.SocketEvents.UPDATE_PLAYERS, callback); + onEvent(protocol.SocketEvents.UPDATE_PLAYERS, callback); }, offUpdatePlayers: (callback) => { - socket.off(protocol.SocketEvents.UPDATE_PLAYERS, callback); + offEvent(protocol.SocketEvents.UPDATE_PLAYERS, callback); }, onRemovePlayer: (callback) => { - socket.on(protocol.SocketEvents.REMOVE_PLAYER, callback); + onEvent(protocol.SocketEvents.REMOVE_PLAYER, callback); }, offRemovePlayer: (callback) => { - socket.off(protocol.SocketEvents.REMOVE_PLAYER, callback); + offEvent(protocol.SocketEvents.REMOVE_PLAYER, callback); }, onUpdateMapCells: (callback) => { - socket.on(protocol.SocketEvents.UPDATE_MAP_CELLS, callback); + onEvent(protocol.SocketEvents.UPDATE_MAP_CELLS, callback); }, offUpdateMapCells: (callback) => { - socket.off(protocol.SocketEvents.UPDATE_MAP_CELLS, callback); + offEvent(protocol.SocketEvents.UPDATE_MAP_CELLS, callback); }, onGameStart: (callback) => { - socket.on(protocol.SocketEvents.GAME_START, callback); + onEvent(protocol.SocketEvents.GAME_START, callback); + }, + onceGameStart: (callback) => { + onceEvent(protocol.SocketEvents.GAME_START, callback); }, offGameStart: (callback) => { - socket.off(protocol.SocketEvents.GAME_START, callback); + offEvent(protocol.SocketEvents.GAME_START, callback); }, sendMove: (x, y) => { - const payload: playerTypes.MovePayload = { x, y }; - socket.emit(protocol.SocketEvents.MOVE, payload); + const payload: MovePayload = { x, y }; + emitEvent(protocol.SocketEvents.MOVE, payload); }, readyForGame: () => { - socket.emit(protocol.SocketEvents.READY_FOR_GAME); + emitEvent(protocol.SocketEvents.READY_FOR_GAME); } }; }; diff --git a/apps/client/src/network/handlers/LobbyHandler.ts b/apps/client/src/network/handlers/LobbyHandler.ts index b315f6c..9b1c594 100644 --- a/apps/client/src/network/handlers/LobbyHandler.ts +++ b/apps/client/src/network/handlers/LobbyHandler.ts @@ -1,23 +1,30 @@ import type { Socket } from "socket.io-client"; import { protocol } from "@repo/shared"; -import type { roomTypes } from "@repo/shared"; +import type { PayloadOf } from "@repo/shared"; +import { createClientSocketEventBridge } from "./socketEventBridge"; type LobbyHandler = { - onRoomUpdate: (callback: (room: roomTypes.Room) => void) => void; - offRoomUpdate: (callback: (room: roomTypes.Room) => void) => void; + onRoomUpdate: (callback: (room: PayloadOf) => void) => void; + onceRoomUpdate: (callback: (room: PayloadOf) => void) => void; + offRoomUpdate: (callback: (room: PayloadOf) => void) => void; startGame: () => void; }; export const createLobbyHandler = (socket: Socket): LobbyHandler => { + const { onEvent, onceEvent, offEvent, emitEvent } = createClientSocketEventBridge(socket); + return { - onRoomUpdate: (callback: (room: roomTypes.Room) => void) => { - socket.on(protocol.SocketEvents.ROOM_UPDATE, callback); + onRoomUpdate: (callback) => { + onEvent(protocol.SocketEvents.ROOM_UPDATE, callback); }, - offRoomUpdate: (callback: (room: roomTypes.Room) => void) => { - socket.off(protocol.SocketEvents.ROOM_UPDATE, callback); + onceRoomUpdate: (callback) => { + onceEvent(protocol.SocketEvents.ROOM_UPDATE, callback); + }, + offRoomUpdate: (callback) => { + offEvent(protocol.SocketEvents.ROOM_UPDATE, callback); }, startGame: () => { - socket.emit(protocol.SocketEvents.START_GAME); + emitEvent(protocol.SocketEvents.START_GAME); } }; }; diff --git a/apps/client/src/network/handlers/TitleHandler.ts b/apps/client/src/network/handlers/TitleHandler.ts index 3246817..04d3a91 100644 --- a/apps/client/src/network/handlers/TitleHandler.ts +++ b/apps/client/src/network/handlers/TitleHandler.ts @@ -1,23 +1,30 @@ import type { Socket } from "socket.io-client"; import { protocol } from "@repo/shared"; -import type { roomTypes } from "@repo/shared"; +import type { PayloadOf } from "@repo/shared"; +import { createClientSocketEventBridge } from "./socketEventBridge"; type TitleHandler = { - joinRoom: (payload: roomTypes.JoinRoomPayload) => void; - onJoinRejected: (callback: (payload: roomTypes.JoinRoomRejectedPayload) => void) => void; - offJoinRejected: (callback: (payload: roomTypes.JoinRoomRejectedPayload) => void) => void; + joinRoom: (payload: PayloadOf) => void; + onJoinRejected: (callback: (payload: PayloadOf) => void) => void; + onceJoinRejected: (callback: (payload: PayloadOf) => void) => void; + offJoinRejected: (callback: (payload: PayloadOf) => void) => void; }; export const createTitleHandler = (socket: Socket): TitleHandler => { + const { onEvent, onceEvent, offEvent, emitEvent } = createClientSocketEventBridge(socket); + return { - joinRoom: (payload: roomTypes.JoinRoomPayload) => { - socket.emit(protocol.SocketEvents.JOIN_ROOM, payload); + joinRoom: (payload) => { + emitEvent(protocol.SocketEvents.JOIN_ROOM, payload); }, - onJoinRejected: (callback: (payload: roomTypes.JoinRoomRejectedPayload) => void) => { - socket.on(protocol.SocketEvents.ROOM_JOIN_REJECTED, callback); + onJoinRejected: (callback) => { + onEvent(protocol.SocketEvents.ROOM_JOIN_REJECTED, callback); }, - offJoinRejected: (callback: (payload: roomTypes.JoinRoomRejectedPayload) => void) => { - socket.off(protocol.SocketEvents.ROOM_JOIN_REJECTED, callback); + onceJoinRejected: (callback) => { + onceEvent(protocol.SocketEvents.ROOM_JOIN_REJECTED, callback); + }, + offJoinRejected: (callback) => { + offEvent(protocol.SocketEvents.ROOM_JOIN_REJECTED, callback); } }; }; diff --git a/apps/client/src/network/handlers/socketEventBridge.ts b/apps/client/src/network/handlers/socketEventBridge.ts new file mode 100644 index 0000000..aead5bc --- /dev/null +++ b/apps/client/src/network/handlers/socketEventBridge.ts @@ -0,0 +1,45 @@ +import type { Socket } from "socket.io-client"; +import type { PayloadOf, SocketPayloadMap } from "@repo/shared"; + +type SocketEventName = keyof SocketPayloadMap; + +export const createClientSocketEventBridge = (socket: Socket) => { + const onEvent = ( + event: TEvent, + callback: (payload: PayloadOf) => void + ) => { + (socket as any).on(event, callback); + }; + + const onceEvent = ( + event: TEvent, + callback: (payload: PayloadOf) => void + ) => { + (socket as any).once(event, callback); + }; + + const offEvent = ( + event: TEvent, + callback: (payload: PayloadOf) => void + ) => { + (socket as any).off(event, callback); + }; + + function emitEvent(event: TEvent): void; + function emitEvent(event: TEvent, payload: PayloadOf): void; + function emitEvent(event: TEvent, payload?: PayloadOf): void { + if (payload === undefined) { + (socket as any).emit(event); + return; + } + + (socket as any).emit(event, payload); + } + + return { + onEvent, + onceEvent, + offEvent, + emitEvent, + }; +}; diff --git a/apps/client/src/scenes/game/application/GameNetworkSync.ts b/apps/client/src/scenes/game/application/GameNetworkSync.ts index 45b6e07..9a8a245 100644 --- a/apps/client/src/scenes/game/application/GameNetworkSync.ts +++ b/apps/client/src/scenes/game/application/GameNetworkSync.ts @@ -4,7 +4,14 @@ * プレイヤー生成更新削除とマップ更新購読を管理する */ import { Container } from "pixi.js"; -import type { playerTypes } from "@repo/shared"; +import type { + CurrentPlayersPayload, + GameStartPayload, + NewPlayerPayload, + RemovePlayerPayload, + UpdateMapCellsPayload, + UpdatePlayersPayload, +} from "@repo/shared"; import { socketManager } from "@client/network/SocketManager"; import { LocalPlayerController, RemotePlayerController } from "@client/scenes/game/entities/player/PlayerController"; import { GameMapController } from "@client/scenes/game/entities/map/GameMapController"; @@ -27,29 +34,28 @@ private onGameStart: (startTime: number) => void; private isBound = false; - private handleCurrentPlayers = (serverPlayers: playerTypes.PlayerData[] | Record) => { - const playersArray = (Array.isArray(serverPlayers) ? serverPlayers : Object.values(serverPlayers)) as playerTypes.PlayerData[]; - playersArray.forEach((p) => { + private handleCurrentPlayers = (serverPlayers: CurrentPlayersPayload) => { + serverPlayers.forEach((p) => { const playerController = p.id === this.myId ? new LocalPlayerController(p) : new RemotePlayerController(p); this.worldContainer.addChild(playerController.getDisplayObject()); this.players[p.id] = playerController; }); }; - private handleNewPlayer = (p: playerTypes.PlayerData) => { + private handleNewPlayer = (p: NewPlayerPayload) => { const playerController = new RemotePlayerController(p); this.worldContainer.addChild(playerController.getDisplayObject()); this.players[p.id] = playerController; }; - private handleGameStart = (data: { startTime: number }) => { + private handleGameStart = (data: GameStartPayload) => { if (data && data.startTime) { this.onGameStart(data.startTime); console.log(`[GameManager] ゲーム開始時刻同期完了: ${data.startTime}`); } }; - private handlePlayerUpdates = (changedPlayers: playerTypes.PlayerData[]) => { + private handlePlayerUpdates = (changedPlayers: UpdatePlayersPayload) => { // UPDATE_PLAYERS は差分のみ届くため,対象IDだけ上書き更新する changedPlayers.forEach((playerData) => { if (playerData.id === this.myId) return; @@ -61,7 +67,7 @@ }); }; - private handleRemovePlayer = (id: string) => { + private handleRemovePlayer = (id: RemovePlayerPayload) => { const target = this.players[id]; if (target) { this.worldContainer.removeChild(target.getDisplayObject()); @@ -70,7 +76,7 @@ } }; - private handleUpdateMapCells = (updates: Parameters[0]) => { + private handleUpdateMapCells = (updates: UpdateMapCellsPayload) => { this.gameMap.updateCells(updates); }; diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index 617d04b..5858507 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -2,7 +2,17 @@ * gameUseCasePorts * ゲーム系ユースケースが利用する入力ポートと出力ポートの契約を定義する */ -import type { gameTypes, gridMapTypes, playerTypes, roomTypes, UpdatePlayersPayload } from "@repo/shared"; +import type { + gameTypes, + playerTypes, + roomTypes, + CurrentPlayersPayload, + GameStartPayload, + PongPayload, + RemovePlayerPayload, + UpdateMapCellsPayload, + UpdatePlayersPayload, +} from "@repo/shared"; /** ゲーム開始ユースケースが利用するゲーム管理入力ポート */ export interface StartGamePort { @@ -45,18 +55,18 @@ /** ゲーム系ユースケースが利用する送信出力ポート */ export interface GameOutputPort { - publishPongToSocket(payload: { clientTime: number; serverTime: number }): void; + publishPongToSocket(payload: PongPayload): void; publishUpdatePlayersToRoom( roomId: roomTypes.Room["roomId"], players: UpdatePlayersPayload ): void; publishMapCellUpdatesToRoom( roomId: roomTypes.Room["roomId"], - cellUpdates: gridMapTypes.CellUpdate[] + cellUpdates: UpdateMapCellsPayload ): void; publishGameEndToRoom(roomId: roomTypes.Room["roomId"]): void; - publishGameStartToRoom(roomId: roomTypes.Room["roomId"], payload: { startTime: number }): void; - publishCurrentPlayersToSocket(players: playerTypes.PlayerData[]): void; - publishGameStartToSocket(payload: { startTime: number }): void; - publishPlayerRemovedToRoom(roomId: roomTypes.Room["roomId"], removedPlayerId: string): void; + publishGameStartToRoom(roomId: roomTypes.Room["roomId"], payload: GameStartPayload): void; + publishCurrentPlayersToSocket(players: CurrentPlayersPayload): void; + publishGameStartToSocket(payload: GameStartPayload): void; + publishPlayerRemovedToRoom(roomId: roomTypes.Room["roomId"], removedPlayerId: RemovePlayerPayload): void; } diff --git a/apps/server/src/domains/game/application/useCases/pingUseCase.ts b/apps/server/src/domains/game/application/useCases/pingUseCase.ts index c357d66..ef302f3 100644 --- a/apps/server/src/domains/game/application/useCases/pingUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/pingUseCase.ts @@ -2,10 +2,11 @@ * pingUseCase * PING受信時に時刻情報付きPONGを返して遅延計測を支援する */ +import type { PingPayload } from "@repo/shared"; import type { GameOutputPort } from "../ports/gameUseCasePorts"; type PingUseCaseParams = { - clientTime: number; + clientTime: PingPayload; output: Pick; }; diff --git a/apps/server/src/network/adapters/socketEmitters.ts b/apps/server/src/network/adapters/socketEmitters.ts index 4895b90..fd5be3b 100644 --- a/apps/server/src/network/adapters/socketEmitters.ts +++ b/apps/server/src/network/adapters/socketEmitters.ts @@ -3,13 +3,29 @@ * Socket.IOの送信処理を用途別に生成するアダプタ */ import { Server, Socket } from "socket.io"; +import type { PayloadOf, SocketPayloadMap } from "@repo/shared"; -type EmitPayload = unknown; +type SocketEventName = keyof SocketPayloadMap; + +type EmitToRoom = { + (roomId: string, event: TEvent): void; + (roomId: string, event: TEvent, payload: PayloadOf): void; +}; + +type EmitToSocket = { + (event: TEvent): void; + (event: TEvent, payload: PayloadOf): void; +}; + +type EmitToAll = { + (event: TEvent): void; + (event: TEvent, payload: PayloadOf): void; +}; const emitWithOptionalPayload = ( - emit: (event: string, payload?: EmitPayload) => void, - event: string, - payload?: EmitPayload + emit: (event: SocketEventName, payload?: unknown) => void, + event: SocketEventName, + payload?: unknown ) => { if (payload === undefined) { emit(event); @@ -20,22 +36,22 @@ }; /** ルーム単位の送信関数を生成する */ -export const createEmitToRoom = (io: Server) => { - return (roomId: string, event: string, payload?: EmitPayload) => { +export const createEmitToRoom = (io: Server): EmitToRoom => { + return (roomId: string, event: SocketEventName, payload?: unknown) => { emitWithOptionalPayload((eventName, body) => io.to(roomId).emit(eventName, body), event, payload); }; }; /** 単一ソケット向けの送信関数を生成する */ -export const createEmitToSocket = (socket: Socket) => { - return (event: string, payload?: EmitPayload) => { +export const createEmitToSocket = (socket: Socket): EmitToSocket => { + return (event: SocketEventName, payload?: unknown) => { emitWithOptionalPayload((eventName, body) => socket.emit(eventName, body), event, payload); }; }; /** 全接続向けの送信関数を生成する */ -export const createEmitToAll = (io: Server) => { - return (event: string, payload?: EmitPayload) => { +export const createEmitToAll = (io: Server): EmitToAll => { + return (event: SocketEventName, payload?: unknown) => { emitWithOptionalPayload((eventName, body) => io.emit(eventName, body), event, payload); }; }; diff --git a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts index 58310c4..a81719e 100644 --- a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts +++ b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts @@ -4,17 +4,20 @@ */ import { Server } from "socket.io"; import { protocol } from "@repo/shared"; -import type { gridMapTypes, playerTypes, roomTypes, UpdatePlayersPayload } from "@repo/shared"; +import type { + GameStartPayload, + PongPayload, + roomTypes, + CurrentPlayersPayload, + RemovePlayerPayload, + UpdateMapCellsPayload, + UpdatePlayersPayload, +} from "@repo/shared"; import type { GameOutputPort } from "@server/domains/game/application/ports/gameUseCasePorts"; import { createEmitToRoom } 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 MapCellUpdatesPayload = gridMapTypes.CellUpdate[]; /** ゲーム出力アダプターのインターフェース */ export type GameOutputAdapter = Omit; @@ -31,7 +34,7 @@ publishUpdatePlayersToRoom: (roomId: RoomId, players: UpdatePlayersPayload) => { common.emitToRoom(roomId, protocol.SocketEvents.UPDATE_PLAYERS, players); }, - publishMapCellUpdatesToRoom: (roomId: RoomId, cellUpdates: MapCellUpdatesPayload) => { + publishMapCellUpdatesToRoom: (roomId: RoomId, cellUpdates: UpdateMapCellsPayload) => { common.emitToRoom(roomId, protocol.SocketEvents.UPDATE_MAP_CELLS, cellUpdates); }, publishGameEndToRoom: (roomId: RoomId) => { @@ -54,7 +57,7 @@ const emitToRoom = createEmitToRoom(io); return { - publishPlayerRemovedToRoom: (roomId: RoomId, removedPlayerId: SocketId) => { + publishPlayerRemovedToRoom: (roomId: RoomId, removedPlayerId: RemovePlayerPayload) => { emitToRoom(roomId, protocol.SocketEvents.REMOVE_PLAYER, removedPlayerId); }, }; diff --git a/apps/server/src/network/handlers/game/registerGameHandlers.ts b/apps/server/src/network/handlers/game/registerGameHandlers.ts index 8b6ada2..5b1b631 100644 --- a/apps/server/src/network/handlers/game/registerGameHandlers.ts +++ b/apps/server/src/network/handlers/game/registerGameHandlers.ts @@ -18,6 +18,7 @@ import { logEvent } from "@server/logging/logEvent"; import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; import { isMovePayload, isPingPayload } from "@server/network/validation/socketPayloadValidators"; +import { createServerSocketOnBridge } from "@server/network/handlers/socketEventBridge"; import { createGameOutputAdapter } from "./createGameOutputAdapter"; /** ゲームイベントの購読とユースケース呼び出しを設定する */ @@ -29,9 +30,10 @@ ) => { const common = createCommonHandlerContext(io, socket); const gameOutputAdapter = createGameOutputAdapter(common); + const { onEvent } = createServerSocketOnBridge(socket); // 遅延計測用のPINGを検証しPONGを返す - socket.on(protocol.SocketEvents.PING, (clientTime: unknown) => { + onEvent(protocol.SocketEvents.PING, (clientTime) => { if (!isPingPayload(clientTime)) { logEvent("Network", { event: "PING", @@ -48,7 +50,7 @@ }); // オーナー開始要求に応じてゲーム進行ユースケースを起動する - socket.on(protocol.SocketEvents.START_GAME, () => { + onEvent(protocol.SocketEvents.START_GAME, () => { startGameCoordinator({ ownerId: socket.id, gameManager, @@ -58,7 +60,7 @@ }); // 参加者の準備完了通知を受けて現在状態を返す - socket.on(protocol.SocketEvents.READY_FOR_GAME, () => { + onEvent(protocol.SocketEvents.READY_FOR_GAME, () => { readyForGameCoordinator({ socketId: socket.id, gameManager, @@ -68,7 +70,7 @@ }); // 移動入力を検証しプレイヤー移動ユースケースへ連携する - socket.on(protocol.SocketEvents.MOVE, (data: unknown) => { + onEvent(protocol.SocketEvents.MOVE, (data) => { if (!isMovePayload(data)) { logEvent("Network", { event: "MOVE", diff --git a/apps/server/src/network/handlers/room/registerRoomHandlers.ts b/apps/server/src/network/handlers/room/registerRoomHandlers.ts index c4b42b9..e799931 100644 --- a/apps/server/src/network/handlers/room/registerRoomHandlers.ts +++ b/apps/server/src/network/handlers/room/registerRoomHandlers.ts @@ -8,6 +8,7 @@ import { joinRoomUseCase } from "@server/domains/room/application/useCases/joinRoomUseCase"; import { logEvent } from "@server/logging/logEvent"; import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; +import { createServerSocketOnBridge } from "@server/network/handlers/socketEventBridge"; import { isJoinRoomPayload } from "@server/network/validation/socketPayloadValidators"; import { createRoomOutputAdapter } from "./createRoomOutputAdapter"; @@ -19,9 +20,10 @@ ) => { const common = createCommonHandlerContext(io, socket); const roomOutputAdapter = createRoomOutputAdapter(common); + const { onEvent } = createServerSocketOnBridge(socket); // 参加要求のペイロード検証と参加処理を実行する - socket.on(protocol.SocketEvents.JOIN_ROOM, async (data: unknown) => { + onEvent(protocol.SocketEvents.JOIN_ROOM, async (data) => { if (!isJoinRoomPayload(data)) { logEvent("Network", { event: "JOIN_ROOM", diff --git a/apps/server/src/network/handlers/socketEventBridge.ts b/apps/server/src/network/handlers/socketEventBridge.ts new file mode 100644 index 0000000..1db3c62 --- /dev/null +++ b/apps/server/src/network/handlers/socketEventBridge.ts @@ -0,0 +1,25 @@ +import type { Socket } from "socket.io"; +import type { PayloadOf, SocketPayloadMap } from "@repo/shared"; + +type SocketEventName = Exclude; + +export const createServerSocketOnBridge = (socket: Socket) => { + const onEvent = ( + event: TEvent, + callback: (payload: PayloadOf) => void + ) => { + (socket as any).on(event, callback); + }; + + const onceEvent = ( + event: TEvent, + callback: (payload: PayloadOf) => void + ) => { + (socket as any).once(event, callback); + }; + + return { + onEvent, + onceEvent, + }; +}; diff --git a/apps/server/src/network/validation/socketPayloadValidators.ts b/apps/server/src/network/validation/socketPayloadValidators.ts index beba0ee..0345b37 100644 --- a/apps/server/src/network/validation/socketPayloadValidators.ts +++ b/apps/server/src/network/validation/socketPayloadValidators.ts @@ -3,6 +3,7 @@ * ソケット受信ペイロードの型ガードを提供する */ import type { playerTypes, roomTypes } from "@repo/shared"; +import type { PingPayload } from "@repo/shared"; const isFiniteNumber = (value: unknown): value is number => { return typeof value === "number" && Number.isFinite(value); @@ -13,7 +14,7 @@ }; /** PINGイベントのペイロードが数値であるか判定する */ -export const isPingPayload = (value: unknown): value is number => { +export const isPingPayload = (value: unknown): value is PingPayload => { return isFiniteNumber(value); }; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 48c6e17..14354f3 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -8,5 +8,17 @@ export * as roomTypes from "./domains/room/room.type"; export * as roomConsts from "./domains/room/room.const"; export * as protocol from "./protocol/events"; -export type { UpdatePlayersPayload } from "./protocol/events"; +export type { + CurrentPlayersPayload, + GameStartPayload, + MovePayload, + NewPlayerPayload, + PayloadOf, + PingPayload, + PongPayload, + RemovePlayerPayload, + SocketPayloadMap, + UpdateMapCellsPayload, + UpdatePlayersPayload, +} from "./protocol/events"; export * as config from "./config"; \ No newline at end of file diff --git a/packages/shared/src/protocol/events.ts b/packages/shared/src/protocol/events.ts index 28a710f..a0e3939 100644 --- a/packages/shared/src/protocol/events.ts +++ b/packages/shared/src/protocol/events.ts @@ -4,6 +4,8 @@ * クライアントとサーバー間のイベント契約を共有する */ import type { TickData } from "../domains/game/game.type"; +import type { MovePayload as PlayerMovePayload, PlayerData } from "../domains/player/player.type"; +import type * as roomTypes from "../domains/room/room.type"; /** ソケットイベント名の一覧定数 */ export const SocketEvents = { @@ -36,3 +38,54 @@ /** UPDATE_PLAYERS イベントで送受信するプレイヤー差分配列 */ export type UpdatePlayersPayload = TickData["playerUpdates"]; + +/** CURRENT_PLAYERS イベントで送受信するプレイヤー一覧 */ +export type CurrentPlayersPayload = TickData["playerUpdates"]; + +/** UPDATE_MAP_CELLS イベントで送受信するマップ差分配列 */ +export type UpdateMapCellsPayload = TickData["cellUpdates"]; + +/** NEW_PLAYER イベントで送受信するプレイヤー情報 */ +export type NewPlayerPayload = PlayerData; + +/** REMOVE_PLAYER イベントで送受信するプレイヤーID */ +export type RemovePlayerPayload = PlayerData["id"]; + +/** GAME_START イベントで送受信するゲーム開始情報 */ +export type GameStartPayload = { startTime: number }; + +/** MOVE イベントで送受信する移動入力情報 */ +export type MovePayload = PlayerMovePayload; + +/** PING イベントで送受信する時刻同期リクエスト */ +export type PingPayload = number; + +/** PONG イベントで送受信する時刻同期レスポンス */ +export type PongPayload = { + clientTime: number; + serverTime: number; +}; + +/** ソケットイベントごとのペイロード対応表 */ +export type SocketPayloadMap = { + [SocketEvents.CONNECT]: undefined; + [SocketEvents.DISCONNECT]: undefined; + [SocketEvents.JOIN_ROOM]: roomTypes.JoinRoomPayload; + [SocketEvents.ROOM_JOIN_REJECTED]: roomTypes.JoinRoomRejectedPayload; + [SocketEvents.ROOM_UPDATE]: roomTypes.Room; + [SocketEvents.START_GAME]: undefined; + [SocketEvents.GAME_START]: GameStartPayload; + [SocketEvents.READY_FOR_GAME]: undefined; + [SocketEvents.CURRENT_PLAYERS]: CurrentPlayersPayload; + [SocketEvents.NEW_PLAYER]: NewPlayerPayload; + [SocketEvents.UPDATE_PLAYERS]: UpdatePlayersPayload; + [SocketEvents.REMOVE_PLAYER]: RemovePlayerPayload; + [SocketEvents.MOVE]: MovePayload; + [SocketEvents.UPDATE_MAP_CELLS]: UpdateMapCellsPayload; + [SocketEvents.PING]: PingPayload; + [SocketEvents.PONG]: PongPayload; + [SocketEvents.GAME_END]: undefined; +}; + +/** 指定イベント名に対応するペイロード型を取得するユーティリティ */ +export type PayloadOf = SocketPayloadMap[TEvent];