/**
* hurricaneSyncService
* ハリケーンのAOI同期送信を提供する
* 受信者ごとの可視集合差分とルーム内最新スナップショットを管理する
*/
import { contracts as protocol, domain } from "@repo/shared";
import type {
CurrentHurricanesPayload,
HurricaneStatePayload,
UpdateHurricanesPayload,
} from "@repo/shared";
import type { RealtimeRoomSyncStateStore } from "@server/network/adapters/realtimeRoomSyncState";
import type { ReliableEmitters } from "../../CommonHandler";
import { isTargetInAoiWindow, resolveViewerAoiWindow, type AoiWindow } from "../aoi/aoiVisibility";
import {
getConnectedSocketIdsInRoom,
getRoomPlayers,
type RuntimeResolverDeps,
} from "../runtime/gameRuntimeResolvers";
type RoomId = domain.room.Room["roomId"];
type SocketId = string;
/** ハリケーン同期サービスが提供する操作契約 */
export type HurricaneSyncService = {
publishCurrentHurricanesToRoom: (
roomId: RoomId,
hurricanes: CurrentHurricanesPayload,
) => void;
publishUpdateHurricanesToRoom: (
roomId: RoomId,
hurricanes: UpdateHurricanesPayload,
) => void;
clearRoomSnapshot: (roomId: RoomId) => void;
};
/** ハリケーン同期サービス生成時の依存集合 */
export type CreateHurricaneSyncServiceDeps = {
reliable: ReliableEmitters;
runtimeDeps: RuntimeResolverDeps;
realtimeRoomSyncState: RealtimeRoomSyncStateStore;
updateViewerAoiCellCache: (
roomId: RoomId,
viewerId: SocketId,
viewer: domain.game.player.PlayerData,
) => void;
};
/** ハリケーンAOI同期サービスを生成する */
export const createHurricaneSyncService = (
deps: CreateHurricaneSyncServiceDeps,
): HurricaneSyncService => {
const hurricaneSnapshotByRoomId = new Map<RoomId, Map<string, HurricaneStatePayload>>();
const isInViewerAoi = (
target: { x: number; y: number },
aoiWindow: AoiWindow,
): boolean => {
return isTargetInAoiWindow(target, aoiWindow);
};
const replaceRoomHurricaneSnapshot = (
roomId: RoomId,
hurricanes: HurricaneStatePayload[],
): void => {
const snapshotMap = new Map<string, HurricaneStatePayload>();
hurricanes.forEach((hurricane) => {
snapshotMap.set(hurricane.id, hurricane);
});
hurricaneSnapshotByRoomId.set(roomId, snapshotMap);
};
const upsertRoomHurricaneSnapshot = (
roomId: RoomId,
hurricanes: HurricaneStatePayload[],
): void => {
const snapshotMap = hurricaneSnapshotByRoomId.get(roomId) ?? new Map();
hurricanes.forEach((hurricane) => {
snapshotMap.set(hurricane.id, hurricane);
});
hurricaneSnapshotByRoomId.set(roomId, snapshotMap);
};
const collectVisibleHurricanesByViewer = (
roomId: RoomId,
viewerId: SocketId,
viewer: domain.game.player.PlayerData,
hurricanes: Iterable<HurricaneStatePayload>,
): HurricaneStatePayload[] => {
deps.updateViewerAoiCellCache(roomId, viewerId, viewer);
const aoiWindow = resolveViewerAoiWindow(viewer);
const visibleHurricanes: HurricaneStatePayload[] = [];
for (const hurricane of hurricanes) {
if (isInViewerAoi(hurricane, aoiWindow)) {
visibleHurricanes.push(hurricane);
}
}
return visibleHurricanes;
};
const syncVisibleHurricaneIdsByViewer = (
roomId: RoomId,
viewerId: SocketId,
hurricanes: HurricaneStatePayload[],
): void => {
const nextVisibleIds = hurricanes.map((hurricane) => hurricane.id);
deps.realtimeRoomSyncState.replaceVisibleHurricaneIds(
roomId,
viewerId,
nextVisibleIds,
);
};
const hasChangedVisibleHurricaneIds = (
previousVisibleIds: Set<string>,
nextVisibleHurricanes: HurricaneStatePayload[],
): boolean => {
if (previousVisibleIds.size !== nextVisibleHurricanes.length) {
return true;
}
for (const hurricane of nextVisibleHurricanes) {
if (!previousVisibleIds.has(hurricane.id)) {
return true;
}
}
return false;
};
return {
publishCurrentHurricanesToRoom: (roomId, hurricanes) => {
replaceRoomHurricaneSnapshot(roomId, hurricanes);
const roomPlayers = getRoomPlayers(deps.runtimeDeps, roomId);
if (roomPlayers.length === 0) {
return;
}
const roomPlayerById = new Map(roomPlayers.map((player) => [player.id, player]));
const recipientSocketIds = getConnectedSocketIdsInRoom(deps.runtimeDeps, roomId);
recipientSocketIds.forEach((viewerId) => {
const viewer = roomPlayerById.get(viewerId);
if (!viewer) {
return;
}
const visibleHurricanes = collectVisibleHurricanesByViewer(
roomId,
viewerId,
viewer,
hurricanes,
);
syncVisibleHurricaneIdsByViewer(roomId, viewerId, visibleHurricanes);
deps.reliable.emitToSocketById(
viewerId,
protocol.SocketEvents.CURRENT_HURRICANES,
visibleHurricanes,
);
});
},
publishUpdateHurricanesToRoom: (roomId, hurricanes) => {
upsertRoomHurricaneSnapshot(roomId, hurricanes);
const roomPlayers = getRoomPlayers(deps.runtimeDeps, roomId);
if (roomPlayers.length === 0) {
return;
}
const roomPlayerById = new Map(roomPlayers.map((player) => [player.id, player]));
const recipientSocketIds = getConnectedSocketIdsInRoom(deps.runtimeDeps, roomId);
const roomSnapshot = hurricaneSnapshotByRoomId.get(roomId);
recipientSocketIds.forEach((viewerId) => {
const viewer = roomPlayerById.get(viewerId);
if (!viewer) {
return;
}
const nextVisibleHurricanes = collectVisibleHurricanesByViewer(
roomId,
viewerId,
viewer,
roomSnapshot?.values() ?? [],
);
const previousVisibleIds = new Set(
deps.realtimeRoomSyncState.getVisibleHurricaneIdsSnapshot(roomId, viewerId),
);
const hasMembershipChanged = hasChangedVisibleHurricaneIds(
previousVisibleIds,
nextVisibleHurricanes,
);
if (hasMembershipChanged) {
deps.reliable.emitToSocketById(
viewerId,
protocol.SocketEvents.CURRENT_HURRICANES,
nextVisibleHurricanes,
);
syncVisibleHurricaneIdsByViewer(roomId, viewerId, nextVisibleHurricanes);
return;
}
const nextVisibleIdSet = new Set(nextVisibleHurricanes.map((hurricane) => hurricane.id));
const visibleUpdateHurricanes = hurricanes.filter((hurricane) => {
return nextVisibleIdSet.has(hurricane.id);
});
if (visibleUpdateHurricanes.length === 0) {
return;
}
deps.reliable.emitToSocketById(
viewerId,
protocol.SocketEvents.UPDATE_HURRICANES,
visibleUpdateHurricanes,
);
syncVisibleHurricaneIdsByViewer(roomId, viewerId, nextVisibleHurricanes);
});
},
clearRoomSnapshot: (roomId) => {
hurricaneSnapshotByRoomId.delete(roomId);
},
};
};