# UiScript (P1) —— 解析原始客户端的 `assets/uiscript/*.py` 布局脚本为嵌套 Dictionary。 # # 这些文件是 Python **dict 字面量**,用到的子集很小: # 语句: import X(忽略)· NAME = (文件常量)· window = {...} # 表达式:str/int/float · NAME · NAME.ATTR · (e) · 元组 (e,e) · 列表 [e,e] · # dict {k:v} · 一元 - · 二元 + - * / # 注释: # 和 ## 到行尾 # # 名字解析顺序:文件常量 → {SCREEN_WIDTH, SCREEN_HEIGHT} → uiScriptLocale. # (交给 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)