# 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": } · 裸词/数字(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