Files

257 lines
6.9 KiB
GDScript
Raw Permalink 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.
# 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