- 修复 WebSocketManager/SocketIOClient 函数缩进错误 - 重命名 is_connected() 避免与 Object 基类冲突 - 修复 tscn 文件多余前导空格 - 修复测试文件 GUT 断言函数调用 - 添加 GUT 测试框架
642 lines
19 KiB
GDScript
642 lines
19 KiB
GDScript
extends Node
|
||
|
||
# ============================================================================
|
||
# ChatManager.gd - 聊天系统业务逻辑核心
|
||
# ============================================================================
|
||
# 管理聊天功能的核心业务逻辑
|
||
#
|
||
# 核心职责:
|
||
# - 聊天消息发送/接收协调
|
||
# - 客户端频率限制(10条/分钟)
|
||
# - 消息历史管理(最多100条)
|
||
# - Signal Up: 通过信号和 EventSystem 向上通知
|
||
# - 整合 AuthManager 获取 token
|
||
#
|
||
# 使用方式:
|
||
# ChatManager.connect_to_chat_server()
|
||
# ChatManager.send_chat_message("Hello", "local")
|
||
# ChatManager.chat_message_received.connect(_on_message_received)
|
||
#
|
||
# 注意事项:
|
||
# - 作为自动加载单例,全局可访问
|
||
# - 遵循 "Signal Up, Call Down" 架构
|
||
# - 所有聊天事件通过 EventSystem 广播
|
||
# ============================================================================
|
||
|
||
# ============================================================================
|
||
# 信号定义 (Signal Up)
|
||
# ============================================================================
|
||
|
||
# 聊天消息已发送信号
|
||
# 参数:
|
||
# message_id: String - 消息 ID
|
||
# timestamp: float - 时间戳
|
||
signal chat_message_sent(message_id: String, timestamp: float)
|
||
|
||
# 聊天消息已接收信号
|
||
# 参数:
|
||
# from_user: String - 发送者用户名
|
||
# content: String - 消息内容
|
||
# show_bubble: bool - 是否显示气泡
|
||
# timestamp: float - 时间戳
|
||
signal chat_message_received(from_user: String, content: String, show_bubble: bool, timestamp: float)
|
||
|
||
# 聊天错误发生信号
|
||
# 参数:
|
||
# error_code: String - 错误代码
|
||
# message: String - 错误消息
|
||
signal chat_error_occurred(error_code: String, message: String)
|
||
|
||
# 聊天连接状态变化信号
|
||
# 参数:
|
||
# state: int - 连接状态(0=DISCONNECTED, 1=CONNECTING, 2=CONNECTED, 3=RECONNECTING, 4=ERROR)
|
||
signal chat_connection_state_changed(state: int)
|
||
|
||
# 位置更新成功信号
|
||
# 参数:
|
||
# stream: String - Stream 名称
|
||
# topic: String - Topic 名称
|
||
signal chat_position_updated(stream: String, topic: String)
|
||
|
||
# ============================================================================
|
||
# 常量定义
|
||
# ============================================================================
|
||
|
||
# WebSocket 服务器 URL
|
||
const WEBSOCKET_URL: String = "wss://whaletownend.xinghangee.icu/game"
|
||
|
||
# 重连配置
|
||
const RECONNECT_MAX_ATTEMPTS: int = 5
|
||
const RECONNECT_BASE_DELAY: float = 3.0
|
||
|
||
# 频率限制配置
|
||
const RATE_LIMIT_MESSAGES: int = 10
|
||
const RATE_LIMIT_WINDOW: float = 60.0 # 秒
|
||
|
||
# 消息限制
|
||
const MAX_MESSAGE_LENGTH: int = 1000
|
||
|
||
# 当前会话消息限制(当前游戏会话,超过后删除最旧的)
|
||
const MAX_SESSION_MESSAGES: int = 100
|
||
|
||
# 历史消息分页大小(从 Zulip 后端每次加载的数量)
|
||
const HISTORY_PAGE_SIZE: int = 100
|
||
|
||
# 错误消息映射
|
||
const CHAT_ERROR_MESSAGES: Dictionary = {
|
||
"AUTH_FAILED": "聊天认证失败,请重新登录",
|
||
"RATE_LIMIT": "消息发送过于频繁,请稍后再试",
|
||
"CONTENT_FILTERED": "消息内容包含违规内容",
|
||
"CONTENT_TOO_LONG": "消息内容过长(最大1000字符)",
|
||
"PERMISSION_DENIED": "您没有权限发送消息",
|
||
"SESSION_EXPIRED": "会话已过期,请重新连接",
|
||
"ZULIP_ERROR": "消息服务暂时不可用",
|
||
"INTERNAL_ERROR": "服务器内部错误"
|
||
}
|
||
|
||
# ============================================================================
|
||
# 成员变量
|
||
# ============================================================================
|
||
|
||
# WebSocket 管理器
|
||
var _websocket_manager: WebSocketManager
|
||
|
||
# Socket.IO 客户端
|
||
var _socket_client: SocketIOClient
|
||
|
||
# 是否已登录
|
||
var _is_logged_in: bool = false
|
||
|
||
# 消息历史记录(当前会话,最多100条,超过后删除最旧的)
|
||
var _message_history: Array[Dictionary] = []
|
||
|
||
# 历史消息加载状态
|
||
var _history_loading: bool = false
|
||
var _has_more_history: bool = true
|
||
var _oldest_message_timestamp: float = 0.0
|
||
|
||
# 消息发送时间戳(用于频率限制)
|
||
var _message_timestamps: Array[float] = []
|
||
|
||
# 当前用户信息
|
||
var _current_username: String = ""
|
||
var _current_map: String = ""
|
||
|
||
# 游戏 token
|
||
var _game_token: String = ""
|
||
|
||
# ============================================================================
|
||
# 生命周期方法
|
||
# ============================================================================
|
||
|
||
# 初始化
|
||
func _ready() -> void:
|
||
print("ChatManager 初始化完成")
|
||
|
||
# 创建 WebSocket 管理器
|
||
_websocket_manager = WebSocketManager.new()
|
||
add_child(_websocket_manager)
|
||
|
||
# 获取 Socket.IO 客户端引用
|
||
_socket_client = _websocket_manager.get_socket_client()
|
||
|
||
# 连接信号
|
||
_connect_signals()
|
||
|
||
# 清理
|
||
func _exit_tree() -> void:
|
||
if is_instance_valid(_websocket_manager):
|
||
_websocket_manager.queue_free()
|
||
|
||
# ============================================================================
|
||
# 公共 API - Token 管理
|
||
# ============================================================================
|
||
|
||
# 设置游戏 token
|
||
#
|
||
# 参数:
|
||
# token: String - 游戏认证 token
|
||
#
|
||
# 使用示例:
|
||
# ChatManager.set_game_token("your_game_token")
|
||
func set_game_token(token: String) -> void:
|
||
_game_token = token
|
||
print("ChatManager: 游戏 token 已设置")
|
||
|
||
# 获取游戏 token
|
||
#
|
||
# 返回值:
|
||
# String - 当前游戏 token
|
||
func get_game_token() -> String:
|
||
return _game_token
|
||
|
||
# ============================================================================
|
||
# 公共 API - 连接管理
|
||
# ============================================================================
|
||
|
||
# 连接到聊天服务器
|
||
func connect_to_chat_server() -> void:
|
||
if _websocket_manager.is_websocket_connected():
|
||
push_warning("聊天服务器已连接")
|
||
return
|
||
|
||
print("=== ChatManager 开始连接 ===")
|
||
_websocket_manager.connect_to_game_server()
|
||
|
||
# 断开聊天服务器
|
||
func disconnect_from_chat_server() -> void:
|
||
print("=== ChatManager 断开连接 ===")
|
||
|
||
# 发送登出消息
|
||
if _is_logged_in:
|
||
var logout_data := {"type": "logout"}
|
||
_socket_client.emit("logout", logout_data)
|
||
_is_logged_in = false
|
||
|
||
# 断开连接
|
||
_websocket_manager.disconnect_websocket()
|
||
|
||
# 检查是否已连接
|
||
#
|
||
# 返回值:
|
||
# bool - 是否已连接
|
||
func is_chat_connected() -> bool:
|
||
return _websocket_manager.is_websocket_connected()
|
||
|
||
# ============================================================================
|
||
# 公共 API - 聊天操作
|
||
# ============================================================================
|
||
|
||
# 发送聊天消息
|
||
#
|
||
# 参数:
|
||
# content: String - 消息内容
|
||
# scope: String - 消息范围("local" 或具体 topic 名称)
|
||
#
|
||
# 使用示例:
|
||
# ChatManager.send_chat_message("Hello, world!", "local")
|
||
func send_chat_message(content: String, scope: String = "local") -> void:
|
||
# 检查连接状态
|
||
if not _websocket_manager.is_websocket_connected():
|
||
_handle_error("NOT_CONNECTED", "未连接到聊天服务器")
|
||
return
|
||
|
||
# 检查登录状态
|
||
if not _is_logged_in:
|
||
_handle_error("NOT_LOGGED_IN", "尚未登录聊天服务器")
|
||
return
|
||
|
||
# 检查消息长度
|
||
if content.length() > MAX_MESSAGE_LENGTH:
|
||
_handle_error("CONTENT_TOO_LONG", "消息内容过长")
|
||
return
|
||
|
||
# 检查频率限制
|
||
if not can_send_message():
|
||
var wait_time := get_time_until_next_message()
|
||
_handle_error("RATE_LIMIT", "请等待 %.1f 秒后再试" % wait_time)
|
||
return
|
||
|
||
# 构建消息数据
|
||
var message_data := {
|
||
"t": "chat",
|
||
"content": content,
|
||
"scope": scope
|
||
}
|
||
|
||
# 发送消息
|
||
_socket_client.emit("chat", message_data)
|
||
|
||
# 记录发送时间
|
||
_record_message_timestamp()
|
||
|
||
# 添加到历史
|
||
_add_message_to_history({
|
||
"from_user": _current_username,
|
||
"content": content,
|
||
"timestamp": Time.get_unix_time_from_system(),
|
||
"is_self": true
|
||
})
|
||
|
||
print("📤 发送聊天消息: ", content)
|
||
|
||
# 更新玩家位置
|
||
#
|
||
# 参数:
|
||
# x: float - X 坐标
|
||
# y: float - Y 坐标
|
||
# map_id: String - 地图 ID
|
||
#
|
||
# 使用示例:
|
||
# ChatManager.update_player_position(150.0, 200.0, "novice_village")
|
||
func update_player_position(x: float, y: float, map_id: String) -> void:
|
||
if not _websocket_manager.is_websocket_connected():
|
||
return
|
||
|
||
var position_data := {
|
||
"t": "position",
|
||
"x": x,
|
||
"y": y,
|
||
"mapId": map_id
|
||
}
|
||
|
||
_socket_client.emit("position", position_data)
|
||
print("📍 更新位置: (%.2f, %.2f) in %s" % [x, y, map_id])
|
||
|
||
# ============================================================================
|
||
# 公共 API - 频率限制
|
||
# ============================================================================
|
||
|
||
# 检查是否可以发送消息
|
||
#
|
||
# 返回值:
|
||
# bool - 是否可以发送
|
||
func can_send_message() -> bool:
|
||
var current_time := Time.get_unix_time_from_system()
|
||
|
||
# 清理过期的时间戳
|
||
var filter_func := func(timestamp: float) -> bool:
|
||
return current_time - timestamp < RATE_LIMIT_WINDOW
|
||
_message_timestamps = _message_timestamps.filter(filter_func)
|
||
|
||
# 检查数量
|
||
return _message_timestamps.size() < RATE_LIMIT_MESSAGES
|
||
|
||
# 获取距离下次可发送消息的时间
|
||
#
|
||
# 返回值:
|
||
# float - 等待时间(秒)
|
||
func get_time_until_next_message() -> float:
|
||
if _message_timestamps.is_empty():
|
||
return 0.0
|
||
|
||
if _message_timestamps.size() < RATE_LIMIT_MESSAGES:
|
||
return 0.0
|
||
|
||
# 找到最早的时间戳
|
||
var earliest_timestamp: float = _message_timestamps[0]
|
||
var current_time := Time.get_unix_time_from_system()
|
||
var elapsed := current_time - earliest_timestamp
|
||
|
||
if elapsed >= RATE_LIMIT_WINDOW:
|
||
return 0.0
|
||
|
||
return RATE_LIMIT_WINDOW - elapsed
|
||
|
||
# ============================================================================
|
||
# 公共 API - 消息历史
|
||
# ============================================================================
|
||
|
||
# 获取消息历史
|
||
#
|
||
# 返回值:
|
||
# Array[Dictionary] - 消息历史数组
|
||
func get_message_history() -> Array[Dictionary]:
|
||
return _message_history.duplicate()
|
||
|
||
# 清空消息历史
|
||
func clear_message_history() -> void:
|
||
_message_history.clear()
|
||
print("🧹 清空消息历史")
|
||
|
||
# 重置当前会话(每次登录/重连时调用)
|
||
#
|
||
# 功能:
|
||
# - 清空当前会话消息缓存
|
||
# - 重置历史消息加载状态
|
||
# - 不影响 Zulip 后端的历史消息
|
||
#
|
||
# 使用场景:
|
||
# - 用户登录成功后
|
||
# - 重新连接到聊天服务器后
|
||
func reset_session() -> void:
|
||
_message_history.clear()
|
||
_history_loading = false
|
||
_has_more_history = true
|
||
_oldest_message_timestamp = 0.0
|
||
print("🔄 重置聊天会话")
|
||
|
||
# 加载历史消息(按需从 Zulip 后端获取)
|
||
#
|
||
# 参数:
|
||
# count: int - 要加载的消息数量(默认 HISTORY_PAGE_SIZE)
|
||
#
|
||
# 功能:
|
||
# - 从 Zulip 后端获取历史消息
|
||
# - 添加到当前会话历史开头
|
||
# - 触发 CHAT_MESSAGE_RECEIVED 事件显示消息
|
||
#
|
||
# 使用场景:
|
||
# - 用户滚动到聊天窗口顶部
|
||
# - 用户主动点击"加载历史"按钮
|
||
#
|
||
# 注意:
|
||
# - 这是异步操作,需要通过 Zulip API 实现
|
||
# - 当前实现为占位符,需要后端 API 支持
|
||
func load_history(count: int = HISTORY_PAGE_SIZE) -> void:
|
||
if _history_loading:
|
||
print("⏳ 历史消息正在加载中...")
|
||
return
|
||
|
||
if not _has_more_history:
|
||
print("📚 没有更多历史消息")
|
||
return
|
||
|
||
_history_loading = true
|
||
print("📜 开始加载历史消息,数量: ", count)
|
||
|
||
# TODO: 实现从 Zulip 后端获取历史消息
|
||
# NetworkManager.get_chat_history(_oldest_message_timestamp, count, _on_history_loaded)
|
||
|
||
# 临时实现:模拟历史消息加载(测试用)
|
||
# await get_tree().create_timer(1.0).timeout
|
||
# _on_history_loaded([])
|
||
|
||
# 历史消息加载完成回调
|
||
func _on_history_loaded(messages: Array) -> void:
|
||
_history_loading = false
|
||
|
||
if messages.is_empty():
|
||
_has_more_history = false
|
||
print("📚 没有更多历史消息")
|
||
return
|
||
|
||
print("📜 历史消息加载完成,数量: ", messages.size())
|
||
|
||
# 将历史消息插入到当前会话历史开头
|
||
for i in range(messages.size() - 1, -1, -1):
|
||
var message: Dictionary = messages[i]
|
||
_message_history.push_front(message)
|
||
|
||
# 触发事件显示消息(Signal Up)
|
||
EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, {
|
||
"from_user": message.get("from_user", ""),
|
||
"content": message.get("content", ""),
|
||
"show_bubble": false,
|
||
"timestamp": message.get("timestamp", 0.0),
|
||
"is_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)
|
||
|
||
# Socket.IO 客户端信号
|
||
_socket_client.connected.connect(_on_socket_connected)
|
||
_socket_client.disconnected.connect(_on_socket_disconnected)
|
||
_socket_client.event_received.connect(_on_socket_event_received)
|
||
_socket_client.error_occurred.connect(_on_socket_error)
|
||
|
||
# ============================================================================
|
||
# 内部方法 - 连接状态处理
|
||
# ============================================================================
|
||
|
||
# Socket 连接成功
|
||
func _on_socket_connected() -> void:
|
||
print("✅ ChatManager: Socket 连接成功")
|
||
|
||
# 发送登录消息
|
||
_send_login_message()
|
||
|
||
# Socket 连接断开
|
||
func _on_socket_disconnected(_clean_close: bool) -> void:
|
||
print("🔌 ChatManager: Socket 连接断开")
|
||
_is_logged_in = false
|
||
|
||
# 连接状态变化
|
||
func _on_connection_state_changed(state: int) -> void:
|
||
var state_names := ["DISCONNECTED", "CONNECTING", "CONNECTED", "RECONNECTING", "ERROR"]
|
||
print("📡 ChatManager: 连接状态变化 - ", state_names[state])
|
||
|
||
# 发射信号
|
||
chat_connection_state_changed.emit(state)
|
||
|
||
# 通过 EventSystem 广播(Signal Up)
|
||
EventSystem.emit_event(EventNames.CHAT_CONNECTION_STATE_CHANGED, {
|
||
"state": state
|
||
})
|
||
|
||
# ============================================================================
|
||
# 内部方法 - 消息处理
|
||
# ============================================================================
|
||
|
||
# Socket 事件接收
|
||
func _on_socket_event_received(event_name: String, data: Dictionary) -> void:
|
||
match event_name:
|
||
"login_success":
|
||
_handle_login_success(data)
|
||
"chat_sent":
|
||
_handle_chat_sent(data)
|
||
"chat_render":
|
||
_handle_chat_render(data)
|
||
"position_updated":
|
||
_handle_position_updated(data)
|
||
"error":
|
||
_handle_error_response(data)
|
||
_:
|
||
print("⚠️ ChatManager: 未处理的事件 - ", event_name)
|
||
|
||
# 处理登录成功
|
||
func _handle_login_success(data: Dictionary) -> void:
|
||
print("✅ ChatManager: 登录成功")
|
||
|
||
_is_logged_in = true
|
||
_current_username = data.get("username", "")
|
||
_current_map = data.get("currentMap", "")
|
||
|
||
# 重置当前会话缓存(每次登录/重连都清空,重新开始接收消息)
|
||
reset_session()
|
||
|
||
print(" 用户名: ", _current_username)
|
||
print(" 地图: ", _current_map)
|
||
|
||
# 通过 EventSystem 广播(Signal Up)
|
||
EventSystem.emit_event(EventNames.CHAT_LOGIN_SUCCESS, {
|
||
"username": _current_username,
|
||
"current_map": _current_map
|
||
})
|
||
|
||
# 处理聊天消息发送成功
|
||
func _handle_chat_sent(data: Dictionary) -> void:
|
||
var message_id: String = data.get("messageId", "")
|
||
var timestamp: float = data.get("timestamp", 0.0)
|
||
|
||
print("✅ 消息发送成功: ", message_id)
|
||
|
||
# 发射信号
|
||
chat_message_sent.emit(message_id, timestamp)
|
||
|
||
# 通过 EventSystem 广播(Signal Up)
|
||
EventSystem.emit_event(EventNames.CHAT_MESSAGE_SENT, {
|
||
"message_id": message_id,
|
||
"timestamp": timestamp
|
||
})
|
||
|
||
# 处理接收到的聊天消息
|
||
func _handle_chat_render(data: Dictionary) -> void:
|
||
var from_user: String = data.get("from", "")
|
||
var content: String = data.get("txt", "")
|
||
var show_bubble: bool = data.get("bubble", false)
|
||
var timestamp: float = data.get("timestamp", 0.0)
|
||
|
||
print("📨 收到聊天消息: ", from_user, " -> ", content)
|
||
|
||
# 添加到历史
|
||
_add_message_to_history({
|
||
"from_user": from_user,
|
||
"content": content,
|
||
"timestamp": timestamp,
|
||
"is_self": false
|
||
})
|
||
|
||
# 发射信号
|
||
chat_message_received.emit(from_user, content, show_bubble, timestamp)
|
||
|
||
# 通过 EventSystem 广播(Signal Up)
|
||
EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, {
|
||
"from_user": from_user,
|
||
"content": content,
|
||
"show_bubble": show_bubble,
|
||
"timestamp": timestamp
|
||
})
|
||
|
||
# 处理位置更新成功
|
||
func _handle_position_updated(data: Dictionary) -> void:
|
||
var stream: String = data.get("stream", "")
|
||
var topic: String = data.get("topic", "")
|
||
|
||
print("✅ 位置更新成功: ", stream, " / ", topic)
|
||
|
||
# 发射信号
|
||
chat_position_updated.emit(stream, topic)
|
||
|
||
# 通过 EventSystem 广播(Signal Up)
|
||
EventSystem.emit_event(EventNames.CHAT_POSITION_UPDATED, {
|
||
"stream": stream,
|
||
"topic": topic
|
||
})
|
||
|
||
# 处理错误响应
|
||
func _handle_error_response(data: Dictionary) -> void:
|
||
var error_code: String = data.get("code", "")
|
||
var error_message: String = data.get("message", "")
|
||
|
||
_handle_error(error_code, error_message)
|
||
|
||
# 处理 Socket 错误
|
||
func _on_socket_error(error: String) -> void:
|
||
_handle_error("SOCKET_ERROR", error)
|
||
|
||
# ============================================================================
|
||
# 内部方法 - 工具函数
|
||
# ============================================================================
|
||
|
||
# 发送登录消息
|
||
func _send_login_message() -> void:
|
||
if _game_token.is_empty():
|
||
push_error("无法获取 access token,请确保已登录")
|
||
_handle_error("AUTH_FAILED", "无法获取 access token,请先登录")
|
||
return
|
||
|
||
var login_data := {
|
||
"type": "login",
|
||
"token": _game_token
|
||
}
|
||
|
||
_socket_client.emit("login", login_data)
|
||
print("📤 发送登录消息(使用 access token)")
|
||
|
||
# 处理错误
|
||
func _handle_error(error_code: String, error_message: String) -> void:
|
||
print("❌ ChatManager 错误: [", error_code, "] ", error_message)
|
||
|
||
# 获取用户友好的错误消息
|
||
var user_message: String = CHAT_ERROR_MESSAGES.get(error_code, error_message) as String
|
||
|
||
# 发射信号
|
||
chat_error_occurred.emit(error_code, user_message)
|
||
|
||
# 通过 EventSystem 广播(Signal Up)
|
||
EventSystem.emit_event(EventNames.CHAT_ERROR_OCCURRED, {
|
||
"error_code": error_code,
|
||
"message": user_message
|
||
})
|
||
|
||
# 特殊处理认证失败
|
||
if error_code == "AUTH_FAILED" or error_code == "SESSION_EXPIRED":
|
||
_is_logged_in = false
|
||
EventSystem.emit_event(EventNames.CHAT_LOGIN_FAILED, {
|
||
"error_code": error_code
|
||
})
|
||
|
||
# 记录消息发送时间戳
|
||
func _record_message_timestamp() -> void:
|
||
var current_time := Time.get_unix_time_from_system()
|
||
_message_timestamps.append(current_time)
|
||
|
||
# 添加消息到当前会话历史
|
||
func _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()
|