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
328 lines
8.2 KiB
GDScript
328 lines
8.2 KiB
GDScript
# 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)
|