extends Node # ============================================================================ # ChatManager.gd - 聊天系统业务逻辑核心 # ============================================================================ # 管理聊天功能的核心业务逻辑 # # 核心职责: # - 聊天消息发送/接收协调 # - 客户端频率限制(10条/分钟) # - 消息历史管理(最多100条) # - Signal Up: 通过信号和 EventSystem 向上通知 # - 整合 AuthManager 获取 token # # 使用方式: # ChatManager.connect_to_chat_server() # ChatManager.send_chat_message("Hello", "global") # 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) # ============================================================================ # 常量定义 # ============================================================================ const CHAT_WEBSOCKET_MANAGER_SCRIPT: Script = preload("res://_Core/managers/WebSocketManager.gd") const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd") # 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 GAME_TOKEN_ENV_KEY: String = "WHALETOWN_GAME_TOKEN" const MAX_QUEUED_MESSAGES: int = 10 const MAX_RECOVERABLE_MESSAGES: int = 5 # 错误消息映射 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: Node var _history_request: HTTPRequest # 是否已登录 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 = "" var _world_ready_map: String = "" # 游戏 token var _game_token: String = "" # 连接/登录完成前暂存用户已经提交的消息,避免刚进游戏时第一条消息被丢掉。 var _queued_messages: Array[Dictionary] = [] var _recoverable_messages: Array[Dictionary] = [] # 发送后本地回显去重(避免服务端也回发导致重复显示) const SELF_ECHO_DEDUPE_WINDOW: float = 10.0 var _pending_self_messages: Array[Dictionary] = [] # 好友列表缓存 var _friends: Array[Dictionary] = [] var _friend_protocol_supported: bool = true # 空消息类型告警限频(避免日志刷屏) const EMPTY_MESSAGE_TYPE_WARNING_INTERVAL: float = 10.0 var _last_empty_message_type_warning_at: float = -1000.0 var _refreshing_auth_session: bool = false var _chat_login_refresh_attempted: bool = false # ============================================================================ # 生命周期方法 # ============================================================================ # 初始化 func _ready() -> void: _load_token_from_environment() # 创建 WebSocket 管理器 _websocket_manager = CHAT_WEBSOCKET_MANAGER_SCRIPT.new() add_child(_websocket_manager) _history_request = HTTPRequest.new() _history_request.timeout = 12.0 _history_request.request_completed.connect(_on_history_request_completed) add_child(_history_request) # 连接信号 _connect_signals() var authManager := get_node_or_null("/root/AuthManager") if authManager != null and authManager.has_signal("profile_update_succeeded"): authManager.connect("profile_update_succeeded", _on_account_profile_updated) if authManager != null and authManager.has_signal("auth_state_changed"): authManager.connect("auth_state_changed", _on_auth_state_changed) # 清理 func _exit_tree() -> void: if is_instance_valid(_websocket_manager): _websocket_manager.queue_free() func _get_event_system() -> Node: return get_node_or_null("/root/EventSystem") func _emit_event(event_name: String, data: Variant = null) -> void: var eventSystem: Node = _get_event_system() if eventSystem == null: return eventSystem.call("emit_event", event_name, data) # ============================================================================ # 公共 API - Token 管理 # ============================================================================ # 设置游戏 token # # 参数: # token: String - 游戏认证 token # # 使用示例: # ChatManager.set_game_token("your_game_token") func set_game_token(token: String) -> void: var normalizedToken := token.strip_edges() if normalizedToken != _game_token: _chat_login_refresh_attempted = false _game_token = normalizedToken if _game_token.strip_edges().is_empty(): _is_logged_in = false _queued_messages.clear() _recoverable_messages.clear() _world_ready_map = "" func is_logged_in() -> bool: return _is_logged_in func mark_world_ready(mapId: String, position: Vector2) -> void: var normalizedMapId := mapId.strip_edges() if not _is_logged_in or normalizedMapId.is_empty() or _world_ready_map == normalizedMapId: return var payload := { "type": "world_ready", "mapId": normalizedMapId, "x": position.x, "y": position.y, } if _websocket_manager.send_message(JSON.stringify(payload)) == OK: _world_ready_map = normalizedMapId func leave_world(sceneId: String = "private_space") -> void: if not _is_logged_in or _world_ready_map.is_empty(): return var normalizedSceneId := sceneId.strip_edges() if normalizedSceneId.is_empty(): normalizedSceneId = "private_space" var payload := { "type": "leave_world", "sceneId": normalizedSceneId, } if _websocket_manager.send_message(JSON.stringify(payload)) == OK: _world_ready_map = "" func notify_appearance_changed() -> void: if not _is_logged_in or _world_ready_map.is_empty(): return _websocket_manager.send_message(JSON.stringify({"type": "appearance_changed"})) # 获取游戏 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 var connection_state: int = _websocket_manager.call("get_connection_state") if connection_state == 1 or connection_state == 3: push_warning("聊天服务器正在连接") return if _game_token.strip_edges().is_empty(): var authToken := _get_auth_manager_token() if not authToken.is_empty(): _game_token = authToken if _game_token.strip_edges().is_empty(): _handle_error("AUTH_FAILED", "请先登录后再使用聊天") return _websocket_manager.connect_to_game_server() # 断开聊天服务器 func disconnect_from_chat_server() -> void: _queued_messages.clear() _recoverable_messages.clear() # 发送登出消息 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() func is_chat_logged_in() -> bool: return _is_logged_in func can_attempt_chat_connection() -> bool: if not _game_token.strip_edges().is_empty(): return true if not _get_auth_manager_token().is_empty(): return true return false # ============================================================================ # 公共 API - 聊天操作 # ============================================================================ # 发送聊天消息 # # 参数: # content: String - 消息内容 # scope: String - 消息范围("local" / "global") # show_bubble: bool - 是否同步显示角色气泡 # # 使用示例: # ChatManager.send_chat_message("Hello, world!", "global") func send_chat_message(content: String, scope: String = "local", show_bubble: bool = false) -> bool: return _send_chat_payload(content, scope, {"bubble": show_bubble and _settings_bool("show_chat_bubbles", true)}) func send_private_message(content: String, target_user_id: String, target_username: String = "", private_context: String = "") -> bool: if not _settings_bool("allow_nearby_private", true) and private_context.strip_edges() == "whisper": _handle_error("PRIVATE_DISABLED", "附近私聊已在设置中关闭") return false var normalized_target_user_id := target_user_id.strip_edges() if normalized_target_user_id.is_empty(): _handle_error("PRIVATE_TARGET_REQUIRED", "请选择悄悄话对象") return false return _send_chat_payload(content, "private", { "targetUserId": normalized_target_user_id, "targetUsername": target_username.strip_edges(), "privateContext": private_context.strip_edges() }) func request_friend_list() -> bool: if not _friend_protocol_supported: _emit_friend_protocol_status("当前聊天后端暂不支持好友功能") return false return _send_friend_command({"type": "friend_list"}) func add_friend(friend_user_id: String, friend_username: String = "") -> bool: if not _friend_protocol_supported: _emit_friend_protocol_status("当前聊天后端暂不支持好友功能") return false var normalized_friend_user_id := friend_user_id.strip_edges() if normalized_friend_user_id.is_empty(): _handle_error("FRIEND_TARGET_REQUIRED", "请选择好友") return false return _send_friend_command({ "type": "friend_add", "friendUserId": normalized_friend_user_id, "friendUsername": friend_username.strip_edges() }) func request_friend(friend_user_id: String, friend_username: String = "") -> bool: if not _settings_bool("allow_nearby_friend_requests", true): _handle_error("FRIEND_REQUEST_DISABLED", "好友申请已在设置中关闭") return false if not _friend_protocol_supported: _emit_friend_protocol_status("当前聊天后端暂不支持好友功能") return false var normalized_friend_user_id := friend_user_id.strip_edges() if normalized_friend_user_id.is_empty(): _handle_error("FRIEND_TARGET_REQUIRED", "请选择好友") return false return _send_friend_command({ "type": "friend_request", "friendUserId": normalized_friend_user_id, "friendUsername": friend_username.strip_edges() }) func accept_friend_request(friend_user_id: String, friend_username: String = "") -> bool: if not _friend_protocol_supported: _emit_friend_protocol_status("当前聊天后端暂不支持好友功能") return false var normalized_friend_user_id := friend_user_id.strip_edges() if normalized_friend_user_id.is_empty(): _handle_error("FRIEND_TARGET_REQUIRED", "请选择好友请求") return false return _send_friend_command({ "type": "friend_accept", "friendUserId": normalized_friend_user_id, "friendUsername": friend_username.strip_edges() }) func reject_friend_request(friend_user_id: String, friend_username: String = "") -> bool: if not _friend_protocol_supported: _emit_friend_protocol_status("当前聊天后端暂不支持好友功能") return false var normalized_friend_user_id := friend_user_id.strip_edges() if normalized_friend_user_id.is_empty(): _handle_error("FRIEND_TARGET_REQUIRED", "请选择好友请求") return false return _send_friend_command({ "type": "friend_reject", "friendUserId": normalized_friend_user_id, "friendUsername": friend_username.strip_edges() }) func remove_friend(friend_user_id: String) -> bool: var normalized_friend_user_id := friend_user_id.strip_edges() if normalized_friend_user_id.is_empty(): return false return _send_friend_command({ "type": "friend_remove", "friendUserId": normalized_friend_user_id }) func get_friends() -> Array[Dictionary]: return _friends.duplicate(true) func _send_friend_command(message_data: Dictionary) -> bool: if not _websocket_manager.is_websocket_connected(): connect_to_chat_server() return false if not _is_logged_in: _handle_error("NOT_LOGGED_IN", "聊天尚未登录") return false var send_err: Error = _websocket_manager.send_message(JSON.stringify(message_data)) if send_err != OK: _handle_error("SEND_FAILED", "WebSocket send failed: %s" % error_string(send_err)) return false return true func _send_chat_payload(content: String, scope: String = "local", extra_data: Dictionary = {}) -> bool: var normalized_content := content.strip_edges() if normalized_content.is_empty(): return false var show_bubble: bool = bool(extra_data.get("bubble", extra_data.get("showBubble", extra_data.get("show_bubble", false)))) and _settings_bool("show_chat_bubbles", true) var suppress_local_echo := bool(extra_data.get("_suppressLocalEcho", false)) var is_recovery_retry := bool(extra_data.get("_isRecoveryRetry", false)) var recoverable_extra_data := extra_data.duplicate(true) recoverable_extra_data.erase("_suppressLocalEcho") # 检查消息长度 if normalized_content.length() > MAX_MESSAGE_LENGTH: _handle_error("CONTENT_TOO_LONG", "消息内容过长") return false # 检查频率限制 if not is_recovery_retry and not can_send_message(): var wait_time := get_time_until_next_message() _handle_error("RATE_LIMIT", "请等待 %.1f 秒后再试" % wait_time) return false # 检查连接状态 if not _websocket_manager.is_websocket_connected(): if _game_token.strip_edges().is_empty(): var authToken := _get_auth_manager_token() if not authToken.is_empty(): _game_token = authToken if _game_token.strip_edges().is_empty(): _handle_error("AUTH_FAILED", "请先登录后再使用聊天") return false if not _queue_message_until_ready(normalized_content, scope, extra_data): return false connect_to_chat_server() return true # 检查登录状态 if not _is_logged_in: return _queue_message_until_ready(normalized_content, scope, extra_data) # 构建消息数据 var message_data := { "type": "chat", "content": normalized_content, "scope": scope, "bubble": show_bubble } for key in extra_data.keys(): if str(key).begins_with("_"): continue message_data[key] = extra_data[key] # 发送消息(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 false _track_recoverable_message({ "content": normalized_content, "scope": scope, "extra_data": recoverable_extra_data }) # 记录发送时间 if not is_recovery_retry: _record_message_timestamp() var now_timestamp: float = Time.get_unix_time_from_system() if not suppress_local_echo: # 添加到历史 _add_message_to_history({ "from_user": _current_username, "content": normalized_content, "timestamp": now_timestamp, "is_self": true, "show_bubble": show_bubble, "scope": scope, "to_user_id": str(extra_data.get("targetUserId", "")), "to_username": str(extra_data.get("targetUsername", "")), "private_context": str(extra_data.get("privateContext", "")), "is_private": scope == "private" }) # 记录待去重的“自己消息”(如果服务端也回发 chat_render,则避免重复显示) _pending_self_messages.append({ "content": normalized_content, "scope": scope, "targetUserId": str(extra_data.get("targetUserId", "")), "privateContext": str(extra_data.get("privateContext", "")), "expires_at": now_timestamp + SELF_ECHO_DEDUPE_WINDOW }) # 本地回显:UI 目前只订阅 CHAT_MESSAGE_RECEIVED,所以这里也发一次 received chat_message_received.emit(_current_username, normalized_content, show_bubble, now_timestamp) _emit_event(EventNames.CHAT_MESSAGE_RECEIVED, { "from_user": _current_username, "content": normalized_content, "show_bubble": show_bubble, "timestamp": now_timestamp, "is_self": true, "scope": scope, "to_user_id": str(extra_data.get("targetUserId", "")), "to_username": str(extra_data.get("targetUsername", "")), "private_context": str(extra_data.get("privateContext", "")), "is_private": scope == "private" }) return true # 消息发送完成回调 func _on_chat_message_sent(_request_id: String, success: bool, data: Dictionary, error_info: Dictionary) -> void: if success: 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) _emit_event(EventNames.CHAT_MESSAGE_SENT, { "message_id": message_id, "timestamp": timestamp }) else: _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: update_player_position_with_appearance(x, y, map_id, {}) func update_player_position_with_appearance(x: float, y: float, map_id: String, _appearance: Dictionary = {}) -> void: if not _websocket_manager.is_websocket_connected(): connect_to_chat_server() return if not _is_logged_in: 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) # ============================================================================ # 公共 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() # 重置当前会话(每次登录/重连时调用) # # 功能: # - 清空当前会话消息缓存 # - 重置历史消息加载状态 # - 不影响 Zulip 后端的历史消息 # # 使用场景: # - 用户登录成功后 # - 重新连接到聊天服务器后 func reset_session() -> void: _message_history.clear() _history_loading = false _has_more_history = true _oldest_message_timestamp = 0.0 # 加载历史消息(按需从 Zulip 后端获取) # # 参数: # count: int - 要加载的消息数量(默认 HISTORY_PAGE_SIZE) # # 功能: # - 从 Zulip 后端获取历史消息 # - 添加到当前会话历史开头 # - 触发 CHAT_MESSAGE_RECEIVED 事件显示消息 # # 使用场景: # - 用户滚动到聊天窗口顶部 # - 用户主动点击"加载历史"按钮 # func load_history(count: int = HISTORY_PAGE_SIZE) -> void: if _history_loading: return if not _has_more_history: return _history_loading = true var token := _get_auth_manager_token() if token.is_empty(): _history_loading = false return var mapId := _current_map.strip_edges() if mapId.is_empty(): mapId = "whale_port" var url := "%s/chat/history?mapId=%s&limit=%d&offset=%d" % [ NetworkConfig.get_api_base_url(), mapId.uri_encode(), max(1, count), _message_history.size(), ] var err := _history_request.request(url, PackedStringArray([ "Accept: application/json", "Authorization: Bearer %s" % token, ]), HTTPClient.METHOD_GET) if err != OK: _history_loading = false _handle_error("INTERNAL_ERROR", "聊天历史请求发送失败") func _on_history_request_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void: if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300: _history_loading = false _handle_error("INTERNAL_ERROR", "聊天历史读取失败") return var json := JSON.new() if json.parse(body.get_string_from_utf8()) != OK or not (json.data is Dictionary): _history_loading = false _handle_error("INTERNAL_ERROR", "聊天历史响应解析失败") return var response: Dictionary = json.data var messagesVariant: Variant = response.get("messages", []) var dataVariant: Variant = response.get("data", {}) if messagesVariant is Array and (messagesVariant as Array).is_empty() and dataVariant is Dictionary: messagesVariant = (dataVariant as Dictionary).get("messages", []) if messagesVariant is Array: _on_history_loaded(_normalize_history_messages(messagesVariant as Array)) return _history_loading = false _has_more_history = false func _normalize_history_messages(messages: Array) -> Array: var normalized: Array = [] for messageVariant in messages: if not (messageVariant is Dictionary): continue var message: Dictionary = messageVariant normalized.append({ "from_user": str(message.get("sender", message.get("from_user", ""))), "from_user_id": str(message.get("fromUserId", message.get("from_user_id", ""))), "content": str(message.get("content", "")), "timestamp": _parse_chat_timestamp_to_unix(message.get("timestamp", 0.0)), "is_self": str(message.get("sender", "")) == _current_username, "scope": str(message.get("scope", "local")), "show_bubble": bool(message.get("bubble", false)), "is_history": true, }) return normalized # 历史消息加载完成回调 func _on_history_loaded(messages: Array) -> void: _history_loading = false if messages.is_empty(): _has_more_history = false return # 将历史消息插入到当前会话历史开头 for i in range(messages.size() - 1, -1, -1): var message: Dictionary = messages[i] _message_history.push_front(message) # 触发事件显示消息(Signal Up) _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) _websocket_manager.connection_lost.connect(_on_connection_lost) func _on_connection_lost() -> void: _is_logged_in = false _world_ready_map = "" _restore_recoverable_messages_to_queue() func _load_token_from_environment() -> void: var env_token: String = OS.get_environment(GAME_TOKEN_ENV_KEY).strip_edges() if not env_token.is_empty(): _game_token = env_token func _is_headless_run() -> bool: if OS.has_feature("headless"): return true return DisplayServer.get_name() == "headless" func _get_auth_manager_token() -> String: var authManager := get_node_or_null("/root/AuthManager") if authManager == null or not authManager.has_method("get_access_token"): return "" return str(authManager.call("get_access_token")).strip_edges() func _queue_message_until_ready(content: String, scope: String, extra_data: Dictionary = {}) -> bool: if _queued_messages.size() >= MAX_QUEUED_MESSAGES: _handle_error("RATE_LIMIT", "聊天正在连接,请稍后再试") return false _queued_messages.append({ "content": content, "scope": scope, "extra_data": extra_data.duplicate(true) }) return true func _track_recoverable_message(message: Dictionary) -> void: _recoverable_messages.append(message.duplicate(true)) if _recoverable_messages.size() > MAX_RECOVERABLE_MESSAGES: _recoverable_messages.pop_front() func _restore_recoverable_messages_to_queue() -> void: if _recoverable_messages.is_empty(): return var messages := _recoverable_messages.duplicate(true) _recoverable_messages.clear() for message_variant in messages: if not (message_variant is Dictionary): continue if _queued_messages.size() >= MAX_QUEUED_MESSAGES: break var message: Dictionary = message_variant var extra_data: Dictionary = {} var extra_variant: Variant = message.get("extra_data", {}) if extra_variant is Dictionary: extra_data = (extra_variant as Dictionary).duplicate(true) extra_data["_suppressLocalEcho"] = true extra_data["_isRecoveryRetry"] = true _queued_messages.append({ "content": str(message.get("content", "")), "scope": str(message.get("scope", "local")), "extra_data": extra_data }) func _flush_queued_messages() -> void: if _queued_messages.is_empty(): return var queued_messages := _queued_messages.duplicate() _queued_messages.clear() for queued_message in queued_messages: var queued_content: String = str(queued_message.get("content", "")) var queued_scope: String = str(queued_message.get("scope", "local")) var queued_extra_data: Dictionary = {} var extra_variant: Variant = queued_message.get("extra_data", {}) if extra_variant is Dictionary: queued_extra_data = extra_variant _send_chat_payload(queued_content, queued_scope, queued_extra_data) func _http_request_result_to_string(result: int) -> String: match result: HTTPRequest.RESULT_SUCCESS: return "SUCCESS" HTTPRequest.RESULT_CHUNKED_BODY_SIZE_MISMATCH: return "CHUNKED_BODY_SIZE_MISMATCH" HTTPRequest.RESULT_CANT_CONNECT: return "CANT_CONNECT" HTTPRequest.RESULT_CANT_RESOLVE: return "CANT_RESOLVE" HTTPRequest.RESULT_CONNECTION_ERROR: return "CONNECTION_ERROR" HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR: return "TLS_HANDSHAKE_ERROR" HTTPRequest.RESULT_NO_RESPONSE: return "NO_RESPONSE" HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED: return "BODY_SIZE_LIMIT_EXCEEDED" HTTPRequest.RESULT_BODY_DECOMPRESS_FAILED: return "BODY_DECOMPRESS_FAILED" HTTPRequest.RESULT_REQUEST_FAILED: return "REQUEST_FAILED" HTTPRequest.RESULT_DOWNLOAD_FILE_CANT_OPEN: return "DOWNLOAD_FILE_CANT_OPEN" HTTPRequest.RESULT_DOWNLOAD_FILE_WRITE_ERROR: return "DOWNLOAD_FILE_WRITE_ERROR" HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED: return "REDIRECT_LIMIT_REACHED" HTTPRequest.RESULT_TIMEOUT: return "TIMEOUT" _: return "UNKNOWN_%d" % result func _preview_text(text: String) -> String: var preview := text.strip_edges() if preview.length() > 180: return preview.substr(0, 180) + "..." return preview # 发送登录消息 func _send_login_message() -> void: if _game_token.strip_edges().is_empty(): _handle_error("AUTH_FAILED", "缺少聊天认证 token") return var login_data := { "type": "login", "token": _game_token } var json_string := JSON.stringify(login_data) _websocket_manager.send_message(json_string) # 连接状态变化 func _on_connection_state_changed(state: int) -> void: # 发射信号 chat_connection_state_changed.emit(state) # 通过 EventSystem 广播(Signal Up) _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: push_error("ChatManager: JSON 解析失败") return var data_variant: Variant = json.data if not (data_variant is Dictionary): push_warning("ChatManager: 收到非对象消息,已忽略") return var data: Dictionary = data_variant # 兼容不同后端字段命名:t / type var message_type: String = str(data.get("t", data.get("type", ""))).strip_edges() if message_type.is_empty(): _warn_empty_message_type_limited(data) return match message_type: "connected": pass "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) "friend_list": _handle_friend_list(data) "friend_added": _handle_friend_added(data) "friend_request_received": _handle_friend_request_received(data) "friend_request_sent": _handle_friend_request_sent(data) "friend_request_accepted": _handle_friend_request_accepted(data) "friend_request_rejected": _handle_friend_request_rejected(data) "friend_removed": _handle_friend_removed(data) "friend_error": _handle_friend_error(data) "map_players_snapshot": _handle_map_players_snapshot(data) "system_presence": _handle_system_presence(data) "position_update": _handle_position_update(data) "player_joined": _handle_player_joined(data) "appearance_changed": _handle_player_joined(data) "world_ready_success": pass "world_left": _world_ready_map = "" "player_left": _handle_player_left(data) "position_updated": _handle_position_updated(data) "error": _handle_error_response(data) _: push_warning("ChatManager: 未处理的消息类型 %s" % message_type) func _warn_empty_message_type_limited(data: Dictionary) -> void: var now: float = Time.get_unix_time_from_system() if now - _last_empty_message_type_warning_at < EMPTY_MESSAGE_TYPE_WARNING_INTERVAL: return _last_empty_message_type_warning_at = now var payload_preview: String = JSON.stringify(data) if payload_preview.length() > 180: payload_preview = payload_preview.substr(0, 180) + "..." push_warning("ChatManager: 收到未带消息类型的消息,已忽略 payload=%s" % payload_preview) # 处理登录成功 func _handle_login_success(data: Dictionary) -> void: _is_logged_in = true _refreshing_auth_session = false _chat_login_refresh_attempted = false _friend_protocol_supported = true _current_username = data.get("username", "") _current_map = data.get("currentMap", "") _world_ready_map = "" # 重置当前会话缓存(每次登录/重连都清空,重新开始接收消息) reset_session() # 通过 EventSystem 广播(Signal Up) _emit_event(EventNames.CHAT_LOGIN_SUCCESS, { "username": _current_username, "current_map": _current_map }) _flush_queued_messages() request_friend_list() func _handle_system_presence(data: Dictionary) -> void: var content := str(data.get("content", "")).strip_edges() if content.is_empty(): return var timestamp := _parse_chat_timestamp_to_unix(data.get("timestamp", 0.0)) var message := { "from_user": "系统", "from_user_id": "", "content": content, "timestamp": timestamp, "is_self": false, "scope": "global", "show_bubble": false, "system_presence": true, } _add_message_to_history(message) chat_message_received.emit("系统", content, false, timestamp) _emit_event(EventNames.CHAT_MESSAGE_RECEIVED, message) func _on_account_profile_updated(_profile: Dictionary) -> void: notify_appearance_changed() func _on_auth_state_changed(isAuthenticated: bool, _user: Dictionary) -> void: if not isAuthenticated: _refreshing_auth_session = false _game_token = "" disconnect_from_chat_server() return var refreshedToken := _get_auth_manager_token() if refreshedToken.is_empty(): return _game_token = refreshedToken if not _refreshing_auth_session: return _refreshing_auth_session = false if _websocket_manager.is_websocket_connected(): _send_login_message() else: connect_to_chat_server() # 处理登录失败 func _handle_login_error(data: Dictionary) -> void: var error_message: String = data.get("message", "登录失败") _is_logged_in = false if _request_auth_session_refresh(): return _queued_messages.clear() # 通过 EventSystem 广播错误(Signal Up) _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) # 发射信号 chat_message_sent.emit(message_id, timestamp) # 通过 EventSystem 广播(Signal Up) _emit_event(EventNames.CHAT_MESSAGE_SENT, { "message_id": message_id, "timestamp": timestamp }) if not _recoverable_messages.is_empty(): _recoverable_messages.pop_front() # 处理聊天消息发送失败 func _handle_chat_error(data: Dictionary) -> void: var error_message: String = data.get("message", "消息发送失败") var error_code: String = str(data.get("code", data.get("error_code", "CHAT_SEND_FAILED"))).strip_edges() if error_code == "SESSION_EXPIRED": _handle_session_expired(error_message) return # 通过 EventSystem 广播错误(Signal Up) _emit_event(EventNames.CHAT_ERROR_OCCURRED, { "error_code": error_code if not error_code.is_empty() else "CHAT_SEND_FAILED", "message": error_message }) func _handle_friend_list(data: Dictionary) -> void: _friends.clear() var friends_variant: Variant = data.get("friends", []) if friends_variant is Array: for friend_variant in friends_variant: if friend_variant is Dictionary: var friend: Dictionary = friend_variant _friends.append({ "user_id": str(friend.get("userId", friend.get("user_id", ""))).strip_edges(), "username": str(friend.get("username", "")), "online": bool(friend.get("online", false)) }) _emit_event(EventNames.CHAT_FRIENDS_UPDATED, { "friends": _friends.duplicate(true), "requests": _normalize_friend_requests(data.get("requests", [])), "supported": true, "status": "" }) func _handle_friend_added(data: Dictionary) -> void: var friend_variant: Variant = data.get("friend", {}) if friend_variant is Dictionary: var friend: Dictionary = friend_variant var friend_user_id := str(friend.get("userId", friend.get("user_id", ""))).strip_edges() if not friend_user_id.is_empty(): _upsert_friend({ "user_id": friend_user_id, "username": str(friend.get("username", "")), "online": bool(friend.get("online", false)) }) request_friend_list() func _handle_friend_request_received(data: Dictionary) -> void: _emit_event(EventNames.CHAT_FRIENDS_UPDATED, { "friends": _friends.duplicate(true), "requests": _normalize_friend_requests([data.get("request", {})]) }) request_friend_list() func _handle_friend_request_sent(_data: Dictionary) -> void: _emit_event(EventNames.CHAT_ERROR_OCCURRED, { "error_code": "FRIEND_REQUEST_SENT", "message": "好友请求已发送" }) func _handle_friend_request_accepted(_data: Dictionary) -> void: request_friend_list() func _handle_friend_request_rejected(data: Dictionary) -> void: var username := str(data.get("username", "")).strip_edges() var message := "好友请求已被拒绝" if not username.is_empty(): message = "%s 拒绝了好友请求" % username _emit_event(EventNames.CHAT_ERROR_OCCURRED, { "error_code": "FRIEND_REQUEST_REJECTED", "message": message }) func _handle_friend_removed(data: Dictionary) -> void: var friend_user_id := str(data.get("friendUserId", data.get("friend_user_id", ""))).strip_edges() if friend_user_id.is_empty(): return for i in range(_friends.size() - 1, -1, -1): if str(_friends[i].get("user_id", "")) == friend_user_id: _friends.remove_at(i) _emit_event(EventNames.CHAT_FRIENDS_UPDATED, { "friends": _friends.duplicate(true), "requests": _normalize_friend_requests(data.get("requests", [])), "supported": true, "status": "" }) func _handle_friend_error(data: Dictionary) -> void: var error_code := str(data.get("code", data.get("error_code", "FRIEND_ERROR"))).strip_edges() var error_message := str(data.get("message", "好友操作失败")) if error_code == "SESSION_EXPIRED": _handle_session_expired(error_message) return _handle_error(error_code if not error_code.is_empty() else "FRIEND_ERROR", error_message) func _upsert_friend(friend: Dictionary) -> void: var friend_user_id := str(friend.get("user_id", "")).strip_edges() if friend_user_id.is_empty(): return for i in range(_friends.size()): if str(_friends[i].get("user_id", "")) == friend_user_id: _friends[i] = friend _emit_event(EventNames.CHAT_FRIENDS_UPDATED, { "friends": _friends.duplicate(true), "requests": [] }) return _friends.append(friend) _emit_event(EventNames.CHAT_FRIENDS_UPDATED, { "friends": _friends.duplicate(true), "requests": [] }) # 处理接收到的聊天消息 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 scope: String = str(data.get("scope", "local")).strip_edges().to_lower() if scope.is_empty(): scope = "local" var from_user_id: String = str(data.get("fromUserId", data.get("from_user_id", data.get("userId", data.get("user_id", ""))))).strip_edges() var to_user_id: String = str(data.get("toUserId", data.get("to_user_id", data.get("targetUserId", data.get("target_user_id", ""))))).strip_edges() var to_username: String = str(data.get("toUsername", data.get("to_username", data.get("targetUsername", data.get("target_username", ""))))).strip_edges() var private_context: String = str(data.get("privateContext", data.get("private_context", ""))).strip_edges() var is_private: bool = scope == "private" 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, scope, to_user_id): # 已经本地回显过,避免重复显示 return # 如果服务端没带发送者信息,但内容匹配最近自己发送的消息,则认为是自己消息 if from_user.is_empty() and _consume_pending_self_message(content, scope, to_user_id): from_user = _current_username is_self = true # 添加到历史 _add_message_to_history({ "from_user": from_user, "from_user_id": from_user_id, "content": content, "timestamp": timestamp, "is_self": is_self, "scope": scope, "to_user_id": to_user_id, "to_username": to_username, "private_context": private_context, "is_private": is_private }) # 发射信号 chat_message_received.emit(from_user, content, show_bubble, timestamp) # 通过 EventSystem 广播(Signal Up) _emit_event(EventNames.CHAT_MESSAGE_RECEIVED, { "from_user": from_user, "from_user_id": from_user_id, "content": content, "show_bubble": show_bubble, "timestamp": timestamp, "is_self": is_self, "scope": scope, "to_user_id": to_user_id, "to_username": to_username, "private_context": private_context, "is_private": is_private }) # 解析聊天消息时间戳(兼容 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", "") # 发射信号 chat_position_updated.emit(stream, topic) # 通过 EventSystem 广播(Signal Up) _emit_event(EventNames.CHAT_POSITION_UPDATED, { "stream": stream, "topic": topic }) func _handle_position_update(data: Dictionary) -> void: var user_id := str(data.get("userId", data.get("user_id", ""))).strip_edges() if user_id.is_empty(): return var appearance := _normalize_player_appearance(data) _emit_event(EventNames.REMOTE_PLAYER_POSITION_UPDATED, { "userId": user_id, "username": str(data.get("username", "")), "position": Vector2(float(data.get("x", 0.0)), float(data.get("y", 0.0))), "mapId": str(data.get("mapId", data.get("map_id", ""))), "skin_id": str(appearance.get("skin_id", "")), "avatar_id": str(appearance.get("avatar_id", "")), "skin_asset": appearance.get("skin_asset", {}), "cafe_companion": _normalize_cafe_companion(data.get("cafeCompanion", data.get("cafe_companion", null))), "movement_locked": bool(data.get("movementLocked", data.get("movement_locked", false))) }) func _handle_map_players_snapshot(data: Dictionary) -> void: var players: Array[Dictionary] = [] var players_variant: Variant = data.get("players", []) if players_variant is Array: for player_variant in players_variant: if not (player_variant is Dictionary): continue var player: Dictionary = player_variant var user_id := str(player.get("userId", player.get("user_id", ""))).strip_edges() if user_id.is_empty(): continue var appearance := _normalize_player_appearance(player) players.append({ "userId": user_id, "username": str(player.get("username", "")), "position": Vector2(float(player.get("x", 0.0)), float(player.get("y", 0.0))), "mapId": str(player.get("mapId", player.get("map_id", data.get("mapId", data.get("map_id", ""))))), "skin_id": str(appearance.get("skin_id", "")), "avatar_id": str(appearance.get("avatar_id", "")), "skin_asset": appearance.get("skin_asset", {}), "cafe_companion": _normalize_cafe_companion(player.get("cafeCompanion", player.get("cafe_companion", null))), "movement_locked": bool(player.get("movementLocked", player.get("movement_locked", false))) }) _emit_event(EventNames.REMOTE_PLAYERS_SNAPSHOT_READY, { "mapId": str(data.get("mapId", data.get("map_id", ""))), "players": players }) func _handle_player_joined(data: Dictionary) -> void: var user_id := str(data.get("userId", data.get("user_id", ""))).strip_edges() if user_id.is_empty(): return var appearance := _normalize_player_appearance(data) _emit_event(EventNames.REMOTE_PLAYER_JOINED, { "userId": user_id, "username": str(data.get("username", "")), "position": Vector2(float(data.get("x", 0.0)), float(data.get("y", 0.0))), "mapId": str(data.get("mapId", data.get("map_id", ""))), "skin_id": str(appearance.get("skin_id", "")), "avatar_id": str(appearance.get("avatar_id", "")), "skin_asset": appearance.get("skin_asset", {}), "cafe_companion": _normalize_cafe_companion(data.get("cafeCompanion", data.get("cafe_companion", null))), "movement_locked": bool(data.get("movementLocked", data.get("movement_locked", false))) }) func _normalize_player_appearance(data: Dictionary) -> Dictionary: var appearanceVariant: Variant = data.get("appearance", {}) var appearance: Dictionary = appearanceVariant if appearanceVariant is Dictionary else {} return { "skin_id": str(data.get("skinId", data.get("skin_id", appearance.get("skinId", appearance.get("skin_id", ""))))), "avatar_id": str(data.get("avatarId", data.get("avatar_id", appearance.get("avatarId", appearance.get("avatar_id", ""))))), "skin_asset": data.get("skinAsset", data.get("skin_asset", appearance.get("skinAsset", appearance.get("skin_asset", {})))), } func _normalize_cafe_companion(value: Variant) -> Dictionary: if not (value is Dictionary): return {} var raw: Dictionary = value var servicePointId := str(raw.get("servicePointId", raw.get("service_point_id", ""))).strip_edges() var companionId := str(raw.get("companionId", raw.get("companion_id", ""))).strip_edges() var personaName := str(raw.get("personaName", raw.get("persona_name", ""))).strip_edges() if servicePointId.is_empty() or companionId.is_empty() or personaName.is_empty(): return {} return { "cafe_id": str(raw.get("cafeId", raw.get("cafe_id", "whale_cafe"))).strip_edges(), "service_point_id": servicePointId, "companion_id": companionId, "companion_type": str(raw.get("companionType", raw.get("companion_type", "hired_player"))).strip_edges(), "persona_name": personaName, "employment_ends_at": str(raw.get("employmentEndsAt", raw.get("employment_ends_at", ""))).strip_edges(), "owner_user_id": str(raw.get("ownerUserId", raw.get("owner_user_id", ""))).strip_edges(), } func _handle_player_left(data: Dictionary) -> void: var user_id := str(data.get("userId", data.get("user_id", ""))).strip_edges() if user_id.is_empty(): return _emit_event(EventNames.REMOTE_PLAYER_LEFT, { "userId": user_id, "username": str(data.get("username", "")), "mapId": str(data.get("mapId", data.get("map_id", ""))) }) # 处理错误响应(如果需要) func _handle_error_response(data: Dictionary) -> void: var error_code: String = str(data.get("code", "")) var error_message: String = str(data.get("message", "")) var unknown_message_type := _extract_unknown_message_type(error_message) if _is_friend_protocol_message_type(unknown_message_type): _handle_friend_protocol_unsupported(unknown_message_type) return if error_code == "SESSION_EXPIRED": _handle_session_expired(error_message) return _handle_error(error_code, error_message) func _handle_session_expired(error_message: String = "") -> void: push_warning("ChatManager: [SESSION_EXPIRED] %s" % error_message) _is_logged_in = false _restore_recoverable_messages_to_queue() _chat_login_refresh_attempted = false if _request_auth_session_refresh(): return _game_token = _get_auth_manager_token() if _game_token.is_empty(): _handle_error("AUTH_FAILED", "请先登录后再使用聊天") return if _websocket_manager.is_websocket_connected(): _send_login_message() else: connect_to_chat_server() func _request_auth_session_refresh() -> bool: if _refreshing_auth_session or _chat_login_refresh_attempted: return false var authManager := get_node_or_null("/root/AuthManager") if authManager == null or not authManager.has_method("refresh_session") or not authManager.has_method("get_refresh_token"): return false if str(authManager.call("get_refresh_token")).strip_edges().is_empty(): return false _chat_login_refresh_attempted = true _refreshing_auth_session = true authManager.call("refresh_session") return true # 处理 Socket 错误(如果需要) func _on_socket_error(error: String) -> void: _handle_error("SOCKET_ERROR", error) func _normalize_friend_requests(requests_variant: Variant) -> Array[Dictionary]: var requests: Array[Dictionary] = [] if not (requests_variant is Array): return requests for request_variant in requests_variant: if not (request_variant is Dictionary): continue var request: Dictionary = request_variant var user_id := str(request.get("userId", request.get("user_id", ""))).strip_edges() if user_id.is_empty(): continue requests.append({ "user_id": user_id, "username": str(request.get("username", "玩家")).strip_edges(), "created_at": str(request.get("createdAt", request.get("created_at", ""))).strip_edges() }) return requests func _extract_unknown_message_type(error_message: String) -> String: var normalized_message := error_message.strip_edges() for marker in ["未知消息类型:", "未知消息类型:", "Unknown message type:", "Unknown message type:"]: var index := normalized_message.find(marker) if index >= 0: return normalized_message.substr(index + marker.length()).strip_edges() return "" func _is_friend_protocol_message_type(message_type: String) -> bool: match message_type.strip_edges(): "friend_list", "friend_add", "friend_remove", "friend_request", "friend_accept", "friend_reject": return true _: return false func _handle_friend_protocol_unsupported(message_type: String) -> void: _friend_protocol_supported = false push_warning("ChatManager: 当前聊天后端暂不支持好友协议: %s" % message_type) _emit_friend_protocol_status("当前聊天后端暂不支持好友功能") func _emit_friend_protocol_status(status_message: String) -> void: _emit_event(EventNames.CHAT_FRIENDS_UPDATED, { "friends": _friends.duplicate(true), "requests": [], "supported": false, "status": status_message }) # ============================================================================ # 内部方法 - 工具函数 # ============================================================================ # 处理错误 func _handle_error(error_code: String, error_message: String) -> void: if error_code == "NOT_CONNECTED" or error_code == "NOT_LOGGED_IN": push_warning("ChatManager: [%s] %s" % [error_code, error_message]) else: push_error("ChatManager: [%s] %s" % [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) _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 _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, scope: String = "", target_user_id: 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): var item: Dictionary = _pending_self_messages[i] var same_content := str(item.get("content", "")) == content var same_scope := scope.is_empty() or str(item.get("scope", "")).is_empty() or str(item.get("scope", "")) == scope var same_target := target_user_id.is_empty() or str(item.get("targetUserId", "")) == target_user_id if same_content and same_scope and same_target: _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() func _settings_bool(key: String, defaultValue: bool) -> bool: var settingsManager := get_node_or_null("/root/SettingsManager") if settingsManager != null and settingsManager.has_method("get_bool"): return bool(settingsManager.call("get_bool", key)) return defaultValue