diff --git a/_Core/managers/AuthManager.gd b/_Core/managers/AuthManager.gd index 3de2b35..68b6af8 100644 --- a/_Core/managers/AuthManager.gd +++ b/_Core/managers/AuthManager.gd @@ -102,6 +102,7 @@ var _game_token: String = "" # @deprecated 使用 _access_token 替代 # 初始化管理器 func _init() -> void: _load_auth_data() + _connect_network_signals() # 清理资源 func cleanup() -> void: @@ -110,6 +111,8 @@ func cleanup() -> void: NetworkManager.cancel_request(request_id) active_request_ids.clear() + _disconnect_network_signals() + # ============ Token 管理 ============ # 保存 Token 到内存 @@ -675,7 +678,7 @@ func _on_send_login_code_response(success: bool, data: Dictionary, error_info: D func _on_forgot_password_response(success: bool, data: Dictionary, error_info: Dictionary): button_state_changed.emit("forgot_password_btn", false, "忘记密码") - var result = ResponseHandler.handle_send_login_code_response(success, data, error_info) + var result = ResponseHandler.handle_forgot_password_response(success, data, error_info) if result.should_show_toast: show_toast_message.emit(result.message, result.success) @@ -714,10 +717,10 @@ func _can_send_verification_code(email: String) -> bool: if not email_data.sent: return true - var current_time = Time.get_time_dict_from_system() - var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second + var current_timestamp: int = int(Time.get_unix_time_from_system()) + var sent_timestamp: int = int(email_data.get("time", 0)) - return (current_timestamp - email_data.time) >= code_cooldown + return float(current_timestamp - sent_timestamp) >= code_cooldown # 获取剩余冷却时间 func get_remaining_cooldown_time(email: String) -> int: @@ -725,15 +728,15 @@ func get_remaining_cooldown_time(email: String) -> int: return 0 var email_data = verification_codes_sent[email] - var current_time = Time.get_time_dict_from_system() - var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second + var current_timestamp: int = int(Time.get_unix_time_from_system()) + var sent_timestamp: int = int(email_data.get("time", 0)) + var remaining: int = int(code_cooldown - float(current_timestamp - sent_timestamp)) - return int(code_cooldown - (current_timestamp - email_data.time)) + return maxi(0, remaining) # 记录验证码发送状态 func _record_verification_code_sent(email: String): - var current_time = Time.get_time_dict_from_system() - var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second + var current_timestamp: int = int(Time.get_unix_time_from_system()) if not verification_codes_sent.has(email): verification_codes_sent[email] = {} @@ -758,6 +761,30 @@ func _has_sent_verification_code(email: String) -> bool: func _is_valid_identifier(identifier: String) -> bool: return StringUtils.is_valid_email(identifier) or _is_valid_phone(identifier) +# 连接/断开 NetworkManager 请求信号,用于回收 active_request_ids +func _connect_network_signals() -> void: + if not NetworkManager.request_completed.is_connected(_on_network_request_completed): + NetworkManager.request_completed.connect(_on_network_request_completed) + if not NetworkManager.request_failed.is_connected(_on_network_request_failed): + NetworkManager.request_failed.connect(_on_network_request_failed) + +func _disconnect_network_signals() -> void: + if NetworkManager.request_completed.is_connected(_on_network_request_completed): + NetworkManager.request_completed.disconnect(_on_network_request_completed) + if NetworkManager.request_failed.is_connected(_on_network_request_failed): + NetworkManager.request_failed.disconnect(_on_network_request_failed) + +func _on_network_request_completed(request_id: String, _success: bool, _data: Dictionary) -> void: + _remove_active_request_id(request_id) + +func _on_network_request_failed(request_id: String, _error_type: String, _message: String) -> void: + _remove_active_request_id(request_id) + +func _remove_active_request_id(request_id: String) -> void: + var index: int = active_request_ids.find(request_id) + if index != -1: + active_request_ids.remove_at(index) + # 验证手机号格式 func _is_valid_phone(phone: String) -> bool: var regex = RegEx.new() diff --git a/_Core/managers/SceneManager.gd b/_Core/managers/SceneManager.gd index e577432..bb46100 100644 --- a/_Core/managers/SceneManager.gd +++ b/_Core/managers/SceneManager.gd @@ -44,12 +44,6 @@ var _next_spawn_name: String = "" # 下一个场景的出生 # 便于统一管理和修改场景路径 var scene_paths: Dictionary = { "main": "res://scenes/MainScene.tscn", # 主场景 - 游戏入口 - "auth": "res://scenes/ui/LoginWindow.tscn", # 认证场景 - 登录窗口 - "game": "res://scenes/maps/game_scene.tscn", # 游戏场景 - 主要游戏内容 - "battle": "res://scenes/maps/battle_scene.tscn", # 战斗场景 - 战斗系统 - "inventory": "res://scenes/ui/InventoryWindow.tscn", # 背包界面 - "shop": "res://scenes/ui/ShopWindow.tscn", # 商店界面 - "settings": "res://scenes/ui/SettingsWindow.tscn", # 设置界面 "square": "res://scenes/Maps/square.tscn", # 广场地图 "room": "res://scenes/Maps/room.tscn", # 房间地图 "fountain": "res://scenes/Maps/fountain.tscn", # 喷泉地图 diff --git a/_Core/managers/WebSocketManager.gd b/_Core/managers/WebSocketManager.gd index f00cbe1..b768657 100644 --- a/_Core/managers/WebSocketManager.gd +++ b/_Core/managers/WebSocketManager.gd @@ -106,6 +106,9 @@ var _reconnect_timer: Timer = Timer.new() # 是否为正常关闭(非异常断开) var _clean_close: bool = true +# 当前 CLOSED 状态是否已经处理过(防止每帧重复处理 close 事件) +var _closed_state_handled: bool = false + # 心跳定时器 var _heartbeat_timer: Timer = Timer.new() @@ -163,14 +166,20 @@ func _exit_tree() -> void: # ============================================================================ # 连接到游戏服务器 -func connect_to_game_server() -> void: +func connect_to_game_server(is_reconnect_attempt: bool = false) -> void: if _connection_state == ConnectionState.CONNECTED or _connection_state == ConnectionState.CONNECTING: push_warning("已经在连接或已连接状态") return - _set_connection_state(ConnectionState.CONNECTING) + if is_reconnect_attempt: + _set_connection_state(ConnectionState.RECONNECTING) + else: + _set_connection_state(ConnectionState.CONNECTING) _clean_close = true - _reconnect_attempt = 0 + _closed_state_handled = false + # 仅在首次/手动连接时重置重连计数,重连流程保持累计尝试次数 + if not is_reconnect_attempt: + _reconnect_attempt = 0 var err: Error = _websocket_peer.connect_to_url(WEBSOCKET_URL) if err != OK: @@ -295,23 +304,40 @@ func _check_websocket_state() -> void: match state: WebSocketPeer.STATE_CONNECTING: + _closed_state_handled = false + # 正在连接 if _connection_state != ConnectionState.CONNECTING and _connection_state != ConnectionState.RECONNECTING: _set_connection_state(ConnectionState.CONNECTING) WebSocketPeer.STATE_OPEN: + _closed_state_handled = false + # 连接成功 if _connection_state != ConnectionState.CONNECTED: _on_websocket_connected() WebSocketPeer.STATE_CLOSING: + _closed_state_handled = false + # 正在关闭 pass WebSocketPeer.STATE_CLOSED: - # 连接关闭 - var code: int = _websocket_peer.get_close_code() - _on_websocket_closed(code != 0) # code=0 表示正常关闭 + if _closed_state_handled: + return + + _closed_state_handled = true + + # 仅在连接生命周期中发生的关闭才触发关闭处理 + var should_handle_close: bool = ( + _connection_state == ConnectionState.CONNECTED + or _connection_state == ConnectionState.CONNECTING + or _connection_state == ConnectionState.RECONNECTING + ) + if should_handle_close: + var close_code: int = _websocket_peer.get_close_code() + _on_websocket_closed(_is_clean_close_code(close_code)) # WebSocket 连接成功处理 func _on_websocket_connected() -> void: @@ -333,6 +359,11 @@ func _on_websocket_closed(clean_close: bool) -> void: else: _set_connection_state(ConnectionState.DISCONNECTED) +# 判断关闭码是否为干净关闭 +# Godot 中 close_code == -1 表示非干净关闭(异常断开) +func _is_clean_close_code(close_code: int) -> bool: + return close_code != -1 + # ============================================================================ # 内部方法 - 重连机制 # ============================================================================ @@ -375,7 +406,7 @@ func _calculate_reconnect_delay() -> float: # 重连定时器超时处理 func _on_reconnect_timeout() -> void: _clean_close = false - connect_to_game_server() + connect_to_game_server(true) # ============================================================================ # 内部方法 - 心跳机制 diff --git a/scenes/Maps/BaseLevel.gd b/scenes/Maps/BaseLevel.gd index 4520fc9..bc70791 100644 --- a/scenes/Maps/BaseLevel.gd +++ b/scenes/Maps/BaseLevel.gd @@ -61,8 +61,12 @@ var _player_spawned: bool = false # Track if local player has been spawned var _local_player: Node = null # Reference to the local player node var _position_update_timer: float = 0.0 # Throttle timer for position updates var _current_session_id: String = "" # Cache session ID to avoid race condition on exit +var _is_joining_session: bool = false const POSITION_UPDATE_INTERVAL: float = 0.125 # Send at most 8 times per second const PRIVATE_SCENES = ["room"] # List of scenes that should be private instances +var _join_token_poll_interval: float = 0.5 +var _join_token_wait_timeout: float = 15.0 +var _join_player_wait_timeout_frames: int = 300 func _on_session_joined(data: Dictionary): # 对账远程玩家列表,使用 position.mapId 过滤 @@ -199,26 +203,51 @@ func _update_remote_player_position(user: Dictionary): player.update_position(Vector2(pos.x, pos.y)) func _join_session_with_player(session_id: String): - # 检查是否有Token,如果没有则等待 - if LocationManager._auth_token == "": - # 轮询等待Token就绪 (简单重试机制) - await get_tree().create_timer(0.5).timeout - _join_session_with_player(session_id) + if _is_joining_session: return - # 等待玩家生成完毕 - if not _player_spawned or not _local_player: + _is_joining_session = true + + # 检查是否有 Token,没有则轮询等待(带超时) + var token_wait_elapsed: float = 0.0 + while LocationManager._auth_token == "": + if not is_inside_tree(): + _is_joining_session = false + return + if token_wait_elapsed >= _join_token_wait_timeout: + push_warning("BaseLevel: 等待认证 Token 超时,取消加入会话 %s" % session_id) + _is_joining_session = false + return + await get_tree().create_timer(_join_token_poll_interval).timeout + token_wait_elapsed += _join_token_poll_interval + + # 等待玩家生成完毕(带帧数上限) + var wait_frames: int = 0 + while not _player_spawned or not _local_player: + if not is_inside_tree(): + _is_joining_session = false + return + if wait_frames >= _join_player_wait_timeout_frames: + push_warning("BaseLevel: 等待本地玩家就绪超时,取消加入会话 %s" % session_id) + _is_joining_session = false + return await get_tree().process_frame - _join_session_with_player(session_id) + wait_frames += 1 + + if not is_instance_valid(_local_player): + push_warning("BaseLevel: 本地玩家无效,取消加入会话 %s" % session_id) + _is_joining_session = false return - var pos = _local_player.global_position if is_instance_valid(_local_player) else Vector2.ZERO - + var pos = _local_player.global_position LocationManager.join_session(session_id, pos) # 进入会话后立即同步一次位置 await get_tree().create_timer(0.1).timeout - LocationManager.send_position_update(session_id, pos) + if is_inside_tree(): + LocationManager.send_position_update(session_id, pos) + + _is_joining_session = false func _process(delta): # 发送位置更新 (节流机制) diff --git a/scenes/ui/AuthScene.gd b/scenes/ui/AuthScene.gd index 849f6f2..420d8a7 100644 --- a/scenes/ui/AuthScene.gd +++ b/scenes/ui/AuthScene.gd @@ -91,6 +91,7 @@ var toast_manager: ToastManager # 验证码冷却计时器 var cooldown_timer: Timer = null +var cooldown_email: String = "" # 当前登录模式(从管理器同步) var current_login_mode: AuthManager.LoginMode = AuthManager.LoginMode.PASSWORD @@ -291,9 +292,6 @@ func _on_register_pressed(): func _on_send_code_pressed(): var email = register_email.text.strip_edges() auth_manager.send_email_verification_code(email) - - # 开始冷却计时器 - _start_cooldown_timer(email) # 获取登录验证码按钮 func _on_get_login_code_pressed(): @@ -374,8 +372,10 @@ func _on_controller_register_failed(_message: String): # 验证码发送成功处理 func _on_controller_verification_code_sent(_message: String): - # 验证码发送成功,冷却计时器已经在按钮点击时启动 - pass + var email = auth_manager.current_email + if email == "": + email = register_email.text.strip_edges() + _start_cooldown_timer(email) # 验证码发送失败处理 func _on_controller_verification_code_failed(_message: String): @@ -429,12 +429,20 @@ func _on_controller_show_toast_message(message: String, is_success: bool): # ============ 验证码冷却管理 ============ # 开始冷却计时器 -func _start_cooldown_timer(_email: String): +func _start_cooldown_timer(email: String): if cooldown_timer != null: cooldown_timer.queue_free() + cooldown_timer = null + + cooldown_email = email + if cooldown_email == "": + cooldown_email = register_email.text.strip_edges() send_code_btn.disabled = true - send_code_btn.text = "重新发送(60)" + var remaining_time = auth_manager.get_remaining_cooldown_time(cooldown_email) + if remaining_time <= 0: + remaining_time = 60 + send_code_btn.text = "重新发送(%d)" % remaining_time cooldown_timer = Timer.new() add_child(cooldown_timer) @@ -444,7 +452,8 @@ func _start_cooldown_timer(_email: String): # 冷却计时器超时处理 func _on_cooldown_timer_timeout(): - var remaining_time = auth_manager.get_remaining_cooldown_time(register_email.text.strip_edges()) + var email = cooldown_email if cooldown_email != "" else register_email.text.strip_edges() + var remaining_time = auth_manager.get_remaining_cooldown_time(email) if remaining_time > 0: send_code_btn.text = "重新发送(%d)" % remaining_time @@ -462,6 +471,7 @@ func _reset_verification_button(): cooldown_timer.queue_free() cooldown_timer = null + cooldown_email = "" send_code_btn.disabled = false send_code_btn.text = "发送验证码" diff --git a/tests/auth/auth_ui_test.tscn b/tests/auth/auth_ui_test.tscn index 0285847..ec9ea55 100644 --- a/tests/auth/auth_ui_test.tscn +++ b/tests/auth/auth_ui_test.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=4 format=3 uid="uid://bvn8y7x2qkqxe"] [ext_resource type="Script" uid="uid://ddb8v5c6aeqe7" path="res://tests/auth/auth_ui_test.gd" id="1_test_script"] -[ext_resource type="PackedScene" uid="uid://by7m8snb4xllf" path="res://scenes/ui/LoginWindow.tscn" id="2_auth_scene"] +[ext_resource type="PackedScene" uid="uid://by7m8snb4xllf" path="res://scenes/ui/AuthScene.tscn" id="2_auth_scene"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_feedback_info"] bg_color = Color(0.2, 0.5, 0.8, 0.9)