/** * Zulip WebSocket网关 * * 功能描述: * - 处理所有Godot游戏客户端的WebSocket连接 * - 实现游戏协议到Zulip协议的转换 * - 提供统一的消息路由和权限控制 * * 主要方法: * - handleConnection(): 处理客户端连接建立 * - handleDisconnect(): 处理客户端连接断开 * - handleLogin(): 处理登录消息 * - handleChat(): 处理聊天消息 * - handlePositionUpdate(): 处理位置更新 * * 使用场景: * - 游戏客户端WebSocket通信的统一入口 * - 消息协议转换和路由分发 * - 连接状态管理和权限验证 * * @author angjustinl * @version 1.0.0 * @since 2025-12-25 */ import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect, MessageBody, ConnectedSocket, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { Injectable, Logger } from '@nestjs/common'; import { ZulipService } from './zulip.service'; import { SessionManagerService } from './services/session_manager.service'; /** * 登录消息接口 - 按guide.md格式 */ interface LoginMessage { type: 'login'; token: string; } /** * 聊天消息接口 - 按guide.md格式 */ interface ChatMessage { t: 'chat'; content: string; scope: string; // "local" 或 topic名称 } /** * 位置更新消息接口 */ interface PositionMessage { t: 'position'; x: number; y: number; mapId: string; } /** * 聊天渲染消息接口 - 发送给客户端 */ interface ChatRenderMessage { t: 'chat_render'; from: string; txt: string; bubble: boolean; } /** * 登录成功消息接口 - 发送给客户端 */ interface LoginSuccessMessage { t: 'login_success'; sessionId: string; userId: string; username: string; currentMap: string; } /** * 客户端数据接口 */ interface ClientData { authenticated: boolean; userId: string | null; sessionId: string | null; username: string | null; connectedAt: Date; } /** * Zulip WebSocket网关类 * * 职责: * - 处理所有Godot游戏客户端的WebSocket连接 * - 实现游戏协议到Zulip协议的转换 * - 提供统一的消息路由和权限控制 * - 管理客户端连接状态和会话 * * 主要方法: * - handleConnection(): 处理客户端连接建立 * - handleDisconnect(): 处理客户端连接断开 * - handleLogin(): 处理登录消息 * - handleChat(): 处理聊天消息 * - handlePositionUpdate(): 处理位置更新 * - sendChatRender(): 向客户端发送聊天渲染消息 * * 使用场景: * - 游戏客户端WebSocket通信的统一入口 * - 消息协议转换和路由分发 * - 连接状态管理和权限验证 * - 实时消息推送和广播 */ @Injectable() @WebSocketGateway({ cors: { origin: '*' }, namespace: '/game', }) export class ZulipWebSocketGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; private readonly logger = new Logger(ZulipWebSocketGateway.name); constructor( private readonly zulipService: ZulipService, private readonly sessionManager: SessionManagerService, ) { this.logger.log('ZulipWebSocketGateway初始化完成', { gateway: 'ZulipWebSocketGateway', namespace: '/game', timestamp: new Date().toISOString(), }); // 设置消息分发器,使ZulipEventProcessorService能够向客户端发送消息 this.setupMessageDistributor(); } /** * 处理客户端连接建立 * * 功能描述: * 当游戏客户端建立WebSocket连接时调用,记录连接信息 * * 业务逻辑: * 1. 记录新连接的建立 * 2. 为连接分配唯一标识 * 3. 初始化连接状态 * * @param client WebSocket客户端连接对象 */ async handleConnection(client: Socket): Promise { this.logger.log('新的WebSocket连接建立', { operation: 'handleConnection', socketId: client.id, remoteAddress: client.handshake.address, timestamp: new Date().toISOString(), }); // 设置连接的初始状态 const clientData: ClientData = { authenticated: false, userId: null, sessionId: null, username: null, connectedAt: new Date(), }; client.data = clientData; } /** * 处理客户端连接断开 * * 功能描述: * 当游戏客户端断开WebSocket连接时调用,清理相关资源 * * 业务逻辑: * 1. 记录连接断开信息 * 2. 清理会话数据 * 3. 注销Zulip事件队列 * 4. 释放相关资源 * * @param client WebSocket客户端连接对象 */ async handleDisconnect(client: Socket): Promise { const clientData = client.data as ClientData | undefined; const connectionDuration = clientData?.connectedAt ? Date.now() - clientData.connectedAt.getTime() : 0; this.logger.log('WebSocket连接断开', { operation: 'handleDisconnect', socketId: client.id, userId: clientData?.userId, authenticated: clientData?.authenticated, connectionDuration, timestamp: new Date().toISOString(), }); // 如果用户已认证,处理登出逻辑 if (clientData?.authenticated) { try { await this.zulipService.handlePlayerLogout(client.id); this.logger.log('玩家登出处理完成', { operation: 'handleDisconnect', socketId: client.id, userId: clientData.userId, }); } catch (error) { const err = error as Error; this.logger.error('处理玩家登出时发生错误', { operation: 'handleDisconnect', socketId: client.id, error: err.message, timestamp: new Date().toISOString(), }, err.stack); } } } /** * 处理登录消息 - 按guide.md格式 * * 功能描述: * 处理游戏客户端发送的登录请求,验证Token并建立会话 * * 业务逻辑: * 1. 验证消息格式 * 2. 调用ZulipService处理登录逻辑 * 3. 更新连接状态 * 4. 返回登录结果 * * @param client WebSocket客户端连接对象 * @param data 登录消息数据 */ @SubscribeMessage('login') async handleLogin( @ConnectedSocket() client: Socket, @MessageBody() data: LoginMessage, ): Promise { this.logger.log('收到登录请求', { operation: 'handleLogin', socketId: client.id, messageType: data?.type, timestamp: new Date().toISOString(), }); try { // 验证消息格式 if (!data || data.type !== 'login' || !data.token) { this.logger.warn('登录请求格式无效', { operation: 'handleLogin', socketId: client.id, data, }); client.emit('login_error', { t: 'login_error', message: '登录请求格式无效', }); return; } // 检查是否已经登录 const clientData = client.data as ClientData; if (clientData?.authenticated) { this.logger.warn('用户已登录,拒绝重复登录', { operation: 'handleLogin', socketId: client.id, userId: clientData.userId, }); client.emit('login_error', { t: 'login_error', message: '您已经登录', }); return; } // 调用ZulipService处理登录 const result = await this.zulipService.handlePlayerLogin({ token: data.token, socketId: client.id, }); if (result.success && result.sessionId) { // 更新连接状态 const updatedClientData: ClientData = { authenticated: true, sessionId: result.sessionId, userId: result.userId || null, username: result.username || null, connectedAt: clientData?.connectedAt || new Date(), }; client.data = updatedClientData; // 发送登录成功消息 const loginSuccess: LoginSuccessMessage = { t: 'login_success', sessionId: result.sessionId, userId: result.userId || '', username: result.username || '', currentMap: result.currentMap || 'novice_village', }; client.emit('login_success', loginSuccess); this.logger.log('登录处理成功', { operation: 'handleLogin', socketId: client.id, sessionId: result.sessionId, userId: result.userId, username: result.username, currentMap: result.currentMap, timestamp: new Date().toISOString(), }); } else { // 发送登录失败消息 client.emit('login_error', { t: 'login_error', message: result.error || '登录失败', }); this.logger.warn('登录处理失败', { operation: 'handleLogin', socketId: client.id, error: result.error, timestamp: new Date().toISOString(), }); } } catch (error) { const err = error as Error; this.logger.error('登录处理异常', { operation: 'handleLogin', socketId: client.id, error: err.message, timestamp: new Date().toISOString(), }, err.stack); client.emit('login_error', { t: 'login_error', message: '系统错误,请稍后重试', }); } } /** * 处理聊天消息 - 按guide.md格式 * * 功能描述: * 处理游戏客户端发送的聊天消息,转发到Zulip对应的Stream/Topic * * 业务逻辑: * 1. 验证用户认证状态 * 2. 验证消息格式 * 3. 调用ZulipService处理消息发送 * 4. 返回发送结果确认 * * @param client WebSocket客户端连接对象 * @param data 聊天消息数据 */ @SubscribeMessage('chat') async handleChat( @ConnectedSocket() client: Socket, @MessageBody() data: ChatMessage, ): Promise { const clientData = client.data as ClientData | undefined; console.log('🔍 DEBUG: handleChat 被调用了!', { socketId: client.id, data: data, clientData: clientData, timestamp: new Date().toISOString(), }); this.logger.log('收到聊天消息', { operation: 'handleChat', socketId: client.id, messageType: data?.t, contentLength: data?.content?.length, scope: data?.scope, timestamp: new Date().toISOString(), }); try { // 验证用户认证状态 if (!clientData?.authenticated) { this.logger.warn('未认证用户尝试发送聊天消息', { operation: 'handleChat', socketId: client.id, }); client.emit('chat_error', { t: 'chat_error', message: '请先登录', }); return; } // 验证消息格式 if (!data || data.t !== 'chat' || !data.content || !data.scope) { this.logger.warn('聊天消息格式无效', { operation: 'handleChat', socketId: client.id, data, }); client.emit('chat_error', { t: 'chat_error', message: '消息格式无效', }); return; } // 验证消息内容不为空 if (!data.content.trim()) { this.logger.warn('聊天消息内容为空', { operation: 'handleChat', socketId: client.id, }); client.emit('chat_error', { t: 'chat_error', message: '消息内容不能为空', }); return; } // 调用ZulipService处理消息发送 const result = await this.zulipService.sendChatMessage({ socketId: client.id, content: data.content, scope: data.scope, }); if (result.success) { // 发送成功确认 client.emit('chat_sent', { t: 'chat_sent', messageId: result.messageId, message: '消息发送成功', }); this.logger.log('聊天消息发送成功', { operation: 'handleChat', socketId: client.id, userId: clientData.userId, messageId: result.messageId, timestamp: new Date().toISOString(), }); } else { // 发送失败通知 client.emit('chat_error', { t: 'chat_error', message: result.error || '消息发送失败', }); this.logger.warn('聊天消息发送失败', { operation: 'handleChat', socketId: client.id, userId: clientData.userId, error: result.error, timestamp: new Date().toISOString(), }); } } catch (error) { const err = error as Error; this.logger.error('聊天消息处理异常', { operation: 'handleChat', socketId: client.id, error: err.message, timestamp: new Date().toISOString(), }, err.stack); client.emit('chat_error', { t: 'chat_error', message: '系统错误,请稍后重试', }); } } /** * 处理位置更新消息 * * 功能描述: * 处理游戏客户端发送的位置更新,用于消息路由和上下文注入 * * @param client WebSocket客户端连接对象 * @param data 位置更新数据 */ @SubscribeMessage('position_update') async handlePositionUpdate( @ConnectedSocket() client: Socket, @MessageBody() data: PositionMessage, ): Promise { const clientData = client.data as ClientData | undefined; this.logger.debug('收到位置更新', { operation: 'handlePositionUpdate', socketId: client.id, mapId: data?.mapId, position: data ? { x: data.x, y: data.y } : null, timestamp: new Date().toISOString(), }); try { // 验证用户认证状态 if (!clientData?.authenticated) { this.logger.debug('未认证用户发送位置更新,忽略', { operation: 'handlePositionUpdate', socketId: client.id, }); return; } // 验证消息格式 if (!data || data.t !== 'position' || !data.mapId || typeof data.x !== 'number' || typeof data.y !== 'number') { this.logger.warn('位置更新消息格式无效', { operation: 'handlePositionUpdate', socketId: client.id, data, }); return; } // 验证坐标有效性 if (!Number.isFinite(data.x) || !Number.isFinite(data.y)) { this.logger.warn('位置坐标无效', { operation: 'handlePositionUpdate', socketId: client.id, x: data.x, y: data.y, }); return; } // 调用ZulipService更新位置 const success = await this.zulipService.updatePlayerPosition({ socketId: client.id, x: data.x, y: data.y, mapId: data.mapId, }); if (success) { this.logger.debug('位置更新成功', { operation: 'handlePositionUpdate', socketId: client.id, mapId: data.mapId, }); } } catch (error) { const err = error as Error; this.logger.error('位置更新处理异常', { operation: 'handlePositionUpdate', socketId: client.id, error: err.message, timestamp: new Date().toISOString(), }, err.stack); } } /** * 向指定客户端发送聊天渲染消息 * * 功能描述: * 向游戏客户端发送格式化的聊天消息,用于显示气泡或聊天框 * * @param socketId 目标客户端Socket ID * @param from 发送者名称 * @param txt 消息文本 * @param bubble 是否显示气泡 */ sendChatRender(socketId: string, from: string, txt: string, bubble: boolean): void { const message: ChatRenderMessage = { t: 'chat_render', from, txt, bubble, }; this.server.to(socketId).emit('chat_render', message); this.logger.debug('发送聊天渲染消息', { operation: 'sendChatRender', socketId, from, textLength: txt.length, bubble, timestamp: new Date().toISOString(), }); } /** * 向指定地图的所有客户端广播消息 * * 功能描述: * 向指定地图区域内的所有在线玩家广播消息 * * @param mapId 地图ID * @param event 事件名称 * @param data 消息数据 */ async broadcastToMap(mapId: string, event: string, data: any): Promise { this.logger.debug('向地图广播消息', { operation: 'broadcastToMap', mapId, event, timestamp: new Date().toISOString(), }); try { // 从SessionManager获取指定地图的所有Socket ID const socketIds = await this.sessionManager.getSocketsInMap(mapId); if (socketIds.length === 0) { this.logger.debug('地图中没有在线玩家', { operation: 'broadcastToMap', mapId, }); return; } // 向每个Socket发送消息 for (const socketId of socketIds) { this.server.to(socketId).emit(event, data); } this.logger.log('地图广播完成', { operation: 'broadcastToMap', mapId, event, recipientCount: socketIds.length, }); } catch (error) { const err = error as Error; this.logger.error('地图广播失败', { operation: 'broadcastToMap', mapId, event, error: err.message, }, err.stack); } } /** * 向指定客户端发送消息 * * 功能描述: * 向指定的WebSocket客户端发送消息 * * @param socketId 目标客户端Socket ID * @param event 事件名称 * @param data 消息数据 */ sendToPlayer(socketId: string, event: string, data: any): void { this.server.to(socketId).emit(event, data); this.logger.debug('发送消息给玩家', { operation: 'sendToPlayer', socketId, event, timestamp: new Date().toISOString(), }); } /** * 获取当前连接数 * * 功能描述: * 获取当前WebSocket网关的连接数量 * * @returns Promise 连接数 */ async getConnectionCount(): Promise { try { const sockets = await this.server.fetchSockets(); return sockets.length; } catch (error) { this.logger.error('获取连接数失败', { operation: 'getConnectionCount', error: (error as Error).message, }); return 0; } } /** * 获取已认证的连接数 * * 功能描述: * 获取当前已认证的WebSocket连接数量 * * @returns Promise 已认证连接数 */ async getAuthenticatedConnectionCount(): Promise { try { const sockets = await this.server.fetchSockets(); return sockets.filter(socket => { const data = socket.data as ClientData | undefined; return data?.authenticated === true; }).length; } catch (error) { this.logger.error('获取已认证连接数失败', { operation: 'getAuthenticatedConnectionCount', error: (error as Error).message, }); return 0; } } /** * 断开指定客户端连接 * * 功能描述: * 强制断开指定的WebSocket客户端连接 * * @param socketId 目标客户端Socket ID * @param reason 断开原因 */ async disconnectClient(socketId: string, reason?: string): Promise { try { const sockets = await this.server.fetchSockets(); const targetSocket = sockets.find(s => s.id === socketId); if (targetSocket) { targetSocket.disconnect(true); this.logger.log('客户端连接已断开', { operation: 'disconnectClient', socketId, reason, }); } else { this.logger.warn('未找到目标客户端', { operation: 'disconnectClient', socketId, }); } } catch (error) { this.logger.error('断开客户端连接失败', { operation: 'disconnectClient', socketId, error: (error as Error).message, }); } } /** * 设置消息分发器 * * 功能描述: * 将当前WebSocket网关设置为ZulipEventProcessorService的消息分发器, * 使其能够接收从Zulip返回的消息并转发给游戏客户端 * * @private */ private setupMessageDistributor(): void { try { // 获取ZulipEventProcessorService实例 const eventProcessor = this.zulipService.getEventProcessor(); if (eventProcessor) { // 设置消息分发器 eventProcessor.setMessageDistributor(this); this.logger.log('消息分发器设置完成', { operation: 'setupMessageDistributor', timestamp: new Date().toISOString(), }); } else { this.logger.warn('无法获取ZulipEventProcessorService实例', { operation: 'setupMessageDistributor', }); } } catch (error) { const err = error as Error; this.logger.error('设置消息分发器失败', { operation: 'setupMessageDistributor', error: err.message, }, err.stack); } } }