Files
mtgodot-poc/project/p10_test.gd
T
shenleiandClaude Sonnet 5 f32064c74a 40250 classic: parallel-dev checkpoint through increment 49 (W0 interface freeze)
Captures the uncommitted parallel-development work (increments 46-49) on the
40250 classic client port, folded into docs/CLIENT-GAP.md + CLIENT-GAP-FIX.md.

Increment 49 (W0 interface-freeze batch, Phase 1 prerequisite) lands the
cross-workflow shared-file scaffolding so the 4 Phase 1 workflows can run in
isolated worktrees without contention:

- entity_store.{h,cpp}: Entity gains empire/affect_flags/owner_vid/state_flags;
  new mut_spawn_full() (single-shot two-packet merge, §2.3), mut_ownership()
  (§2.5), mut_map_bgm() + take_bgm_dirty()/bgm_name()/bgm_volume() (§9.1),
  drain_dirty() (§2.1/§2.5 bare-field-update queue); mut_char_info() now stores
  empire; reset_for_map_change() clears m_dirty.
- m2_client.cpp: new bgm_changed(name, volume) signal; classic + m2dev pump
  loops drain drain_dirty() -> entity_info and take_bgm_dirty() -> bgm_changed;
  entity_dict() exposes the 4 new keys.
- classic/classic_parser.{h,cpp}: GC_MAIN_CHARACTER3_BGM /
  GC_MAIN_CHARACTER4_BGM_VOL route bgm_name/bgm_volume to mut_map_bgm() (was
  discarded); new m_pending_actor staging map (W1 fills §2.2 merge logic).
- project/bgm_director.gd (new) + game_scene.gd: BGM consumption split out of
  net_world.gd so W2/W4 don't collide; §8.6/§4.8 keybind convergence (digits
  1-4 -> quickslots 0-3, F1-F4 -> quickslots 4-7, Ctrl+1..9 -> _emote()).

Tests: cmake --build build clean; ctest 16/16; 15 GDScript regressions green
(netbridge, gamescene, netplay, p9, p10, p2b, p8, system_menu_ui, skill,
combat_fx, skill_fx, player_motion, char_status_ui, chat, inventory), no skips.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
2026-09-02 15:13:01 +09:00

130 lines
5.1 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.
# p10_test —— P10serverinfo + loading 遮罩 + 断线重连 + 串场状态机 headless 自检。
# godot --headless --path project --script p10_test.gd
extends SceneTree
const ServerInfoRes = preload("res://net/serverinfo.gd")
const LoadingScreen = preload("res://ui/loading_screen.gd")
const ReconnectUI = preload("res://ui/reconnect_ui.gd")
const AppFlow = preload("res://app_flow.gd")
class FakeClient extends Node:
signal phase_changed(phase: String)
signal char_list(characters: Array)
signal entered_game()
signal disconnected(reason: String)
signal login_failed(reason: String)
var reconnects := 0
var connects := []
var selects := []
func connect_to_server(ah, ap, gh, gp, id, pw) -> void:
connects.append([ah, ap, gh, gp, id, pw])
func select_character(i) -> bool: selects.append(i); return true
func reconnect() -> bool: reconnects += 1; return true
var _fail := 0
func _ck(c: bool, m: String) -> void:
if not c:
_fail += 1
printerr("FAIL: " + m)
func _init() -> void:
_run()
if _fail == 0:
print("PASS: p10_test (serverinfo + loading + reconnect + app flow)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _run() -> void:
# --- serverinfo ---
var si: RefCounted = ServerInfoRes.new()
_ck(si.count() == 1 and si.server(0)["auth_host"] == "192.168.21.203", "serverinfo: built-in default")
var addr: Dictionary = si.address(0, 3)
_ck(addr["game_port"] == 13022, "serverinfo: 内置 CH3 -> 13022 (step 10)")
var tmp := "user://p10_servers.txt"
var f := FileAccess.open(tmp, FileAccess.WRITE)
f.store_line("# test")
f.store_line("欧服\teu.example.com\t13000\teu.example.com\t13011\t1,2\t10")
f.close()
_ck(si.load_file(tmp) and si.count() == 1 and si.server(0)["name"] == "欧服", "serverinfo: parse file")
_ck(si.address(0, 2)["game_port"] == 13021, "serverinfo: file port_step 10 覆盖 -> CH2 = 13021")
# --- loading screen ---
var ls: Node = LoadingScreen.new()
get_root().add_child(ls)
ls.setup(null)
_ck(not ls.is_showing(), "loading: hidden initially")
ls._on_phase("loading")
_ck(ls.is_showing() and ls._label.text.contains("地图"), "loading: shows on phase=loading")
ls.set_progress(0.5)
_ck(ls._bar.visible and ls._bar.value == 50.0, "loading: progress bar")
ls._on_phase("select")
_ck(not ls.is_showing(), "loading: hides on phase=select (选人界面要露出来)")
ls._on_phase("loading")
ls._on_phase("game")
_ck(not ls.is_showing(), "loading: hides on phase=game")
ls._on_phase("login")
_ck(not ls.is_showing(), "loading: hides on phase=login")
# --- reconnect ---
var fc := FakeClient.new()
get_root().add_child(fc)
var rc: Node = ReconnectUI.new()
get_root().add_child(rc)
rc.setup(fc)
_ck(not rc.is_showing(), "reconnect: hidden initially")
fc.disconnected.emit("peer closed")
_ck(rc.is_showing() and rc._msg.text.contains("peer closed"), "reconnect: shows on disconnect")
# 倒计时耗尽 -> reconnect()
rc._left = 0.05
rc._process(0.1)
_ck(fc.reconnects == 1, "reconnect: auto-retry fired")
# 手动
rc._retry_now()
_ck(fc.reconnects == 2, "reconnect: manual retry")
# entered_game -> 收起
fc.entered_game.emit()
_ck(not rc.is_showing(), "reconnect: dismissed on entered_game")
# --- app flow 状态机 ---
var fc2 := FakeClient.new()
var af: Node = AppFlow.new()
af.build_game_scene = false
get_root().add_child(af)
af.start("", fc2)
_ck(af.state() == AppFlow.LOGIN, "flow: starts at LOGIN")
# 检测频道按钮存在 + 探测不可达端口快速返回 false
var has_probe := false
for b in af._ui.find_children("*", "Button", true, false):
if String(b.text) == "检测频道":
has_probe = true
_ck(has_probe, "flow: 检测频道 button present")
var reachable: bool = await af._probe_tcp("127.0.0.1", 1, 0.3)
_ck(reachable == false, "flow: _probe_tcp unreachable -> false")
af._do_connect()
_ck(fc2.connects.size() == 1 and fc2.connects[0][3] == 13002, "flow: connect uses serverinfo addr")
if af._loading and af._loading.has_method("show_for"):
af._loading.show_for("载入角色…") # 模拟 phase=select 时遮罩已亮
fc2.char_list.emit([{"index": 0, "name": "Hero", "level": 42}])
_ck(af.state() == AppFlow.SELECT, "flow: char_list -> SELECT")
_ck(af._loading == null or not af._loading.is_showing(),
"flow: _goto_select 收起读取遮罩(选人界面不被盖)")
# 新版选人页:默认已选 slot 0,点“开始”进游戏
var picked := false
for b in af._ui.find_children("*", "Button", true, false):
if String(b.text) == "开始":
b.pressed.emit()
picked = true
_ck(picked and fc2.selects == [0], "flow: pick char -> select_character(0)")
fc2.entered_game.emit()
_ck(af.state() == AppFlow.GAME, "flow: entered_game -> GAME")
# 在 GAME 时断线 -> 回 LOGINClientVS22 SetLoginPhase 语义)
fc2.disconnected.emit("game dropped")
_ck(af.state() == AppFlow.LOGIN, "flow: disconnect in GAME -> LOGIN")
# 再次收到角色列表时允许回到 SELECT
fc2.char_list.emit([{"index": 0, "name": "Hero", "level": 42}])
_ck(af.state() == AppFlow.SELECT, "flow: back to SELECT")
fc2.disconnected.emit("dropped")
_ck(af.state() == AppFlow.LOGIN, "flow: disconnect in SELECT -> LOGIN")