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
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
# ui_test —— P1 UI 工具层 headless 自检(解析器 + 构建器 + 窗口管理器)。
|
||||
# godot --headless --path project --script ui_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const UiScript = preload("res://ui/uiscript.gd")
|
||||
const UiBuild = preload("res://ui/ui_build.gd")
|
||||
const UiManager = preload("res://ui/ui_manager.gd")
|
||||
const Dialogs = preload("res://ui/dialogs.gd")
|
||||
|
||||
const FIXTURE := """
|
||||
import uiScriptLocale
|
||||
ROOT = "d:/ymir work/ui/public/"
|
||||
PAD = 10
|
||||
|
||||
window = {
|
||||
"name" : "TestWin",
|
||||
"style" : ("float",),
|
||||
"x" : SCREEN_WIDTH/2 - 250,
|
||||
"y" : SCREEN_HEIGHT/2 - 40,
|
||||
"width" : 280,
|
||||
"height" : 105,
|
||||
"children" : (
|
||||
{
|
||||
"name" : "board",
|
||||
"type" : "board_with_titlebar",
|
||||
"x" : 0, "y" : 0,
|
||||
"width" : 280, "height" : 105,
|
||||
"title" : uiScriptLocale.HELLO,
|
||||
"children" : (
|
||||
{
|
||||
"name" : "message",
|
||||
"type" : "text",
|
||||
"x" : PAD, "y" : 38,
|
||||
"text" : uiScriptLocale.MESSAGE,
|
||||
"text_horizontal_align" : "center",
|
||||
"text_color" : 0xffF8BF24,
|
||||
},
|
||||
{
|
||||
"name" : "accept",
|
||||
"type" : "button",
|
||||
"x" : 0, "y" : 63,
|
||||
"width" : 61, "height" : 21,
|
||||
"horizontal_align" : "center",
|
||||
"text" : uiScriptLocale.OK,
|
||||
"default_image" : ROOT + "middle_button_01.sub",
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
"""
|
||||
|
||||
var _fail := 0
|
||||
func _ck(c: bool, m: String) -> void:
|
||||
if not c:
|
||||
_fail += 1
|
||||
printerr("FAIL: " + m)
|
||||
|
||||
func _init() -> void:
|
||||
_test_parser()
|
||||
_test_builder()
|
||||
_test_manager()
|
||||
_test_dialogs()
|
||||
_test_real_file()
|
||||
if _fail == 0:
|
||||
print("PASS: ui_test (parser + builder + manager)")
|
||||
quit(0)
|
||||
else:
|
||||
printerr("%d check(s) failed" % _fail)
|
||||
quit(1)
|
||||
|
||||
func _test_parser() -> void:
|
||||
var us := UiScript.new()
|
||||
us.screen = Vector2i(1920, 1080)
|
||||
us.locale = func(k: String) -> String:
|
||||
return {"HELLO": "Hi", "MESSAGE": "Body", "OK": "OK"}.get(k, "")
|
||||
var w := us.parse_text(FIXTURE)
|
||||
_ck(us.last_error == "", "parser: no error (%s)" % us.last_error)
|
||||
_ck(w.get("name") == "TestWin", "parser: window name")
|
||||
_ck(int(w.get("x")) == 710, "parser: arithmetic SCREEN_WIDTH/2-250 = 710, got %s" % w.get("x"))
|
||||
_ck(int(w.get("width")) == 280, "parser: width")
|
||||
var kids: Array = w.get("children", [])
|
||||
_ck(kids.size() == 1 and kids[0].get("type") == "board_with_titlebar", "parser: child type")
|
||||
_ck(kids[0].get("title") == "Hi", "parser: locale HELLO -> 'Hi'")
|
||||
var inner: Array = kids[0].get("children", [])
|
||||
_ck(inner.size() == 2, "parser: 2 grandchildren")
|
||||
_ck(inner[0].get("text") == "Body", "parser: locale MESSAGE")
|
||||
_ck(int(inner[0].get("text_color")) == 0xffF8BF24, "parser: hex color")
|
||||
_ck(int(inner[1].get("x")) == 0 and inner[1].get("horizontal_align") == "center",
|
||||
"parser: const PAD + align kept")
|
||||
# 空输入 / 坏输入
|
||||
_ck(UiScript.new().parse_text("import x\n").is_empty(), "parser: no window -> {}")
|
||||
|
||||
func _test_builder() -> void:
|
||||
var us := UiScript.new()
|
||||
us.screen = Vector2i(1920, 1080)
|
||||
us.locale = func(k: String) -> String: return k
|
||||
var spec := us.parse_text(FIXTURE)
|
||||
var r := UiBuild.build(spec, "") # 无 assets -> 主题兜底
|
||||
var root: Control = r.root
|
||||
_ck(root is Control, "builder: root is Control")
|
||||
_ck(root.size == Vector2(280, 105), "builder: root size")
|
||||
_ck(root.position == Vector2(710, 500), "builder: root pos, got %s" % root.position)
|
||||
var nodes: Dictionary = r.nodes
|
||||
_ck(nodes.has("accept") and nodes["accept"] is Button, "builder: accept is Button")
|
||||
_ck(nodes["accept"].text == "OK", "builder: button text")
|
||||
# horizontal_align center:x = (280 - 61)/2 + 0 = 109(父 board 宽 280)
|
||||
_ck(nodes["accept"].position.x == 109, "builder: center align x = 109, got %s" % nodes["accept"].position.x)
|
||||
_ck(nodes.has("message") and nodes["message"] is Label, "builder: message is Label")
|
||||
_ck(nodes["message"].horizontal_alignment == HORIZONTAL_ALIGNMENT_CENTER, "builder: text align")
|
||||
_ck(nodes["board"] is Panel, "builder: board is Panel")
|
||||
_ck(nodes["board"].has_node("TitleText"), "builder: board_with_titlebar has title label")
|
||||
|
||||
func _test_manager() -> void:
|
||||
var ui: CanvasLayer = UiManager.new()
|
||||
get_root().add_child(ui)
|
||||
ui._ready() # ensure built (add_child 后一帧才 _ready,这里直接调)
|
||||
var w1 := Control.new()
|
||||
w1.custom_minimum_size = Vector2(100, 100)
|
||||
var w2 := Control.new()
|
||||
ui.open(w1)
|
||||
ui.open(w2, true) # modal
|
||||
_ck(ui.top() == w2, "manager: w2 on top")
|
||||
_ck(ui._dim.visible, "manager: modal dim shown")
|
||||
_ck(ui.close_top(), "manager: close_top ok")
|
||||
_ck(ui.top() == w1, "manager: w1 now top")
|
||||
_ck(not ui._dim.visible, "manager: dim hidden after modal closed")
|
||||
_ck(not ui.close_top() == false, "manager: close last")
|
||||
ui.close_top()
|
||||
_ck(ui.top() == null, "manager: stack empty")
|
||||
ui.queue_free()
|
||||
|
||||
func _test_dialogs() -> void:
|
||||
var ui: CanvasLayer = UiManager.new()
|
||||
get_root().add_child(ui)
|
||||
ui._ready()
|
||||
var dlg := Dialogs.new(ui)
|
||||
var got := [0]
|
||||
dlg.confirm("delete?", func(): got[0] = 1, func(): got[0] = -1)
|
||||
_ck(ui.top() != null, "dialogs: confirm opened")
|
||||
_ck(ui._dim.visible, "dialogs: confirm is modal")
|
||||
# 找到「确定」按钮点一下
|
||||
var ok_btn: Button = null
|
||||
for n in ui.top().get_children():
|
||||
if n is Button and n.text == "确定":
|
||||
ok_btn = n
|
||||
_ck(ok_btn != null, "dialogs: 确定 button present")
|
||||
if ok_btn:
|
||||
ok_btn.pressed.emit()
|
||||
_ck(got[0] == 1, "dialogs: on_ok fired")
|
||||
_ck(ui.top() == null, "dialogs: closed after confirm")
|
||||
ui.queue_free()
|
||||
|
||||
func _test_real_file() -> void:
|
||||
var p := AssetRoot.sub("uiscript/uiscript/popupdialog.py")
|
||||
if not FileAccess.file_exists(p):
|
||||
print(" (skip real-file test: %s not present)" % p)
|
||||
return
|
||||
var us := UiScript.new()
|
||||
us.locale = func(k: String) -> String: return "L_" + k
|
||||
var w := us.parse_file(p)
|
||||
_ck(us.last_error == "", "real: popupdialog parsed (%s)" % us.last_error)
|
||||
_ck(w.get("name") == "PopupDialog", "real: name == PopupDialog, got %s" % w.get("name"))
|
||||
var kids: Array = w.get("children", [])
|
||||
_ck(kids.size() >= 1 and kids[0].get("type") == "board", "real: first child is board")
|
||||
var found_btn := false
|
||||
for c in kids[0].get("children", []):
|
||||
if c.get("type") == "button" and c.get("name") == "accept":
|
||||
found_btn = true
|
||||
_ck(String(c.get("default_image", "")).ends_with(".sub"), "real: button default_image is .sub")
|
||||
_ck(found_btn, "real: found accept button")
|
||||
# 构建也不炸
|
||||
var r := UiBuild.build(w, "")
|
||||
_ck(r.root is Control and r.nodes.has("accept"), "real: build ok")
|
||||
Reference in New Issue
Block a user