257 lines
6.9 KiB
GDScript
257 lines
6.9 KiB
GDScript
# Mse (P5) —— 解析 Metin2 `.mse` 特效脚本(EffectLib,非逆向,有完整源码)。
|
||
#
|
||
# `.mse` 是 CTextFileLoader 花括号文本树:
|
||
# BoundingSphereRadius 120.0
|
||
# Group Particle {
|
||
# StartTime 0.0
|
||
# List TimeEventPosition { 0.0 "MOVING_TYPE_DIRECT" 8.0 0.0 25.0 }
|
||
# Group EmitterProperty { MaxEmissionCount 5 CycleLength 0.4 List TimeEventSizeX { 0.0 64.0 } ... }
|
||
# Group ParticleProperty { SrcBlendType 5 BillboardType 1 List TimeEventAlpha { 0.0 0.71 0.86 0.0 } List TextureFiles { "x.dds" } }
|
||
# }
|
||
# Group Mesh { MeshFileName "a.mde" MeshElementCount 1 Group MeshElement00 { ... } }
|
||
#
|
||
# var m := preload("res://fx/mse.gd").new()
|
||
# var spec := m.parse_file(path)
|
||
# # spec = { bsphere_r, bsphere_pos:Vector3, particles:[Dict], meshes:[Dict], dir }
|
||
#
|
||
# 每个 particle dict 保留原始键(大小写不变):
|
||
# start_time:float, position: [ [t, "MOVING_TYPE", x,y,z], ... ],
|
||
# emitter: Dict, particle: Dict (List 键 -> Array[Array],标量键 -> float/String/Array)
|
||
extends RefCounted
|
||
|
||
var last_error := ""
|
||
|
||
var _toks: Array = []
|
||
var _p := 0
|
||
|
||
func parse_file(path: String) -> Dictionary:
|
||
last_error = ""
|
||
if not FileAccess.file_exists(path):
|
||
last_error = "no such file: " + path
|
||
return {}
|
||
var spec := parse_text(FileAccess.get_file_as_string(path))
|
||
spec["dir"] = path.get_base_dir()
|
||
return spec
|
||
|
||
func parse_text(src: String) -> Dictionary:
|
||
_toks = _tokenize(src)
|
||
_p = 0
|
||
var top := _parse_block(true)
|
||
var out := {
|
||
"bsphere_r": float(_scalar(top.get("BoundingSphereRadius", 0.0))),
|
||
"bsphere_pos": _vec3(top.get("BoundingSpherePosition", [0, 0, 0])),
|
||
"particles": [],
|
||
"meshes": [],
|
||
"lights": [],
|
||
"dir": "",
|
||
}
|
||
for g in top.get("__group_Particle", []):
|
||
out.particles.append(_norm_particle(g))
|
||
for g in top.get("__group_Mesh", []):
|
||
out.meshes.append(_norm_mesh(g))
|
||
for g in top.get("__group_Light", []):
|
||
out.lights.append(_norm_light(g))
|
||
return out
|
||
|
||
# --- 归一化 -----------------------------------------------------------
|
||
|
||
func _norm_particle(g: Dictionary) -> Dictionary:
|
||
return {
|
||
"start_time": float(_scalar(g.get("StartTime", 0.0))),
|
||
"position": g.get("TimeEventPosition", []),
|
||
"emitter": _first(g.get("__group_EmitterProperty", [])),
|
||
"particle": _first(g.get("__group_ParticleProperty", [])),
|
||
}
|
||
|
||
func _norm_mesh(g: Dictionary) -> Dictionary:
|
||
var elems := []
|
||
for k in g:
|
||
if String(k).begins_with("__group_MeshElement"):
|
||
for e in g[k]:
|
||
elems.append(e)
|
||
return {
|
||
"start_time": float(_scalar(g.get("StartTime", 0.0))),
|
||
"position": g.get("TimeEventPosition", []),
|
||
"mesh_file": String(_scalar(g.get("MeshFileName", ""))),
|
||
"loop": int(_scalar(g.get("MeshAnimationLoopEnable", 0))),
|
||
"loop_count": int(_scalar(g.get("MeshAnimationLoopCount", 0))),
|
||
"frame_delay": float(_scalar(g.get("MeshAnimationFrameDelay", 0.0))),
|
||
"elements": elems,
|
||
}
|
||
|
||
func _norm_light(g: Dictionary) -> Dictionary:
|
||
return {
|
||
"start_time": float(_scalar(g.get("StartTime", 0.0))),
|
||
"position": g.get("TimeEventPosition", []),
|
||
"duration": float(_scalar(g.get("Duration", 1.0))),
|
||
"loop": int(_scalar(g.get("LoopFlag", 0))),
|
||
"loop_count": int(_scalar(g.get("LoopCount", 0))),
|
||
"max_range": float(_scalar(g.get("MaxRange", 300.0))),
|
||
"ambient": g.get("AmbientColor", [0.5, 0.5, 0.5, 1.0]),
|
||
"diffuse": g.get("DiffuseColor", [0.0, 0.0, 0.0, 1.0]),
|
||
"attenuation0": float(_scalar(g.get("Attenuation0", 0.0))),
|
||
"attenuation1": float(_scalar(g.get("Attenuation1", 0.1))),
|
||
"attenuation2": float(_scalar(g.get("Attenuation2", 0.0))),
|
||
"range": g.get("TimeEventRange", []),
|
||
}
|
||
|
||
func _first(a) -> Dictionary:
|
||
return a[0] if a is Array and a.size() > 0 else {}
|
||
|
||
func _scalar(v):
|
||
if v is Array:
|
||
return v[0] if v.size() > 0 else 0.0
|
||
return v
|
||
|
||
func _vec3(v) -> Vector3:
|
||
if v is Array and v.size() >= 3:
|
||
return Vector3(float(v[0]), float(v[1]), float(v[2]))
|
||
return Vector3.ZERO
|
||
|
||
# --- 词法 -----------------------------------------------------------
|
||
# tokens: "{" "}" "\n" · {"s": <quoted string>} · 裸词/数字(String)
|
||
|
||
func _tokenize(src: String) -> Array:
|
||
var out := []
|
||
var i := 0
|
||
var n := src.length()
|
||
while i < n:
|
||
var c := src[i]
|
||
if c == "\n":
|
||
out.append("\n")
|
||
i += 1
|
||
elif c in [" ", "\t", "\r"]:
|
||
i += 1
|
||
elif c == '"':
|
||
var j := i + 1
|
||
var s := ""
|
||
while j < n and src[j] != '"':
|
||
s += src[j]
|
||
j += 1
|
||
out.append({"q": s}) # 带引号的字符串(q 键区分裸词)
|
||
i = j + 1
|
||
elif c == "{" or c == "}":
|
||
out.append(c)
|
||
i += 1
|
||
elif c == "#" or (c == "/" and i + 1 < n and src[i + 1] == "/"):
|
||
while i < n and src[i] != "\n":
|
||
i += 1
|
||
else:
|
||
var j := i
|
||
while j < n and not (src[j] in [" ", "\t", "\r", "\n", "{", "}", '"']):
|
||
j += 1
|
||
out.append(src.substr(i, j - i))
|
||
i = j
|
||
return out
|
||
|
||
func _eq(v, ch) -> bool:
|
||
return v is String and v == ch
|
||
|
||
func _val(t):
|
||
if t is Dictionary:
|
||
return t.get("q", "")
|
||
var s := String(t)
|
||
# 数字?
|
||
if s.is_valid_float():
|
||
return float(s)
|
||
if s.is_valid_int():
|
||
return int(s)
|
||
return s
|
||
|
||
# 解析一个 { } 块(top=true 时是隐式顶层)。
|
||
func _parse_block(top := false) -> Dictionary:
|
||
var d := {}
|
||
var guard := 0
|
||
while _p < _toks.size():
|
||
guard += 1
|
||
if guard > 500000:
|
||
last_error = "runaway"
|
||
break
|
||
var t = _toks[_p]
|
||
if _eq(t, "}"):
|
||
_p += 1
|
||
return d
|
||
if _eq(t, "\n"):
|
||
_p += 1
|
||
continue
|
||
if _eq(t, "{"):
|
||
_p += 1
|
||
continue
|
||
var word := "" if (t is Dictionary) else String(t)
|
||
if word == "Group":
|
||
_p += 1
|
||
var name := _next_word()
|
||
_skip_newlines()
|
||
if _eq(_cur(), "{"):
|
||
_p += 1
|
||
var sub := _parse_block()
|
||
var key := "__group_" + name
|
||
if not d.has(key):
|
||
d[key] = []
|
||
d[key].append(sub)
|
||
continue
|
||
if word == "List":
|
||
_p += 1
|
||
var lname := _next_word()
|
||
_skip_newlines()
|
||
var rows := []
|
||
if _eq(_cur(), "{"):
|
||
_p += 1
|
||
rows = _parse_list_rows()
|
||
d[lname] = rows
|
||
continue
|
||
# KEY v1 v2 ... (到行尾)
|
||
var key := word
|
||
_p += 1
|
||
var vals := []
|
||
while _p < _toks.size() and not _eq(_toks[_p], "\n") and not _eq(_toks[_p], "{") and not _eq(_toks[_p], "}"):
|
||
vals.append(_val(_toks[_p]))
|
||
_p += 1
|
||
d[key] = vals[0] if vals.size() == 1 else vals
|
||
if top and _p >= _toks.size():
|
||
break
|
||
return d
|
||
|
||
func _parse_list_rows() -> Array:
|
||
var rows := []
|
||
var row := []
|
||
var guard := 0
|
||
while _p < _toks.size():
|
||
guard += 1
|
||
if guard > 200000:
|
||
break
|
||
var t = _toks[_p]
|
||
if _eq(t, "}"):
|
||
_p += 1
|
||
if not row.is_empty():
|
||
rows.append(row)
|
||
break
|
||
if _eq(t, "\n"):
|
||
_p += 1
|
||
if not row.is_empty():
|
||
rows.append(row)
|
||
row = []
|
||
continue
|
||
if _eq(t, "{"):
|
||
_p += 1
|
||
continue
|
||
row.append(_val(t))
|
||
_p += 1
|
||
return rows
|
||
|
||
func _cur():
|
||
return _toks[_p] if _p < _toks.size() else null
|
||
|
||
func _next_word() -> String:
|
||
while _p < _toks.size() and _eq(_toks[_p], "\n"):
|
||
_p += 1
|
||
if _p < _toks.size() and not (_toks[_p] is Dictionary):
|
||
var w := String(_toks[_p])
|
||
_p += 1
|
||
return w
|
||
return "?"
|
||
|
||
func _skip_newlines() -> void:
|
||
while _p < _toks.size() and _eq(_toks[_p], "\n"):
|
||
_p += 1
|