- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
358 lines
9.3 KiB
GDScript
358 lines
9.3 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
|
||
|
||
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)
|