diff --git a/apps/server/src/domains/game/GameManager.ts b/apps/server/src/domains/game/GameManager.ts index af223a0..dc3ae97 100644 --- a/apps/server/src/domains/game/GameManager.ts +++ b/apps/server/src/domains/game/GameManager.ts @@ -120,8 +120,8 @@ } /** 現在アクティブな爆弾一覧を返す */ - getActiveBombs(): ActiveBombSnapshot[] { - return this.lifecycleService.getActiveBombs(); + getActiveBombSnapshots(): ActiveBombSnapshot[] { + return this.lifecycleService.getActiveBombSnapshots(); } dispose(): void { diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index db39eae..e053f6f 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -99,7 +99,7 @@ export interface BombPlacementOutputPort { publishBombPlacedToOthersInRoom( roomId: domain.room.Room["roomId"], - ownerSocketId: string, + excludedSocketId: string, payload: BombPlacedPayload, ): void; publishBombPlacedAckToSocket( @@ -168,7 +168,7 @@ /** アクティブ爆弾一覧を参照する入力ポート */ export interface ActiveBombQueryPort { - getActiveBombs(): ActiveBombSnapshot[]; + getActiveBombSnapshots(): ActiveBombSnapshot[]; } /** 被弾報告ユースケースが利用する重複排除入力ポート */ diff --git a/apps/server/src/domains/game/application/services/GameRoomSession.ts b/apps/server/src/domains/game/application/services/GameRoomSession.ts index 34995d7..a6ae809 100644 --- a/apps/server/src/domains/game/application/services/GameRoomSession.ts +++ b/apps/server/src/domains/game/application/services/GameRoomSession.ts @@ -249,8 +249,8 @@ } /** 現在アクティブな爆弾一覧を返す */ - public getActiveBombs(): ActiveBombSnapshot[] { - return this.bombStateStore.activeBombRegistry.getBombsSnapshot().map((bomb) => { + public getActiveBombSnapshots(): ActiveBombSnapshot[] { + return this.bombStateStore.activeBombRegistry.getActiveBombSnapshots().map((bomb) => { return { bombId: bomb.bombId, ownerPlayerId: bomb.ownerPlayerId, diff --git a/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts b/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts index a5317ae..7536e13 100644 --- a/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts +++ b/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts @@ -85,8 +85,8 @@ } /** 現在アクティブな爆弾一覧を返す */ - public getActiveBombs(): ActiveBombSnapshot[] { - return this.sessionRef.current?.getActiveBombs() ?? []; + public getActiveBombSnapshots(): ActiveBombSnapshot[] { + return this.sessionRef.current?.getActiveBombSnapshots() ?? []; } public startRoomSession( diff --git a/apps/server/src/domains/game/entities/bomb/ActiveBombRegistry.ts b/apps/server/src/domains/game/entities/bomb/ActiveBombRegistry.ts index 0d42465..e9fc150 100644 --- a/apps/server/src/domains/game/entities/bomb/ActiveBombRegistry.ts +++ b/apps/server/src/domains/game/entities/bomb/ActiveBombRegistry.ts @@ -43,7 +43,7 @@ } /** 現在アクティブな爆弾のスナップショットを返す */ - public getBombsSnapshot(): ActiveBomb[] { + public getActiveBombSnapshots(): ActiveBomb[] { return Array.from(this.bombs.values()); } } diff --git a/apps/server/src/network/adapters/realtimeRoomSyncState.ts b/apps/server/src/network/adapters/realtimeRoomSyncState.ts index ef7b6b2..bb6fb40 100644 --- a/apps/server/src/network/adapters/realtimeRoomSyncState.ts +++ b/apps/server/src/network/adapters/realtimeRoomSyncState.ts @@ -33,9 +33,24 @@ ) => RoomPlayerPositionCache; getLastAoiCell: (roomId: RoomId, socketId: SocketId) => SocketAoiCell | undefined; setLastAoiCell: (roomId: RoomId, socketId: SocketId, cell: SocketAoiCell) => void; - getVisiblePlayerIds: (roomId: RoomId, socketId: SocketId) => Set; - getVisibleBombIds: (roomId: RoomId, socketId: SocketId) => Set; - getVisibleHurricaneIds: (roomId: RoomId, socketId: SocketId) => Set; + getVisiblePlayerIdsSnapshot: (roomId: RoomId, socketId: SocketId) => Set; + getVisibleBombIdsSnapshot: (roomId: RoomId, socketId: SocketId) => Set; + getVisibleHurricaneIdsSnapshot: (roomId: RoomId, socketId: SocketId) => Set; + replaceVisiblePlayerIds: ( + roomId: RoomId, + socketId: SocketId, + nextIds: Iterable, + ) => void; + replaceVisibleBombIds: ( + roomId: RoomId, + socketId: SocketId, + nextIds: Iterable, + ) => void; + replaceVisibleHurricaneIds: ( + roomId: RoomId, + socketId: SocketId, + nextIds: Iterable, + ) => void; resetRoom: (roomId: RoomId) => void; }; @@ -86,29 +101,68 @@ aoiCellCache.set(socketId, cell); aoiCellCacheByRoomId.set(roomId, aoiCellCache); }, - getVisiblePlayerIds: (roomId, socketId) => { - return getOrCreateSocketScopedCache( + getVisiblePlayerIdsSnapshot: (roomId, socketId) => { + const cache = getOrCreateSocketScopedCache( visiblePlayerIdsByRoomId, roomId, socketId, () => new Set(), ); + return new Set(cache); }, - getVisibleBombIds: (roomId, socketId) => { - return getOrCreateSocketScopedCache( + getVisibleBombIdsSnapshot: (roomId, socketId) => { + const cache = getOrCreateSocketScopedCache( visibleBombIdsByRoomId, roomId, socketId, () => new Set(), ); + return new Set(cache); }, - getVisibleHurricaneIds: (roomId, socketId) => { - return getOrCreateSocketScopedCache( + getVisibleHurricaneIdsSnapshot: (roomId, socketId) => { + const cache = getOrCreateSocketScopedCache( visibleHurricaneIdsByRoomId, roomId, socketId, () => new Set(), ); + return new Set(cache); + }, + replaceVisiblePlayerIds: (roomId, socketId, nextIds) => { + const cache = getOrCreateSocketScopedCache( + visiblePlayerIdsByRoomId, + roomId, + socketId, + () => new Set(), + ); + cache.clear(); + for (const nextId of nextIds) { + cache.add(nextId); + } + }, + replaceVisibleBombIds: (roomId, socketId, nextIds) => { + const cache = getOrCreateSocketScopedCache( + visibleBombIdsByRoomId, + roomId, + socketId, + () => new Set(), + ); + cache.clear(); + for (const nextId of nextIds) { + cache.add(nextId); + } + }, + replaceVisibleHurricaneIds: (roomId, socketId, nextIds) => { + const cache = getOrCreateSocketScopedCache( + visibleHurricaneIdsByRoomId, + roomId, + socketId, + () => new Set(), + ); + cache.clear(); + for (const nextId of nextIds) { + cache.add(nextId); + } }, resetRoom: (roomId) => { playerPositionCacheByRoomId.delete(roomId); diff --git a/apps/server/src/network/handlers/game/aoi/aoiVisibility.ts b/apps/server/src/network/handlers/game/aoi/aoiVisibility.ts new file mode 100644 index 0000000..c6c1897 --- /dev/null +++ b/apps/server/src/network/handlers/game/aoi/aoiVisibility.ts @@ -0,0 +1,55 @@ +/** + * aoiVisibility + * ゲーム送信で利用するAOI可視判定ロジックを提供する + * 受信者視点のAOI窓計算と対象可視判定を共通化する + */ +import { domain } from "@repo/shared"; +import { config } from "@server/config"; + +/** AOI窓の境界型 */ +export type AoiWindow = domain.game.aoi.AoiWindow; + +/** グリッド座標を持つ可視判定対象型 */ +export type AoiTarget = { + x: number; + y: number; +}; + +/** 受信者座標からAOI窓を解決する */ +export const resolveViewerAoiWindow = ( + viewer: AoiTarget, +): AoiWindow => { + const centerCell = domain.game.aoi.resolveAoiCellFromPosition( + viewer.x, + viewer.y, + config.GAME_CONFIG.AOI_CELL_SIZE, + ); + + return domain.game.aoi.resolveAoiWindowFromCell( + centerCell, + config.GAME_CONFIG.AOI_WINDOW_COLS, + config.GAME_CONFIG.AOI_WINDOW_ROWS, + ); +}; + +/** 対象座標がAOI窓に含まれるか判定する */ +export const isTargetInAoiWindow = ( + target: AoiTarget, + aoiWindow: AoiWindow, +): boolean => { + return domain.game.aoi.isPositionInAoiWindow( + target.x, + target.y, + aoiWindow, + config.GAME_CONFIG.AOI_CELL_SIZE, + ); +}; + +/** 受信者座標からAOIセルを解決する */ +export const resolveViewerAoiCell = (viewer: AoiTarget): domain.game.aoi.AoiCell => { + return domain.game.aoi.resolveAoiCellFromPosition( + viewer.x, + viewer.y, + config.GAME_CONFIG.AOI_CELL_SIZE, + ); +}; diff --git a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts index 7246ad6..e45b0dc 100644 --- a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts +++ b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts @@ -34,11 +34,22 @@ import { createRealtimeRoomSyncStateStore } from "@server/network/adapters/realtimeRoomSyncState"; import { createEmitToRoom } from "@server/network/adapters/socketEmitters"; import type { CommonHandlerContext } from "../CommonHandler"; -import { config } from "@server/config"; import type { FindGameByRoomPort, FindRoomByIdPort, } from "@server/domains/room/application/ports/roomUseCasePorts"; +import { + isTargetInAoiWindow, + resolveViewerAoiCell, + resolveViewerAoiWindow, + type AoiWindow, +} from "./aoi/aoiVisibility"; +import { + getActiveBombSnapshotsInRoom, + getConnectedSocketIdsInRoom, + getRoomPlayers, + type RuntimeResolverDeps, +} from "./runtime/gameRuntimeResolvers"; type RoomId = domain.room.Room["roomId"]; type SocketId = string; @@ -70,10 +81,7 @@ /** 共通送信コンテキストからゲーム出力アダプターを生成する */ export const createGameOutputAdapter = ( common: CommonHandlerContext, - deps: { - roomManager: FindRoomByIdPort; - runtimeRegistry: FindGameByRoomPort; - }, + deps: RuntimeResolverDeps, ): GameOutputAdapter => { const { reliable } = common; const { roomManager, runtimeRegistry } = deps; @@ -83,61 +91,19 @@ Map >(); - const getConnectedSocketIdsInRoom = (roomId: RoomId): SocketId[] => { - const room = roomManager.getRoomById(roomId); - if (!room) { - return []; - } - - return room.players - .map((player) => player.id) - .filter((playerId) => !isBotPlayerId(playerId)); - }; - const getPlayersInRoom = (roomId: RoomId): domain.game.player.PlayerData[] => { - const gameManager = runtimeRegistry.getGameManagerByRoomId(roomId); - if (!gameManager) { - return []; - } - - return gameManager.getRoomPlayers(); + return getRoomPlayers(deps, roomId); }; const getActiveBombsInRoom = (roomId: RoomId): ActiveBombSnapshot[] => { - const gameManager = runtimeRegistry.getGameManagerByRoomId(roomId); - if (!gameManager) { - return []; - } - - return gameManager.getActiveBombs(); - }; - - const getAoiWindowForViewer = ( - viewer: domain.game.player.PlayerData, - ): { minCol: number; maxCol: number; minRow: number; maxRow: number } => { - const centerCell = domainNs.game.aoi.resolveAoiCellFromPosition( - viewer.x, - viewer.y, - config.GAME_CONFIG.AOI_CELL_SIZE, - ); - - return domainNs.game.aoi.resolveAoiWindowFromCell( - centerCell, - config.GAME_CONFIG.AOI_WINDOW_COLS, - config.GAME_CONFIG.AOI_WINDOW_ROWS, - ); + return getActiveBombSnapshotsInRoom(deps, roomId); }; const isInViewerAoi = ( target: { x: number; y: number }, - aoiWindow: { minCol: number; maxCol: number; minRow: number; maxRow: number }, + aoiWindow: AoiWindow, ): boolean => { - return domainNs.game.aoi.isPositionInAoiWindow( - target.x, - target.y, - aoiWindow, - config.GAME_CONFIG.AOI_CELL_SIZE, - ); + return isTargetInAoiWindow(target, aoiWindow); }; const updateViewerAoiCellCache = ( @@ -145,11 +111,7 @@ viewerId: SocketId, viewer: domain.game.player.PlayerData, ): void => { - const nextCell = domainNs.game.aoi.resolveAoiCellFromPosition( - viewer.x, - viewer.y, - config.GAME_CONFIG.AOI_CELL_SIZE, - ); + const nextCell = resolveViewerAoiCell(viewer); const previousCell = realtimeRoomSyncState.getLastAoiCell(roomId, viewerId); if (previousCell && domainNs.game.aoi.isSameAoiCell(previousCell, nextCell)) { @@ -164,7 +126,10 @@ viewerId: SocketId, visiblePlayers: domain.game.player.PlayerData[], ): void => { - const visibleIdsCache = realtimeRoomSyncState.getVisiblePlayerIds(roomId, viewerId); + const previousVisibleIds = realtimeRoomSyncState.getVisiblePlayerIdsSnapshot( + roomId, + viewerId, + ); const viewerPositionCache = realtimeRoomSyncState.getPlayerPositionCache( roomId, viewerId, @@ -172,12 +137,12 @@ const nextVisibleIds = new Set(visiblePlayers.map((player) => player.id)); visiblePlayers.forEach((player) => { - if (!visibleIdsCache.has(player.id)) { + if (!previousVisibleIds.has(player.id)) { reliable.emitToSocketById(viewerId, protocol.SocketEvents.NEW_PLAYER, player); } }); - visibleIdsCache.forEach((playerId) => { + previousVisibleIds.forEach((playerId) => { if (nextVisibleIds.has(playerId)) { return; } @@ -186,10 +151,7 @@ reliable.emitToSocketById(viewerId, protocol.SocketEvents.REMOVE_PLAYER, playerId); }); - visibleIdsCache.clear(); - nextVisibleIds.forEach((playerId) => { - visibleIdsCache.add(playerId); - }); + realtimeRoomSyncState.replaceVisiblePlayerIds(roomId, viewerId, nextVisibleIds); }; const syncVisibleBombsByViewer = ( @@ -199,8 +161,11 @@ bombs: ActiveBombSnapshot[], ): void => { updateViewerAoiCellCache(roomId, viewerId, viewer); - const aoiWindow = getAoiWindowForViewer(viewer); - const visibleBombIds = realtimeRoomSyncState.getVisibleBombIds(roomId, viewerId); + const aoiWindow = resolveViewerAoiWindow(viewer); + const previousVisibleBombIds = realtimeRoomSyncState.getVisibleBombIdsSnapshot( + roomId, + viewerId, + ); const nextVisibleBombIds = new Set(); bombs.forEach((bomb) => { @@ -209,7 +174,7 @@ } nextVisibleBombIds.add(bomb.bombId); - if (visibleBombIds.has(bomb.bombId)) { + if (previousVisibleBombIds.has(bomb.bombId)) { return; } @@ -227,10 +192,11 @@ reliable.emitToSocketById(viewerId, protocol.SocketEvents.BOMB_PLACED, payload); }); - visibleBombIds.clear(); - nextVisibleBombIds.forEach((bombId) => { - visibleBombIds.add(bombId); - }); + realtimeRoomSyncState.replaceVisibleBombIds( + roomId, + viewerId, + nextVisibleBombIds, + ); }; const replaceRoomHurricaneSnapshot = ( @@ -262,7 +228,7 @@ hurricanes: Iterable, ): HurricaneStatePayload[] => { updateViewerAoiCellCache(roomId, viewerId, viewer); - const aoiWindow = getAoiWindowForViewer(viewer); + const aoiWindow = resolveViewerAoiWindow(viewer); const visibleHurricanes: HurricaneStatePayload[] = []; for (const hurricane of hurricanes) { @@ -279,14 +245,12 @@ viewerId: SocketId, hurricanes: HurricaneStatePayload[], ): void => { - const visibleIdsCache = realtimeRoomSyncState.getVisibleHurricaneIds( + const nextVisibleIds = hurricanes.map((hurricane) => hurricane.id); + realtimeRoomSyncState.replaceVisibleHurricaneIds( roomId, viewerId, + nextVisibleIds, ); - visibleIdsCache.clear(); - hurricanes.forEach((hurricane) => { - visibleIdsCache.add(hurricane.id); - }); }; const hasChangedVisibleHurricaneIds = ( @@ -335,7 +299,7 @@ const quantizedPlayers = quantizeUpdatePlayersPayload(players); const roomPlayerById = new Map(roomPlayers.map((player) => [player.id, player])); - const recipientSocketIds = getConnectedSocketIdsInRoom(roomId); + const recipientSocketIds = getConnectedSocketIdsInRoom(deps, roomId); recipientSocketIds.forEach((viewerId) => { const viewer = roomPlayerById.get(viewerId); @@ -346,7 +310,7 @@ syncVisibleBombsByViewer(roomId, viewerId, viewer, activeBombs); updateViewerAoiCellCache(roomId, viewerId, viewer); - const aoiWindow = getAoiWindowForViewer(viewer); + const aoiWindow = resolveViewerAoiWindow(viewer); const visibleSnapshotPlayers = roomPlayers.filter((player) => { if (player.id === viewerId) { @@ -405,7 +369,7 @@ } const roomPlayerById = new Map(roomPlayers.map((player) => [player.id, player])); - const recipientSocketIds = getConnectedSocketIdsInRoom(roomId); + const recipientSocketIds = getConnectedSocketIdsInRoom(deps, roomId); recipientSocketIds.forEach((viewerId) => { const viewer = roomPlayerById.get(viewerId); @@ -440,7 +404,7 @@ } const roomPlayerById = new Map(roomPlayers.map((player) => [player.id, player])); - const recipientSocketIds = getConnectedSocketIdsInRoom(roomId); + const recipientSocketIds = getConnectedSocketIdsInRoom(deps, roomId); const roomSnapshot = hurricaneSnapshotByRoomId.get(roomId); recipientSocketIds.forEach((viewerId) => { @@ -456,7 +420,7 @@ roomSnapshot?.values() ?? [], ); const previousVisibleIds = new Set( - realtimeRoomSyncState.getVisibleHurricaneIds(roomId, viewerId), + realtimeRoomSyncState.getVisibleHurricaneIdsSnapshot(roomId, viewerId), ); const hasMembershipChanged = hasChangedVisibleHurricaneIds( previousVisibleIds, @@ -510,7 +474,7 @@ }, publishBombPlacedToOthersInRoom: ( roomId: RoomId, - ownerSocketId: string, + excludedSocketId: string, payload: BombPlacedPayload, ) => { const roomPlayers = getPlayersInRoom(roomId); @@ -519,10 +483,10 @@ } const roomPlayerById = new Map(roomPlayers.map((player) => [player.id, player])); - const recipientSocketIds = getConnectedSocketIdsInRoom(roomId); + const recipientSocketIds = getConnectedSocketIdsInRoom(deps, roomId); recipientSocketIds.forEach((viewerId) => { - if (viewerId === ownerSocketId && !isBotPlayerId(ownerSocketId)) { + if (viewerId === excludedSocketId && !isBotPlayerId(excludedSocketId)) { return; } @@ -532,7 +496,7 @@ } updateViewerAoiCellCache(roomId, viewerId, viewer); - const aoiWindow = getAoiWindowForViewer(viewer); + const aoiWindow = resolveViewerAoiWindow(viewer); if (!isInViewerAoi(payload, aoiWindow)) { return; } diff --git a/apps/server/src/network/handlers/game/runtime/gameRuntimeResolvers.ts b/apps/server/src/network/handlers/game/runtime/gameRuntimeResolvers.ts new file mode 100644 index 0000000..65df557 --- /dev/null +++ b/apps/server/src/network/handlers/game/runtime/gameRuntimeResolvers.ts @@ -0,0 +1,59 @@ +/** + * gameRuntimeResolvers + * ゲーム送信処理で利用するルーム/ランタイム参照処理を提供する + * 受信者ソケット一覧,プレイヤー一覧,アクティブ爆弾一覧の解決を集約する + */ +import type { domain } from "@repo/shared"; +import type { ActiveBombSnapshot } from "@server/domains/game/application/ports/gameUseCasePorts"; +import { isBotPlayerId } from "@server/domains/game/application/services/bot/index.js"; +import type { + FindGameByRoomPort, + FindRoomByIdPort, +} from "@server/domains/room/application/ports/roomUseCasePorts"; + +/** ランタイム参照処理で利用する依存型 */ +export type RuntimeResolverDeps = { + roomManager: FindRoomByIdPort; + runtimeRegistry: FindGameByRoomPort; +}; + +/** ルームの接続済み受信者ソケットID一覧を返す */ +export const getConnectedSocketIdsInRoom = ( + deps: RuntimeResolverDeps, + roomId: domain.room.Room["roomId"], +): string[] => { + const room = deps.roomManager.getRoomById(roomId); + if (!room) { + return []; + } + + return room.players + .map((player) => player.id) + .filter((playerId) => !isBotPlayerId(playerId)); +}; + +/** ルーム内プレイヤー一覧を返す */ +export const getRoomPlayers = ( + deps: RuntimeResolverDeps, + roomId: domain.room.Room["roomId"], +): domain.game.player.PlayerData[] => { + const gameManager = deps.runtimeRegistry.getGameManagerByRoomId(roomId); + if (!gameManager) { + return []; + } + + return gameManager.getRoomPlayers(); +}; + +/** ルーム内アクティブ爆弾スナップショット一覧を返す */ +export const getActiveBombSnapshotsInRoom = ( + deps: RuntimeResolverDeps, + roomId: domain.room.Room["roomId"], +): ActiveBombSnapshot[] => { + const gameManager = deps.runtimeRegistry.getGameManagerByRoomId(roomId); + if (!gameManager) { + return []; + } + + return gameManager.getActiveBombSnapshots(); +};