Files
whale-town-front-v2/_Core/managers/ChatManager.gd

1626 lines
55 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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