# RaceSpec (P2) —— 解析角色种族规格文件 `.msm`(文本树,非二进制 mesh)。 # # 客户端 GameLib/RaceDataFile.cpp `CRaceData::LoadRaceData`。格式是 Metin2 的 # CTextFileLoader 花括号树: # ScriptType RaceDataScript # BaseModelFileName "D:/YMIR WORK/pc/warrior/warrior_novice.GR2" # Group ShapeData { # PathName "d:/ymir Work/pc/warrior/" # ShapeDataCount 26 # Group ShapeData00 { ShapeIndex 0 Model "warrior_novice.gr2" # SourceSkin "..dds" TargetSkin "..dds" } # ... # } # Group HairData { ... 同结构,HairIndex/Model/SourceSkin/TargetSkin ... } # # var rs := preload("res://ui/race_spec.gd").new() # rs.load_file("/PC/ymir work/pc/warrior/warrior.msm") # 或 season1/pc/warrior.msm # rs.shape(9) # { model:"warrior_cheongrin.gr2", source_skin:"..", target_skin:"..", path:"d:/ymir Work/pc/warrior/" } # rs.hair(3) # 同上 extends RefCounted var base_model := "" var shape_path := "" var hair_path := "" var shapes := {} # shape_index:int -> { model, source_skin, target_skin } var hairs := {} # hair_index:int -> { model, source_skin, target_skin } var _dir := "" # 规格文件所在目录(资产实际就在这旁边) func load_file(path: String) -> bool: shapes.clear() hairs.clear() if not FileAccess.file_exists(path): return false _dir = path.get_base_dir() var toks := _tokenize(FileAccess.get_file_as_string(path)) var i := [0] var tree := _parse_block(toks, i, true) # 顶层当一个隐式 block base_model = _s(tree.get("BaseModelFileName", "")) var sd: Dictionary = tree.get("__group_ShapeData", {}) shape_path = _s(sd.get("PathName", "")) for k in sd: if String(k).begins_with("__group_ShapeData") and sd[k] is Dictionary: var g: Dictionary = sd[k] if g.has("ShapeIndex"): shapes[int(g["ShapeIndex"])] = _entry(g) var hd: Dictionary = tree.get("__group_HairData", {}) hair_path = _s(hd.get("PathName", "")) for k in hd: if String(k).begins_with("__group_HairData") and hd[k] is Dictionary: var g: Dictionary = hd[k] if g.has("HairIndex"): hairs[int(g["HairIndex"])] = _entry(g) return shapes.size() > 0 func shape(idx: int) -> Dictionary: var e: Dictionary = shapes.get(idx, {}) if e.is_empty(): return {} e = e.duplicate() e["path"] = shape_path e["spec_dir"] = _dir return e func hair(idx: int) -> Dictionary: var e: Dictionary = hairs.get(idx, {}) if e.is_empty(): return {} e = e.duplicate() e["path"] = hair_path e["spec_dir"] = _dir return e # --- internals -------------------------------------------------------- func _entry(g: Dictionary) -> Dictionary: return { "model": _s(g.get("Model", g.get("local_model", ""))).strip_edges(), "source_skin": _s(g.get("SourceSkin", "")).strip_edges(), "target_skin": _s(g.get("TargetSkin", "")).strip_edges(), } func _s(v) -> String: return v if v is String else str(v) # 词法:按空白切;"..." 是一个 token;{ } 单独 token;# / // 到行尾注释 func _tokenize(src: String) -> Array: var out := [] var i := 0 var n := src.length() while i < n: var c := src[i] if c in [" ", "\t", "\r", "\n"]: i += 1 elif c == '"': var j := i + 1 var s := "" while j < n and src[j] != '"': s += src[j] j += 1 out.append("" + s) # 前缀标记「这是带引号的字符串值」 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 # 解析一个 { } 块(或 top=true 的隐式顶层)为 Dictionary。 # KEY VALUE -> d[KEY] = VALUE # Group NAME { ... } -> d["__group_NAME"] = func _parse_block(toks: Array, i: Array, top := false) -> Dictionary: var d := {} var guard := 0 while i[0] < toks.size(): guard += 1 if guard > 100000: break var t = toks[i[0]] if t == "}": i[0] += 1 return d if t is String and t == "Group": i[0] += 1 var name := String(toks[i[0]]) if i[0] < toks.size() else "?" i[0] += 1 if i[0] < toks.size() and toks[i[0]] == "{": i[0] += 1 d["__group_" + name] = _parse_block(toks, i) continue if t == "{": i[0] += 1 continue # KEY VALUE var key: String = str(_tokval(t)) i[0] += 1 if i[0] < toks.size() and toks[i[0]] != "}" and toks[i[0]] != "{": d[key] = _tokval(toks[i[0]]) i[0] += 1 if top and i[0] >= toks.size(): break return d func _tokval(t): if t is Dictionary and t.has("str"): return t["str"] return str(t)