extends Node # ============================================================================ # ChatManager.gd - 聊天系统业务逻辑核心 # ============================================================================ # 管理聊天功能的核心业务逻辑 # # 核心职责: # - 聊天消息发送/接收协调 # - 客户端频率限制(10条/分钟) # - 消息历史管理(最多100条) # - Signal Up: 通过信号和 EventSystem 向上通知 # - 整合 AuthManager 获取 token # # 使用方式: # ChatManager.connect_to_chat_server() # ChatManager.send_chat_message("Hello", "local") # ChatManager.chat_message_received.connect(_on_message_received) # # 注意事项: # - 作为自动加载单例,全局可访问 # - 遵循 "Signal Up, Call Down" 架构 # - 所有聊天事件通过 EventSystem 广播 # ============================================================================ # ============================================================================ # 信号定义 (Signal Up) # ============================================================================ # 聊天消息已发送信号 # 参数: # message_id: String - 消息 ID # timestamp: float - 时间戳 signal chat_message_sent(message_id: String, timestamp: float) # 聊天消息已接收信号 # 参数: # from_user: String - 发送者用户名 # content: String - 消息内容 # show_bubble: bool - 是否显示气泡 # timestamp: float - 时间戳 signal chat_message_received(from_user: String, content: String, show_bubble: bool, timestamp: float) # 聊天错误发生信号 # 参数: # error_code: String - 错误代码 # message: String - 错误消息 signal chat_error_occurred(error_code: String, message: String) # 聊天连接状态变化信号 # 参数: # state: int - 连接状态(0=DISCONNECTED, 1=CONNECTING, 2=CONNECTED, 3=RECONNECTING, 4=ERROR) signal chat_connection_state_changed(state: int) # 位置更新成功信号 # 参数: # stream: String - Stream 名称 # topic: String - Topic 名称 signal chat_position_updated(stream: String, topic: String) # ============================================================================ # 常量定义 # ============================================================================ # WebSocket 服务器 URL(原生 WebSocket) const WEBSOCKET_URL: String = "wss://whaletownend.xinghangee.icu/game" # 重连配置 const RECONNECT_MAX_ATTEMPTS: int = 5 const RECONNECT_BASE_DELAY: float = 3.0 # 频率限制配置 const RATE_LIMIT_MESSAGES: int = 10 const RATE_LIMIT_WINDOW: float = 60.0 # 秒 # 消息限制 const MAX_MESSAGE_LENGTH: int = 1000 # 当前会话消息限制(当前游戏会话,超过后删除最旧的) const MAX_SESSION_MESSAGES: int = 100 # 历史消息分页大小(从 Zulip 后端每次加载的数量) const HISTORY_PAGE_SIZE: int = 100 # 错误消息映射 const CHAT_ERROR_MESSAGES: Dictionary = { "AUTH_FAILED": "聊天认证失败,请重新登录", "RATE_LIMIT": "消息发送过于频繁,请稍后再试", "CONTENT_FILTERED": "消息内容包含违规内容", "CONTENT_TOO_LONG": "消息内容过长(最大1000字符)", "PERMISSION_DENIED": "您没有权限发送消息", "SESSION_EXPIRED": "会话已过期,请重新连接", "ZULIP_ERROR": "消息服务暂时不可用", "INTERNAL_ERROR": "服务器内部错误" } # ============================================================================ # 成员变量 # ============================================================================ # WebSocket 管理器 var _websocket_manager: WebSocketManager # 是否已登录 var _is_logged_in: bool = false # 消息历史记录(当前会话,最多100条,超过后删除最旧的) var _message_history: Array[Dictionary] = [] # 历史消息加载状态 var _history_loading: bool = false var _has_more_history: bool = true var _oldest_message_timestamp: float = 0.0 # 消息发送时间戳(用于频率限制) var _message_timestamps: Array[float] = [] # 当前用户信息 var _current_username: String = "" var _current_map: String = "" # 游戏 token var _game_token: String = "" # 发送后本地回显去重(避免服务端也回发导致重复显示) const SELF_ECHO_DEDUPE_WINDOW: float = 10.0 var _pending_self_messages: Array[Dictionary] = [] # ============================================================================ # 生命周期方法 # ============================================================================ # 初始化 func _ready() -> void: print("ChatManager 初始化完成") # 创建 WebSocket 管理器 _websocket_manager = WebSocketManager.new() add_child(_websocket_manager) # 连接信号 _connect_signals() # 清理 func _exit_tree() -> void: if is_instance_valid(_websocket_manager): _websocket_manager.queue_free() # ============================================================================ # 公共 API - Token 管理 # ============================================================================ # 设置游戏 token # # 参数: # token: String - 游戏认证 token # # 使用示例: # ChatManager.set_game_token("your_game_token") func set_game_token(token: String) -> void: _game_token = token print("ChatManager: 游戏 token 已设置") # 获取游戏 token # # 返回值: # String - 当前游戏 token func get_game_token() -> String: return _game_token # ============================================================================ # 公共 API - 连接管理 # ============================================================================ # 连接到聊天服务器 func connect_to_chat_server() -> void: if _websocket_manager.is_websocket_connected(): push_warning("聊天服务器已连接") return print("=== ChatManager 开始连接 ===") _websocket_manager.connect_to_game_server() # 断开聊天服务器 func disconnect_from_chat_server() -> void: print("=== ChatManager 断开连接 ===") # 发送登出消息 if _is_logged_in: var logout_data := {"type": "logout"} _websocket_manager.send_message(JSON.stringify(logout_data)) _is_logged_in = false # 断开连接 _websocket_manager.disconnect_websocket() # 检查是否已连接 # # 返回值: # bool - 是否已连接 func is_chat_connected() -> bool: return _websocket_manager.is_websocket_connected() # ============================================================================ # 公共 API - 聊天操作 # ============================================================================ # 发送聊天消息 # # 参数: # content: String - 消息内容 # scope: String - 消息范围("local" 或具体 topic 名称) # # 使用示例: # ChatManager.send_chat_message("Hello, world!", "local") func send_chat_message(content: String, scope: String = "local") -> void: # 检查连接状态 if not _websocket_manager.is_websocket_connected(): _handle_error("NOT_CONNECTED", "未连接到聊天服务器") return # 检查登录状态 if not _is_logged_in: _handle_error("NOT_LOGGED_IN", "尚未登录聊天服务器") return # 检查消息长度 if content.length() > MAX_MESSAGE_LENGTH: _handle_error("CONTENT_TOO_LONG", "消息内容过长") return # 检查频率限制 if not can_send_message(): var wait_time := get_time_until_next_message() _handle_error("RATE_LIMIT", "请等待 %.1f 秒后再试" % wait_time) return # 构建消息数据 var message_data := { "type": "chat", "content": content, "scope": scope } # 发送消息(JSON 字符串) var json_string := JSON.stringify(message_data) var send_err: Error = _websocket_manager.send_message(json_string) if send_err != OK: _handle_error("SEND_FAILED", "WebSocket send failed: %s" % error_string(send_err)) return # 记录发送时间 _record_message_timestamp() # 添加到历史 _add_message_to_history({ "from_user": _current_username, "content": content, "timestamp": Time.get_unix_time_from_system(), "is_self": true }) var now_timestamp: float = Time.get_unix_time_from_system() # 记录待去重的“自己消息”(如果服务端也回发 chat_render,则避免重复显示) _pending_self_messages.append({ "content": content, "expires_at": now_timestamp + SELF_ECHO_DEDUPE_WINDOW }) # 本地回显:UI 目前只订阅 CHAT_MESSAGE_RECEIVED,所以这里也发一次 received chat_message_received.emit(_current_username, content, true, now_timestamp) EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, { "from_user": _current_username, "content": content, "show_bubble": true, "timestamp": now_timestamp, "is_self": true }) print("📤 发送聊天消息: ", content) # 消息发送完成回调 func _on_chat_message_sent(request_id: String, success: bool, data: Dictionary, error_info: Dictionary) -> void: if success: print("✅ 消息发送成功: ", data) var message_id: String = str(data.get("data", {}).get("id", "")) var timestamp: float = Time.get_unix_time_from_system() chat_message_sent.emit(message_id, timestamp) EventSystem.emit_event(EventNames.CHAT_MESSAGE_SENT, { "message_id": message_id, "timestamp": timestamp }) else: print("❌ 消息发送失败: ", error_info) _handle_error("SEND_FAILED", error_info.get("message", "发送失败")) # 更新玩家位置 # # 参数: # x: float - X 坐标 # y: float - Y 坐标 # map_id: String - 地图 ID # # 使用示例: # ChatManager.update_player_position(150.0, 200.0, "novice_village") func update_player_position(x: float, y: float, map_id: String) -> void: if not _websocket_manager.is_websocket_connected(): return var position_data := { "type": "position", "x": x, "y": y, "mapId": map_id } # 发送消息(JSON 字符串) var json_string := JSON.stringify(position_data) _websocket_manager.send_message(json_string) print("📍 更新位置: (%.2f, %.2f) in %s" % [x, y, map_id]) # ============================================================================ # 公共 API - 频率限制 # ============================================================================ # 检查是否可以发送消息 # # 返回值: # bool - 是否可以发送 func can_send_message() -> bool: var current_time := Time.get_unix_time_from_system() # 清理过期的时间戳 var filter_func := func(timestamp: float) -> bool: return current_time - timestamp < RATE_LIMIT_WINDOW _message_timestamps = _message_timestamps.filter(filter_func) # 检查数量 return _message_timestamps.size() < RATE_LIMIT_MESSAGES # 获取距离下次可发送消息的时间 # # 返回值: # float - 等待时间(秒) func get_time_until_next_message() -> float: if _message_timestamps.is_empty(): return 0.0 if _message_timestamps.size() < RATE_LIMIT_MESSAGES: return 0.0 # 找到最早的时间戳 var earliest_timestamp: float = _message_timestamps[0] var current_time := Time.get_unix_time_from_system() var elapsed := current_time - earliest_timestamp if elapsed >= RATE_LIMIT_WINDOW: return 0.0 return RATE_LIMIT_WINDOW - elapsed # ============================================================================ # 公共 API - 消息历史 # ============================================================================ # 获取消息历史 # # 返回值: # Array[Dictionary] - 消息历史数组 func get_message_history() -> Array[Dictionary]: return _message_history.duplicate() # 清空消息历史 func clear_message_history() -> void: _message_history.clear() print("🧹 清空消息历史") # 重置当前会话(每次登录/重连时调用) # # 功能: # - 清空当前会话消息缓存 # - 重置历史消息加载状态 # - 不影响 Zulip 后端的历史消息 # # 使用场景: # - 用户登录成功后 # - 重新连接到聊天服务器后 func reset_session() -> void: _message_history.clear() _history_loading = false _has_more_history = true _oldest_message_timestamp = 0.0 print("🔄 重置聊天会话") # 加载历史消息(按需从 Zulip 后端获取) # # 参数: # count: int - 要加载的消息数量(默认 HISTORY_PAGE_SIZE) # # 功能: # - 从 Zulip 后端获取历史消息 # - 添加到当前会话历史开头 # - 触发 CHAT_MESSAGE_RECEIVED 事件显示消息 # # 使用场景: # - 用户滚动到聊天窗口顶部 # - 用户主动点击"加载历史"按钮 # # 注意: # - 这是异步操作,需要通过 Zulip API 实现 # - 当前实现为占位符,需要后端 API 支持 func load_history(count: int = HISTORY_PAGE_SIZE) -> void: if _history_loading: print("⏳ 历史消息正在加载中...") return if not _has_more_history: print("📚 没有更多历史消息") return _history_loading = true print("📜 开始加载历史消息,数量: ", count) # TODO: 实现从 Zulip 后端获取历史消息 # NetworkManager.get_chat_history(_oldest_message_timestamp, count, _on_history_loaded) # 临时实现:模拟历史消息加载(测试用) # await get_tree().create_timer(1.0).timeout # _on_history_loaded([]) # 历史消息加载完成回调 func _on_history_loaded(messages: Array) -> void: _history_loading = false if messages.is_empty(): _has_more_history = false print("📚 没有更多历史消息") return print("📜 历史消息加载完成,数量: ", messages.size()) # 将历史消息插入到当前会话历史开头 for i in range(messages.size() - 1, -1, -1): var message: Dictionary = messages[i] _message_history.push_front(message) # 触发事件显示消息(Signal Up) EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, { "from_user": message.get("from_user", ""), "content": message.get("content", ""), "show_bubble": false, "timestamp": message.get("timestamp", 0.0), "is_self": (not _current_username.is_empty() and message.get("from_user", "") == _current_username), "is_history": true # 标记为历史消息 }) # 更新最旧消息时间戳 var oldest: Dictionary = messages.back() if oldest.has("timestamp"): _oldest_message_timestamp = oldest.timestamp # 检查是否还有更多历史 if messages.size() < HISTORY_PAGE_SIZE: _has_more_history = false # ============================================================================ # 内部方法 - 信号连接 # ============================================================================ # 连接信号 func _connect_signals() -> void: # WebSocket 管理器信号 _websocket_manager.connection_state_changed.connect(_on_connection_state_changed) _websocket_manager.data_received.connect(_on_data_received) # 发送登录消息 func _send_login_message() -> void: print("📤 发送登录消息...") var login_data := { "type": "login", "token": _game_token } var json_string := JSON.stringify(login_data) _websocket_manager.send_message(json_string) print(" Token: ", _game_token.left(20) + "..." if _game_token.length() > 20 else _game_token) # 连接状态变化 func _on_connection_state_changed(state: int) -> void: var state_names := ["DISCONNECTED", "CONNECTING", "CONNECTED", "RECONNECTING", "ERROR"] print("📡 ChatManager: 连接状态变化 - ", state_names[state]) # 发射信号 chat_connection_state_changed.emit(state) # 通过 EventSystem 广播(Signal Up) EventSystem.emit_event(EventNames.CHAT_CONNECTION_STATE_CHANGED, { "state": state }) # 如果连接成功,发送登录消息 if state == 2: # CONNECTED _send_login_message() # ============================================================================ # 内部方法 - 消息处理 # ============================================================================ # WebSocket 数据接收 func _on_data_received(message: String) -> void: # 解析 JSON 消息 var json := JSON.new() var parse_result := json.parse(message) if parse_result != OK: print("❌ ChatManager: JSON 解析失败 - ", message) return var data: Dictionary = json.data # 检查消息类型字段 var message_type: String = data.get("t", "") match message_type: "login_success": _handle_login_success(data) "login_error": _handle_login_error(data) "chat": _handle_chat_render(data) "chat_sent": _handle_chat_sent(data) "chat_error": _handle_chat_error(data) "chat_render": _handle_chat_render(data) "position_updated": _handle_position_updated(data) _: print("⚠️ ChatManager: 未处理的消息类型 - ", message_type) print(" 消息内容: ", data) # 处理登录成功 func _handle_login_success(data: Dictionary) -> void: print("✅ ChatManager: 登录成功") _is_logged_in = true _current_username = data.get("username", "") _current_map = data.get("currentMap", "") # 重置当前会话缓存(每次登录/重连都清空,重新开始接收消息) reset_session() print(" 用户名: ", _current_username) print(" 地图: ", _current_map) # 通过 EventSystem 广播(Signal Up) EventSystem.emit_event(EventNames.CHAT_LOGIN_SUCCESS, { "username": _current_username, "current_map": _current_map }) # 处理登录失败 func _handle_login_error(data: Dictionary) -> void: var error_message: String = data.get("message", "登录失败") print("❌ ChatManager: 登录失败 - ", error_message) _is_logged_in = false # 通过 EventSystem 广播错误(Signal Up) EventSystem.emit_event(EventNames.CHAT_LOGIN_FAILED, { "error_code": "LOGIN_FAILED", "message": error_message }) # 处理聊天消息发送成功 func _handle_chat_sent(data: Dictionary) -> void: var message_id: String = str(data.get("messageId", "")) var timestamp: float = data.get("timestamp", 0.0) print("✅ 消息发送成功: ", message_id) # 发射信号 chat_message_sent.emit(message_id, timestamp) # 通过 EventSystem 广播(Signal Up) EventSystem.emit_event(EventNames.CHAT_MESSAGE_SENT, { "message_id": message_id, "timestamp": timestamp }) # 处理聊天消息发送失败 func _handle_chat_error(data: Dictionary) -> void: var error_message: String = data.get("message", "消息发送失败") print("❌ ChatManager: 聊天错误 - ", error_message) # 通过 EventSystem 广播错误(Signal Up) EventSystem.emit_event(EventNames.CHAT_ERROR_OCCURRED, { "error_code": "CHAT_SEND_FAILED", "message": error_message }) # 处理接收到的聊天消息 func _handle_chat_render(data: Dictionary) -> void: # 兼容不同后端字段命名: # - chat_render: {from, txt, bubble, timestamp} # - chat: {content, scope, (可选 from/username/timestamp)} var from_user: String = data.get("from", data.get("from_user", data.get("username", ""))) var content: String = data.get("txt", data.get("content", "")) var show_bubble: bool = bool(data.get("bubble", data.get("show_bubble", false))) var timestamp: float = _parse_chat_timestamp_to_unix(data.get("timestamp", 0.0)) var is_self: bool = (not _current_username.is_empty() and from_user == _current_username) if is_self and _consume_pending_self_message(content): # 已经本地回显过,避免重复显示 return # 如果服务端没带发送者信息,但内容匹配最近自己发送的消息,则认为是自己消息 if from_user.is_empty() and _consume_pending_self_message(content): from_user = _current_username is_self = true print("📨 收到聊天消息: ", from_user, " -> ", content) # 添加到历史 _add_message_to_history({ "from_user": from_user, "content": content, "timestamp": timestamp, "is_self": is_self }) # 发射信号 chat_message_received.emit(from_user, content, show_bubble, timestamp) # 通过 EventSystem 广播(Signal Up) EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, { "from_user": from_user, "content": content, "show_bubble": show_bubble, "timestamp": timestamp, "is_self": is_self }) # 解析聊天消息时间戳(兼容 unix 秒 / ISO 8601 字符串) func _parse_chat_timestamp_to_unix(timestamp_raw: Variant) -> float: if typeof(timestamp_raw) == TYPE_INT or typeof(timestamp_raw) == TYPE_FLOAT: var ts := float(timestamp_raw) return ts if ts > 0.0 else Time.get_unix_time_from_system() var ts_str := str(timestamp_raw) if ts_str.strip_edges().is_empty(): return Time.get_unix_time_from_system() # 纯数字字符串(必须整串都是数字/小数点,避免把 ISO 字符串前缀 "2026" 误判成时间戳) var numeric_regex := RegEx.new() numeric_regex.compile("^\\s*-?\\d+(?:\\.\\d+)?\\s*$") if numeric_regex.search(ts_str) != null: var ts_num := float(ts_str) return ts_num if ts_num > 0.0 else Time.get_unix_time_from_system() # ISO 8601: 2026-01-19T15:15:43.930Z var regex := RegEx.new() regex.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})") var result := regex.search(ts_str) if result == null: return Time.get_unix_time_from_system() var utc_dict := { "year": int(result.get_string(1)), "month": int(result.get_string(2)), "day": int(result.get_string(3)), "hour": int(result.get_string(4)), "minute": int(result.get_string(5)), "second": int(result.get_string(6)) } return Time.get_unix_time_from_datetime_dict(utc_dict) # 处理位置更新成功 func _handle_position_updated(data: Dictionary) -> void: var stream: String = data.get("stream", "") var topic: String = data.get("topic", "") print("✅ 位置更新成功: ", stream, " / ", topic) # 发射信号 chat_position_updated.emit(stream, topic) # 通过 EventSystem 广播(Signal Up) EventSystem.emit_event(EventNames.CHAT_POSITION_UPDATED, { "stream": stream, "topic": topic }) # 处理错误响应(如果需要) func _handle_error_response(data: Dictionary) -> void: var error_code: String = data.get("code", "") var error_message: String = data.get("message", "") _handle_error(error_code, error_message) # 处理 Socket 错误(如果需要) func _on_socket_error(error: String) -> void: _handle_error("SOCKET_ERROR", error) # ============================================================================ # 内部方法 - 工具函数 # ============================================================================ # 处理错误 func _handle_error(error_code: String, error_message: String) -> void: print("❌ ChatManager 错误: [", error_code, "] ", error_message) # 获取用户友好的错误消息 var user_message: String = CHAT_ERROR_MESSAGES.get(error_code, error_message) as String # 发射信号 chat_error_occurred.emit(error_code, user_message) # 通过 EventSystem 广播(Signal Up) EventSystem.emit_event(EventNames.CHAT_ERROR_OCCURRED, { "error_code": error_code, "message": user_message }) # 特殊处理认证失败 if error_code == "AUTH_FAILED" or error_code == "SESSION_EXPIRED": _is_logged_in = false EventSystem.emit_event(EventNames.CHAT_LOGIN_FAILED, { "error_code": error_code }) # 记录消息发送时间戳 func _record_message_timestamp() -> void: var current_time := Time.get_unix_time_from_system() _message_timestamps.append(current_time) # 消费一个待去重的“自己消息”(允许相同内容多次发送:每次消费一个) func _consume_pending_self_message(content: String) -> bool: var now := Time.get_unix_time_from_system() # 先清理过期项 for i in range(_pending_self_messages.size() - 1, -1, -1): var item: Dictionary = _pending_self_messages[i] if float(item.get("expires_at", 0.0)) < now: _pending_self_messages.remove_at(i) # 再匹配内容 for i in range(_pending_self_messages.size() - 1, -1, -1): if str(_pending_self_messages[i].get("content", "")) == content: _pending_self_messages.remove_at(i) return true return false # 添加消息到当前会话历史 func _add_message_to_history(message: Dictionary) -> void: _message_history.append(message) # 更新最旧消息时间戳(用于历史消息加载) if _oldest_message_timestamp == 0.0 or message.timestamp < _oldest_message_timestamp: _oldest_message_timestamp = message.timestamp # 限制当前会话消息数量(超过后删除最旧的) if _message_history.size() > MAX_SESSION_MESSAGES: _message_history.pop_front()