fix(chat): 调整输入交互并加固 WS/告警处理

This commit is contained in:
2026-03-11 18:11:09 +08:00
parent dc403179f7
commit 558a0ff9bc
12 changed files with 315 additions and 44 deletions

View File

@@ -126,6 +126,10 @@ var _game_token: String = ""
const SELF_ECHO_DEDUPE_WINDOW: float = 10.0
var _pending_self_messages: Array[Dictionary] = []
# 空消息类型告警限频(避免日志刷屏)
const EMPTY_MESSAGE_TYPE_WARNING_INTERVAL: float = 10.0
var _last_empty_message_type_warning_at: float = -1000.0
# ============================================================================
# 生命周期方法
# ============================================================================
@@ -489,12 +493,22 @@ func _on_data_received(message: String) -> void:
push_error("ChatManager: JSON 解析失败")
return
var data: Dictionary = json.data
var data_variant: Variant = json.data
if not (data_variant is Dictionary):
push_warning("ChatManager: 收到非对象消息,已忽略")
return
# 检查消息类型字段
var message_type: String = data.get("t", "")
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":
@@ -509,9 +523,22 @@ func _on_data_received(message: String) -> void:
_handle_chat_render(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

View File

@@ -6,7 +6,11 @@ extends Node
# 负责与后端 WebSocket 服务进行位置同步和多人会话管理
#
# 协议文档: new_docs/game_architecture_design.md
# 后端地址: wss://whaletownend.xinghangee.icu/location-broadcast
# 后端地址默认值: wss://whaletownend.xinghangee.icu/game
# 可通过以下方式覆盖:
# 1) 环境变量 WHALETOWN_LOCATION_WS_URL
# 2) Config/game_config.json 或 config/game_config.json 中的
# network.location_ws_url / network.game_ws_url
# ============================================================================
signal connected_to_server()
@@ -17,23 +21,32 @@ signal user_joined(data: Dictionary)
signal user_left(data: Dictionary)
signal position_updated(data: Dictionary)
const WS_URL = "wss://whaletownend.xinghangee.icu/location-broadcast"
const DEFAULT_WS_URL: String = "wss://whaletownend.xinghangee.icu/game"
const WS_URL_ENV_KEY: String = "WHALETOWN_LOCATION_WS_URL"
const PING_INTERVAL = 25.0 # 秒
var _socket: WebSocketPeer
var _connected: bool = false
var _ping_timer: float = 0.0
var _auth_token: String = ""
var _connection_error_reported: bool = false
var _is_connecting: bool = false
var _ws_url: String = DEFAULT_WS_URL
func _ready():
_socket = WebSocketPeer.new()
process_mode = Node.PROCESS_MODE_ALWAYS # 保证暂停时也能处理网络
_ws_url = _resolve_ws_url()
func _process(delta):
_socket.poll()
var state = _socket.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
_connection_error_reported = false
_is_connecting = false
if not _connected:
_on_connected()
@@ -51,19 +64,71 @@ func _process(delta):
elif state == WebSocketPeer.STATE_CLOSED:
if _connected:
_on_disconnected()
elif _is_connecting and not _connection_error_reported:
var close_code := _socket.get_close_code()
var close_reason := _socket.get_close_reason()
push_warning(
"LocationManager: WebSocket 握手失败close_code=%d, reason=%s" % [close_code, close_reason]
)
connection_error.emit()
_connection_error_reported = true
_is_connecting = false
func connect_to_server():
if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
var state: WebSocketPeer.State = _socket.get_ready_state()
if state == WebSocketPeer.STATE_OPEN or state == WebSocketPeer.STATE_CONNECTING:
return
var err = _socket.connect_to_url(WS_URL)
_connection_error_reported = false
_is_connecting = true
var err = _socket.connect_to_url(_ws_url)
if err != OK:
push_error("LocationManager: WebSocket 连接请求失败,错误码: %d" % err)
push_error("LocationManager: WebSocket 连接请求失败,url=%s, 错误码: %d" % [_ws_url, err])
connection_error.emit()
_connection_error_reported = true
_is_connecting = false
else:
# Godot WebSocket connect is non-blocking, wait for state change in _process
pass
func _resolve_ws_url() -> String:
var env_url: String = OS.get_environment(WS_URL_ENV_KEY).strip_edges()
if not env_url.is_empty():
return env_url
for config_path in ["res://Config/game_config.json", "res://config/game_config.json"]:
var config_url: String = _load_ws_url_from_config(config_path)
if not config_url.is_empty():
return config_url
return DEFAULT_WS_URL
func _load_ws_url_from_config(config_path: String) -> String:
if not FileAccess.file_exists(config_path):
return ""
var content: String = FileAccess.get_file_as_string(config_path)
if content.is_empty():
return ""
var json := JSON.new()
if json.parse(content) != OK:
push_warning("LocationManager: 读取配置失败 %s - %s" % [config_path, json.get_error_message()])
return ""
var data_variant: Variant = json.data
if not (data_variant is Dictionary):
return ""
var root: Dictionary = data_variant
var network_variant: Variant = root.get("network", {})
if not (network_variant is Dictionary):
return ""
var network_config: Dictionary = network_variant
var ws_url: String = str(network_config.get("location_ws_url", network_config.get("game_ws_url", ""))).strip_edges()
return ws_url
func close_connection():
_socket.close()

View File

@@ -186,9 +186,9 @@ func set_next_spawn_name(spawn_name: String) -> void:
# - 此方法会清除存储的名称,只能获取一次
# - 如果未设置名称,返回空字符串
func get_next_spawn_name() -> String:
var name = _next_spawn_name
var spawn_name: String = _next_spawn_name
_next_spawn_name = ""
return name
return spawn_name
# ============ 场景注册方法 ============