Files
mtgodot-poc/project/system_menu_ui_test.gd
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

143 lines
5.3 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.
# system_menu_ui_test —— ESC 系统菜单 + 游戏设置窗 headless 自检(真 uiscript + 假 client)。
# godot --headless --path project --script system_menu_ui_test.gd
# 校验 systemdialog.py / gameoptiondialog.py 装载、/setblockmode + /pkmode 真命令、
# /in_game_mall + /phase_select + /logout、显示开关持久化。缺 uiscript 时优雅跳过。
extends SceneTree
const GameOptionUI = preload("res://ui/game_option_ui.gd")
const SystemMenuUI = preload("res://ui/system_menu_ui.gd")
class FakeClient extends Node:
var says: Array = []
func say(type: int, text: String) -> bool:
says.append([type, text]); return true
func last_say() -> String:
return String(says[-1][1]) if not says.is_empty() else ""
class StubWin extends Node:
var opened := 0
func open() -> void: opened += 1
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: system_menu_ui_test (systemdialog + gameoptiondialog + /setblockmode /pkmode /phase_select)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _cfg_path() -> String:
return ProjectSettings.globalize_path("user://system_option.cfg")
func _has_say(fc, text: String) -> bool:
for s in fc.says:
if String(s[1]) == text:
return true
return false
func _run() -> void:
var assets := AssetRoot.path()
if not FileAccess.file_exists(assets.path_join("uiscript/uiscript/systemdialog.py")) \
or not FileAccess.file_exists(assets.path_join("uiscript/uiscript/gameoptiondialog.py")):
print(" (skip: no systemdialog.py / gameoptiondialog.py — assets checkout missing)")
return
if FileAccess.file_exists(_cfg_path()):
DirAccess.remove_absolute(_cfg_path())
var UiManager = load("res://ui/ui_manager.gd")
var ui: CanvasLayer = UiManager.new()
get_root().add_child(ui)
ui._ready()
var fc := FakeClient.new()
get_root().add_child(fc)
# ---------------- GameOptionUI ----------------
var go: Node = GameOptionUI.new()
get_root().add_child(go)
go.setup(ui, fc, assets)
go.open()
_ck(go.is_open(), "game_option window opened")
if not go.is_open():
return
# 屏蔽 toggle -> /setblockmode <mask ^ bit>EBlockAction 位)
(go._node("block_exchange_button") as BaseButton).pressed.emit()
_ck(fc.last_say() == "/setblockmode 1", "block exchange -> /setblockmode 1 (bit 1<<0)")
(go._node("block_party_button") as BaseButton).pressed.emit()
_ck(fc.last_say() == "/setblockmode 3", "block party -> /setblockmode 3 (1|2)")
(go._node("block_exchange_button") as BaseButton).pressed.emit()
_ck(fc.last_say() == "/setblockmode 2", "block exchange again -> /setblockmode 2 (bit cleared)")
_ck(go._block_mode == 2, "_block_mode tracked = 2")
# PK radio -> /pkmode {0,1,4,2}
(go._node("pvp_revenge") as BaseButton).pressed.emit()
_ck(fc.last_say() == "/pkmode 1", "pvp_revenge -> /pkmode 1")
(go._node("pvp_guild") as BaseButton).pressed.emit()
_ck(fc.last_say() == "/pkmode 4", "pvp_guild -> /pkmode 4")
_ck(not (go._node("pvp_revenge") as BaseButton).button_pressed, "pvp radio exclusive")
# 显示开关 -> 持久化 user://system_option.cfg [gameopt]
(go._node("always_show_name_off_button") as BaseButton).pressed.emit()
_ck(int(go._display.get("always_show_name", -1)) == 0, "always_show_name -> 0")
var cfg := ConfigFile.new()
cfg.load(_cfg_path())
_ck(int(cfg.get_value("gameopt", "always_show_name", -1)) == 0, "cfg gameopt.always_show_name persisted")
_ck(int(cfg.get_value("gameopt", "block_mode", -1)) == 2, "cfg gameopt.block_mode persisted")
go.close()
# 二次实例读 cfgblock_mode 回填
var go2: Node = GameOptionUI.new()
get_root().add_child(go2)
go2.setup(ui, fc, assets)
_ck(go2._block_mode == 2, "reload: block_mode from cfg")
go2.open()
_ck((go2._node("block_party_button") as BaseButton).button_pressed, "reload: block_party button reflects bit 1<<1")
_ck(not (go2._node("block_exchange_button") as BaseButton).button_pressed, "reload: block_exchange button off")
go2.close()
# ---------------- SystemMenuUI ----------------
fc.says.clear()
var sys_opt := StubWin.new()
var game_opt := StubWin.new()
get_root().add_child(sys_opt)
get_root().add_child(game_opt)
var sm: Node = SystemMenuUI.new()
get_root().add_child(sm)
sm.setup(ui, fc, assets, sys_opt, game_opt)
sm.open()
_ck(sm.is_open(), "system menu opened")
(sm._node("mall_button") as BaseButton).pressed.emit()
_ck(_has_say(fc, "/in_game_mall"), "mall_button -> /in_game_mall")
_ck(not sm.is_open(), "mall_button closes menu")
sm.open()
(sm._node("change_button") as BaseButton).pressed.emit()
_ck(_has_say(fc, "/phase_select"), "change_button -> /phase_select (net.ExitGame)")
sm.open()
(sm._node("logout_button") as BaseButton).pressed.emit()
_ck(_has_say(fc, "/logout"), "logout_button -> /logout (net.LogOutGame)")
sm.open()
(sm._node("system_option_button") as BaseButton).pressed.emit()
_ck(sys_opt.opened == 1 and not sm.is_open(), "system_option_button -> opens system_option_ui + closes menu")
sm.open()
(sm._node("game_option_button") as BaseButton).pressed.emit()
_ck(game_opt.opened == 1 and not sm.is_open(), "game_option_button -> opens game_option_ui + closes menu")
sm.open()
(sm._node("cancel_button") as BaseButton).pressed.emit()
_ck(not sm.is_open(), "cancel_button closes menu")
if FileAccess.file_exists(_cfg_path()):
DirAccess.remove_absolute(_cfg_path())