Files
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

328 lines
8.2 KiB
GDScript
Raw Permalink 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.
# UiScript (P1) —— 解析原始客户端的 `assets/uiscript/*.py` 布局脚本为嵌套 Dictionary。
#
# 这些文件是 Python **dict 字面量**,用到的子集很小:
# 语句: import X(忽略)· NAME = <expr>(文件常量)· window = {...}
# 表达式:str/int/float · NAME · NAME.ATTR · (e) · 元组 (e,e) · 列表 [e,e] ·
# dict {k:v} · 一元 - · 二元 + - * /
# 注释: # 和 ## 到行尾
#
# 名字解析顺序:文件常量 → {SCREEN_WIDTH, SCREEN_HEIGHT} → uiScriptLocale.<X>
# (交给 locale 回调)→ 未知则 0 / ""。除法按浮点算(调用方按需取整)。
#
# 用法:
# var us := preload("res://ui/uiscript.gd").new()
# us.screen = Vector2i(1920, 1080)
# us.locale = func(key): return Locale.t(key) # 可选
# var tree: Dictionary = us.parse_file("res://../assets/uiscript/uiscript/popupdialog.py")
# # tree = { name, type?, x, y, width, height, children:[...], ... }
extends RefCounted
var screen := Vector2i(1920, 1080)
var locale: Callable # func(key: String) -> String
var last_error := ""
var _consts := {} # 文件顶层 NAME = expr
var _s := "" # 源文本
var _p := 0 # 游标
# --- public ---------------------------------------------------------------
func parse_file(path: String) -> Dictionary:
if not FileAccess.file_exists(path):
last_error = "no such file: " + path
return {}
return parse_text(FileAccess.get_file_as_string(path))
func parse_text(src: String) -> Dictionary:
last_error = ""
_consts = {"SCREEN_WIDTH": screen.x, "SCREEN_HEIGHT": screen.y}
_s = _strip_comments(src)
_p = 0
var window := {}
while true:
_skip_ws()
if _p >= _s.length():
break
if _match_kw("import"):
_skip_to_newline()
continue
var name := _read_ident()
if name == "":
# 可能是散落的括号/逗号;跳一个字符防死循环
_p += 1
continue
_skip_ws()
if _p < _s.length() and _s[_p] == "=":
_p += 1
var val = _parse_expr()
if name == "window":
window = val if val is Dictionary else {}
else:
_consts[name] = val
else:
_skip_to_newline()
if window.is_empty() and last_error == "":
last_error = "no `window = {...}` found"
return window
# --- lexing helpers -----------------------------------------------------
func _strip_comments(src: String) -> String:
var out := ""
var in_str := false
var q := ""
var i := 0
while i < src.length():
var c := src[i]
if in_str:
out += c
if c == "\\" and i + 1 < src.length():
out += src[i + 1]
i += 2
continue
if c == q:
in_str = false
i += 1
continue
if c == '"' or c == "'":
in_str = true
q = c
out += c
elif c == "#":
while i < src.length() and src[i] != "\n":
i += 1
continue
else:
out += c
i += 1
return out
func _skip_ws() -> void:
while _p < _s.length() and _s[_p] in [" ", "\t", "\r", "\n", ","]:
_p += 1
func _skip_to_newline() -> void:
while _p < _s.length() and _s[_p] != "\n":
_p += 1
func _match_kw(kw: String) -> bool:
_skip_ws()
if _s.substr(_p, kw.length()) == kw:
var after := _p + kw.length()
if after >= _s.length() or not _is_ident_char(_s[after]):
_p = after
return true
return false
func _is_ident_char(c: String) -> bool:
return c == "_" or (c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or (c >= "0" and c <= "9")
func _read_ident() -> String:
_skip_ws()
var start := _p
while _p < _s.length() and _is_ident_char(_s[_p]):
_p += 1
return _s.substr(start, _p - start)
# --- expression parser (precedence: + - then * /) ----------------------
func _parse_expr():
var lhs = _parse_term()
while _p < _s.length():
_skip_ws_no_comma()
if _p >= _s.length():
break
var c := _s[_p]
if c == "+" or c == "-":
_p += 1
var rhs = _parse_term()
if c == "+" and (lhs is String or rhs is String):
lhs = _str_or(lhs) + _str_or(rhs)
elif c == "+":
lhs = _num(lhs) + _num(rhs)
else:
lhs = _num(lhs) - _num(rhs)
elif c == "%":
# Python 字符串格式化 "fmt" % (args):取 fmt,丢掉格式参数
_p += 1
_parse_term()
elif c == '"' or c == "'":
# Python 相邻字符串字面量隐式拼接: "a" "b"
lhs = _str_or(lhs) + _str_or(_parse_atom())
else:
break
return lhs
func _parse_term():
var lhs = _parse_atom()
while true:
_skip_ws_no_comma()
if _p < _s.length() and (_s[_p] == "*" or _s[_p] == "/"):
var op := _s[_p]
_p += 1
var rhs = _parse_atom()
if op == "*":
lhs = _num(lhs) * _num(rhs)
else:
lhs = _num(lhs) / _num(rhs) if _num(rhs) != 0.0 else 0.0
else:
break
return lhs
func _skip_ws_no_comma() -> void:
while _p < _s.length() and _s[_p] in [" ", "\t", "\r", "\n"]:
_p += 1
func _parse_atom():
_skip_ws_no_comma()
if _p >= _s.length():
return 0
var c := _s[_p]
if c == "-":
_p += 1
return -_num(_parse_atom())
if c == '"' or c == "'":
return _parse_string()
if c == "{":
return _parse_dict()
if c == "[":
return _parse_seq("[", "]")
if c == "(":
return _parse_seq("(", ")") # 元组或加括号的表达式,_parse_seq 统一处理
if c == "_" or (c >= "A" and c <= "Z") or (c >= "a" and c <= "z"):
return _parse_name()
if (c >= "0" and c <= "9") or c == ".":
return _parse_number()
_p += 1
return 0
func _parse_string():
var q := _s[_p]
_p += 1
var out := ""
while _p < _s.length() and _s[_p] != q:
if _s[_p] == "\\" and _p + 1 < _s.length():
var n := _s[_p + 1]
out += "\n" if n == "n" else ("\t" if n == "t" else n)
_p += 2
continue
out += _s[_p]
_p += 1
_p += 1 # closing quote
return out
func _parse_number():
var start := _p
var is_float := false
while _p < _s.length() and ((_s[_p] >= "0" and _s[_p] <= "9") or _s[_p] == "." or _s[_p] == "x" \
or (_s[_p] >= "a" and _s[_p] <= "f") or (_s[_p] >= "A" and _s[_p] <= "F")):
if _s[_p] == ".":
is_float = true
_p += 1
var tok := _s.substr(start, _p - start)
if tok.begins_with("0x") or tok.begins_with("0X"):
return tok.hex_to_int()
return float(tok) if is_float else int(tok)
func _parse_name():
var name := _read_ident()
# 属性链:uiScriptLocale.KEY / X.Y
while _p < _s.length() and _s[_p] == ".":
_p += 1
var attr := _read_ident()
if name == "uiScriptLocale":
return _resolve_locale(attr)
name += "." + attr
if _consts.has(name):
return _consts[name]
if name == "True":
return true
if name == "False":
return false
if name == "None":
return null
# 未知名字:数值上下文当 0,字符串上下文当 "";这里返回 0_str_or 会兜
return 0
func _resolve_locale(key: String):
if locale.is_valid():
var v = locale.call(key)
if v != null and String(v) != "":
return String(v)
return "<" + key + ">"
func _parse_dict() -> Dictionary:
var d := {}
_p += 1 # {
var guard := 0
while _p < _s.length():
guard += 1
if guard > 4000:
last_error = "dict parse runaway"
break
_skip_ws()
if _p >= _s.length() or _s[_p] == "}":
_p += 1
break
var before := _p
var key = _parse_atom()
_skip_ws()
if _p < _s.length() and _s[_p] == ":":
_p += 1
var val = _parse_expr()
d[_key(key)] = val
elif _p == before:
_p += 1 # 没进展也没 ':',强制前进防死循环
# 否则:孤立 token(相邻字符串续行残留等)——丢弃,继续
_skip_ws()
return d
func _key(k) -> String:
if k is String:
return k
if k is int or k is float:
return str(k)
return "_"
# 统一处理 ( ) 和 [ ]:单元素且无尾逗号的 (x) 退化成 x,否则是数组。
func _parse_seq(open: String, close: String):
_p += 1 # opening
var items := []
var had_comma := false
var guard := 0
while _p < _s.length():
guard += 1
if guard > 4000:
break
_skip_ws_no_comma()
if _p >= _s.length() or _s[_p] == close:
_p += 1
break
if _s[_p] == ",":
had_comma = true
_p += 1
continue
var before := _p
items.append(_parse_expr())
if _p == before:
_p += 1
if open == "(" and items.size() == 1 and not had_comma:
return items[0]
return items
# --- coercion ----------------------------------------------------------
func _num(v) -> float:
if v is float:
return v
if v is int:
return float(v)
if v is bool:
return 1.0 if v else 0.0
return 0.0
func _str_or(v) -> String:
if v is String:
return v
return str(v)