fix: 装备属性面板避让逻辑 + 多项功能更新
- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
This commit is contained in:
+112
-41
@@ -6,8 +6,8 @@
|
||||
# af.start(assets_root) # assets_root 传给 game_scene
|
||||
#
|
||||
# 状态机:LOGIN → SELECT → GAME。M2Client 信号驱动切屏。
|
||||
# 断线:登录/选人阶段回到登录;游戏阶段保留 GameScene,显示重连遮罩,
|
||||
# 重连成功后自动重新进入上次角色,避免短暂闪回选人界面。
|
||||
# 断线:所有阶段都回到 LoginPhase,并释放旧 GameScene;LoginPhase 是唯一
|
||||
# 的网络/界面属主。40250 的 OnRemoteDisconnect 不保留游戏窗口重连遮罩。
|
||||
extends Node
|
||||
|
||||
const ServerInfoRes = preload("res://net/serverinfo.gd")
|
||||
@@ -16,8 +16,8 @@ const LoadingScreen = preload("res://ui/loading_screen.gd")
|
||||
const CharSelectScreen = preload("res://ui/char_select_screen.gd")
|
||||
const GameScene = preload("res://game_scene.gd")
|
||||
const AppLifecycle = preload("res://app_lifecycle.gd")
|
||||
const ReconnectUI = preload("res://ui/reconnect_ui.gd")
|
||||
const UiProfile = preload("res://ui/ui_profile.gd")
|
||||
const MobView = preload("res://ui/mob_view.gd")
|
||||
const UiAssets = preload("res://ui/ui_assets.gd")
|
||||
|
||||
enum { LOGIN, SELECT, GAME }
|
||||
|
||||
@@ -39,6 +39,8 @@ var _chars: Array = []
|
||||
var _selected_char_slot := -1
|
||||
var _reconnecting_game := false
|
||||
var _mark_redl_after := 0 # guild-mark re-download cooldown (ticks_msec)
|
||||
var _probe_in_flight := false
|
||||
var _closing := false
|
||||
# MT_AUTOLOGIN=1:进 LOGIN 自动连接,收到角色列表自动选第一个(冒烟 / CI 用)。
|
||||
var auto_login := OS.get_environment("MT_AUTOLOGIN") == "1" and not _id.is_empty() and not _pw.is_empty()
|
||||
var _auto_char_slot := -1 # MT_CHAR_SLOT:自动登录时优先选择的角色槽位。
|
||||
@@ -48,6 +50,18 @@ func _init() -> void:
|
||||
if raw_slot.is_valid_int():
|
||||
_auto_char_slot = int(raw_slot)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
# CPythonApplication::Destroy releases process-wide model, motion, text/UI
|
||||
# and NPC resource caches after the live scene has been torn down. Keeping
|
||||
# these Ref-backed caches through ObjectDB shutdown causes the same native
|
||||
# leak/SIGSEGV seen in a real map + animated crowd scenario.
|
||||
if ClassDB.class_exists("Metin2Model"):
|
||||
Metin2Model.clear_texture_cache()
|
||||
if ClassDB.class_exists("Metin2AnimPlayer"):
|
||||
Metin2AnimPlayer.clear_clip_cache()
|
||||
MobView.clear_cache()
|
||||
UiAssets.clear_cache()
|
||||
|
||||
func start(assets_root: String = "", injected_client: Node = null) -> void:
|
||||
_assets = assets_root
|
||||
serverinfo = ServerInfoRes.new()
|
||||
@@ -78,11 +92,13 @@ func start(assets_root: String = "", injected_client: Node = null) -> void:
|
||||
_lifecycle.name = "AppLifecycle"
|
||||
add_child(_lifecycle)
|
||||
_lifecycle.bind(client)
|
||||
_reconnect = ReconnectUI.new()
|
||||
_reconnect.name = "ReconnectUI"
|
||||
add_child(_reconnect)
|
||||
_reconnect.setup(client)
|
||||
_reconnect.set_mobile_mode(UiProfile.resolve() == UiProfile.Mode.MOBILE)
|
||||
# 40250 WM_CLOSE enters the application exit owner. Keep the Godot
|
||||
# notification on the same AppFlow owner that created the client/session so
|
||||
# disconnect, scene teardown and process exit happen once and in order.
|
||||
_lifecycle.close_requested.connect(_on_close_requested)
|
||||
# 40250 SetLoginPhase replaces the current phase window. Do not attach the
|
||||
# optional ReconnectUI to `disconnected`: doing so would reopen a reconnect
|
||||
# overlay after AppFlow has already become the LoginPhase owner.
|
||||
_goto_login()
|
||||
if auto_login:
|
||||
call_deferred("_do_connect")
|
||||
@@ -103,7 +119,7 @@ func select_test_server(server_index: int, channel: int) -> Dictionary:
|
||||
|
||||
# Read-only adapter for the packaged playable test. It deliberately returns
|
||||
# summaries rather than the private UI/game nodes so a report cannot retain a
|
||||
# stale scene reference across reconnect or map reload.
|
||||
# stale scene reference across a phase or map transition.
|
||||
func get_playable_snapshot() -> Dictionary:
|
||||
var stage := "LOGIN"
|
||||
match _state:
|
||||
@@ -134,7 +150,7 @@ func get_playable_context() -> Dictionary:
|
||||
# --- client 信号 -------------------------------------------------------------
|
||||
|
||||
func _wire_client() -> void:
|
||||
for sig in ["char_list", "char_name_changed", "entered_game", "disconnected", "login_failed"]:
|
||||
for sig in ["char_list", "char_name_changed", "entered_game", "disconnected", "login_failed", "connect_failed", "phase_changed", "server_quit_requested"]:
|
||||
if client.has_signal(sig):
|
||||
client.connect(sig, Callable(self, "_on_" + sig))
|
||||
# 建 / 删号结果 —— 一次性接,转发给当前选人页
|
||||
@@ -144,16 +160,6 @@ func _wire_client() -> void:
|
||||
|
||||
func _on_char_list(list: Array) -> void:
|
||||
_chars = list
|
||||
if _reconnecting_game:
|
||||
# A full reconnect replays the character list before it can enter the
|
||||
# previous character. Do not briefly replace the live game with SELECT;
|
||||
# re-enter the remembered slot and keep the reconnect mask on screen.
|
||||
var slot := _selected_char_slot
|
||||
if slot < 0 and not _chars.is_empty():
|
||||
slot = int(_chars[0].get("index", 0))
|
||||
if slot >= 0:
|
||||
_enter_character(slot)
|
||||
return
|
||||
# 已在选人页(建号 / 删号后 M2Client 会重发 char_list)——只刷数据,别整屏重建
|
||||
if _state == SELECT and _ui:
|
||||
var sc: Node = _ui.get_node_or_null("CharSelect")
|
||||
@@ -172,8 +178,8 @@ func _on_char_list(list: Array) -> void:
|
||||
idx = _auto_char_slot
|
||||
break
|
||||
if idx >= 0:
|
||||
# Remember the slot so a later in-game reconnect re-enters the same character.
|
||||
_enter_character(idx)
|
||||
# Keep the selected slot for the ordinary character-select flow.
|
||||
_enter_character(idx)
|
||||
|
||||
func _on_char_name_changed(pid: int, name: String) -> void:
|
||||
var sc: Node = _ui.get_node_or_null("CharSelect") if _ui and _state == SELECT else null
|
||||
@@ -182,11 +188,27 @@ func _on_char_name_changed(pid: int, name: String) -> void:
|
||||
|
||||
func _on_entered_game() -> void:
|
||||
_reconnecting_game = false
|
||||
if _reconnect and _reconnect.has_method("dismiss"):
|
||||
_reconnect.dismiss()
|
||||
_goto_game()
|
||||
_start_guild_mark_download()
|
||||
|
||||
func _on_phase_changed(phase: String) -> void:
|
||||
# The native classic bridge reports 40250 PHASE_CLOSE as a transition back
|
||||
# to LoginPhase. Explicit PHASE_CLOSE is not a transport disconnect; keep
|
||||
# this phase-only owner path separate from the peer-close `disconnected`
|
||||
# signal so both sources have the same single LoginPhase owner.
|
||||
var p := phase.to_lower()
|
||||
if p not in ["login", "close"]:
|
||||
return
|
||||
if _state != GAME:
|
||||
if _state == SELECT:
|
||||
_goto_login()
|
||||
return
|
||||
if _game == null or not is_instance_valid(_game):
|
||||
_goto_login()
|
||||
return
|
||||
var reason := "服务器返回登录阶段"
|
||||
_on_disconnected(reason)
|
||||
|
||||
# 会徽下载:serverlist 第 8 列配了 mark_port 才连(0 = 跳过)。
|
||||
# `guild_mark_updated` 时按服务器 1s 冷却重连一次。
|
||||
func _start_guild_mark_download() -> void:
|
||||
@@ -209,26 +231,60 @@ func _on_guild_mark_updated(_guild_id: int, _img_idx: int) -> void:
|
||||
_start_guild_mark_download()
|
||||
|
||||
func _on_login_failed(reason: String) -> void:
|
||||
if _reconnecting_game:
|
||||
if _reconnect and _reconnect.has_method("set_error"):
|
||||
_reconnect.set_error(reason)
|
||||
return
|
||||
if _state != GAME:
|
||||
# 40250 intrologin.OnLoginFailure closes only the connecting dialog and
|
||||
# leaves the LoginPhase window alive. Preserve the existing login owner so
|
||||
# its callbacks, entered text and retry controls remain valid. Rebuild only
|
||||
# when the failure arrived from SELECT/another phase that has no login UI.
|
||||
if _state != LOGIN or _ui == null:
|
||||
_goto_login()
|
||||
_set_status("登录失败:%s" % reason)
|
||||
_focus_login_password()
|
||||
_set_status("登录失败:%s" % reason)
|
||||
|
||||
func _on_connect_failed(reason: String) -> void:
|
||||
# 40250 LoginWindow.OnConnectFailure is separate from OnLoginFailure: keep
|
||||
# the LoginPhase owner and only report transport failure to its status/focus
|
||||
# surface. A game-socket refusal must not be presented as bad credentials.
|
||||
if _state != LOGIN or _ui == null:
|
||||
_goto_login()
|
||||
_focus_login_password()
|
||||
_set_status("连接失败:%s" % reason)
|
||||
|
||||
func _focus_login_password() -> void:
|
||||
if _ui == null:
|
||||
return
|
||||
var password := _ui.find_child("Password", true, false)
|
||||
if password is LineEdit:
|
||||
password.grab_focus()
|
||||
|
||||
func _on_disconnected(reason: String) -> void:
|
||||
if _state == GAME and _game != null and is_instance_valid(_game):
|
||||
_reconnecting_game = true
|
||||
if _reconnect and _reconnect.has_method("set_error"):
|
||||
_reconnect.set_error(reason)
|
||||
return
|
||||
# 40250 OnRemoteDisconnect -> SetLoginPhase always replaces the current
|
||||
# phase window. Retaining GameScene or a reconnect overlay here creates a
|
||||
# second owner and diverges from peer-close and explicit PHASE_CLOSE behavior.
|
||||
_reconnecting_game = false
|
||||
_goto_login()
|
||||
if _reconnect and _reconnect.has_method("dismiss"):
|
||||
_reconnect.dismiss()
|
||||
_set_status("连接断开:%s" % reason)
|
||||
|
||||
func _on_server_quit_requested() -> void:
|
||||
# 40250 ServerCommand("quit") posts the application quit message. Route it
|
||||
# through the same lifecycle owner as WM_CLOSE so the network is disconnected,
|
||||
# the retained world is reset, and the process exits exactly once.
|
||||
_on_close_requested()
|
||||
|
||||
func _on_close_requested() -> void:
|
||||
if _closing:
|
||||
return
|
||||
_closing = true
|
||||
# Mirror CPythonApplication::Destroy's network-before-window teardown. The
|
||||
# world_reset signal is emitted before the retained GameScene is released.
|
||||
if client and client.has_method("disconnect_from_server"):
|
||||
client.disconnect_from_server()
|
||||
if _lifecycle and is_instance_valid(_lifecycle):
|
||||
_lifecycle.bind(null, null)
|
||||
if _game and is_instance_valid(_game):
|
||||
_game.queue_free()
|
||||
_game = null
|
||||
get_tree().quit()
|
||||
|
||||
# --- 切屏 ------------------------------------------------------------------
|
||||
|
||||
func _clear_ui() -> void:
|
||||
@@ -278,8 +334,9 @@ func _goto_game() -> void:
|
||||
if is_instance_valid(_game) and "_model_built" in _game and client != null:
|
||||
var wait_msec := Time.get_ticks_msec()
|
||||
while not _game._model_built and (Time.get_ticks_msec() - wait_msec < 3000):
|
||||
if client.has_method("net_poll"):
|
||||
client.net_poll()
|
||||
# M2Client._process is the single frame-pump owner, matching
|
||||
# CPythonApplication::Process. Yielding lets that owner deliver
|
||||
# spawn/phase packets without a second same-frame pump here.
|
||||
if _game.has_method("_sync_main_character"):
|
||||
_game._sync_main_character()
|
||||
if _game._model_built:
|
||||
@@ -330,6 +387,7 @@ func _build_login_form() -> void:
|
||||
id_edit.text_changed.connect(func(t): _id = t)
|
||||
box.add_child(id_edit)
|
||||
var pw_edit := LineEdit.new()
|
||||
pw_edit.name = "Password"
|
||||
pw_edit.text = _pw
|
||||
pw_edit.secret = true
|
||||
pw_edit.placeholder_text = "密码"
|
||||
@@ -368,6 +426,13 @@ func _refresh_channels() -> void:
|
||||
# 频道负载 —— 先问 CServerStateChecker(一个连接拿全部频道状态:正常/拥挤/爆满/关闭),
|
||||
# 没答上的频道回退纯 TCP 连通性(● 通 / ○ 不通)。
|
||||
func _probe_channels() -> void:
|
||||
if _probe_in_flight:
|
||||
return
|
||||
_probe_in_flight = true
|
||||
await _probe_channels_impl()
|
||||
_probe_in_flight = false
|
||||
|
||||
func _probe_channels_impl() -> void:
|
||||
var s: Dictionary = serverinfo.server(_sel_server)
|
||||
if s.is_empty():
|
||||
return
|
||||
@@ -433,7 +498,13 @@ func _do_connect() -> void:
|
||||
_set_status("没有服务器")
|
||||
return
|
||||
_set_status("连接 %s:%d …" % [addr["game_host"], addr["game_port"]])
|
||||
if client.has_method("connect_to_server"):
|
||||
if client.has_method("retry_login"):
|
||||
# M2Client selects the 40250 LoginWindow.Connect retry owner when the
|
||||
# current LoginPhase came from a credential/connection failure, and falls
|
||||
# back to the initial full connect when no retry is pending.
|
||||
client.retry_login(addr["auth_host"], addr["auth_port"],
|
||||
addr["game_host"], addr["game_port"], _id, _pw)
|
||||
elif client.has_method("connect_to_server"):
|
||||
client.connect_to_server(addr["auth_host"], addr["auth_port"],
|
||||
addr["game_host"], addr["game_port"], _id, _pw)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user