Files
mtgodot-poc/project/ui/uiscript.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

358 lines
9.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.
# 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
static var _locale_dict := {}
static func _ensure_locale_dict() -> void:
if not _locale_dict.is_empty():
return
_locale_dict["WINDOWS_PATH"] = "locale/en/ui/windows/"
_locale_dict["LOCALE_UISCRIPT_PATH"] = "locale/en/ui/"
_locale_dict["LOGIN_PATH"] = "locale/en/ui/login/"
_locale_dict["EMPIRE_PATH"] = "locale/en/ui/empire/"
_locale_dict["GUILD_PATH"] = "locale/en/ui/guild/"
_locale_dict["SELECT_PATH"] = "locale/en/ui/select/"
_locale_dict["MAPNAME_PATH"] = "locale/en/ui/mapname/"
var cands := [
"assets/locale/locale/en/locale_interface.txt",
"assets/locale/en/locale_interface.txt",
"../assets/locale/locale/en/locale_interface.txt",
"../assets/locale/en/locale_interface.txt",
]
for p in cands:
if FileAccess.file_exists(p):
var txt := FileAccess.get_file_as_string(p)
for line in txt.split("\n"):
var parts := line.strip_edges().split("\t", false)
if parts.size() >= 2:
_locale_dict[parts[0].strip_edges()] = parts[1].strip_edges()
break
func _resolve_locale(key: String):
if locale.is_valid():
var v = locale.call(key)
if v != null and String(v) != "":
return String(v)
_ensure_locale_dict()
if _locale_dict.has(key):
return _locale_dict[key]
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)