// M2 T1 —— Metin2 文本脚本 token 树解析。 #include "textscript.h" #include "m2_tokvec.h" // fmt::read_file (host-injectable; res:// on mobile) #include #include #include #include namespace fmt { namespace { bool ieq(const std::string& a, const char* b) { if (a.size() != std::strlen(b)) return false; for (size_t i = 0; i < a.size(); ++i) if (std::tolower((unsigned char)a[i]) != std::tolower((unsigned char)b[i])) return false; return true; } // tokenize 一行为止:支持 "quoted"(含空白)、# 注释 std::vector tokenize_all(const std::string& text) { std::vector toks; size_t i = 0, n = text.size(); while (i < n) { char c = text[i]; if (c == '#' || (c == '/' && i + 1 < n && text[i + 1] == '/')) { // 行注释 while (i < n && text[i] != '\n') ++i; continue; } if (std::isspace((unsigned char)c)) { ++i; continue; } if (c == '{' || c == '}') { toks.emplace_back(1, c); ++i; continue; } if (c == '"') { ++i; std::string s; while (i < n && text[i] != '"') s += text[i++]; if (i < n) ++i; // 跳过右引号 toks.push_back(s); continue; } std::string s; while (i < n && !std::isspace((unsigned char)text[i]) && text[i] != '{' && text[i] != '}') s += text[i++]; toks.push_back(s); } return toks; } // 递归解析一个 Group 体(已消费到 '{' 之后;到匹配的 '}' 或流尾停) bool parse_body(const std::vector& t, size_t& i, Node& node, std::string* err) { while (i < t.size()) { const std::string& tok = t[i]; if (tok == "}") { ++i; return true; } if (ieq(tok, "Group")) { if (i + 2 >= t.size()) { if (err) *err = "Group 缺名字 / {"; return false; } Node sub; sub.name = t[i + 1]; if (t[i + 2] != "{") { if (err) *err = "Group '" + sub.name + "' 后缺 {"; return false; } i += 3; if (!parse_body(t, i, sub, err)) return false; node.groups.push_back(std::move(sub)); continue; } // 普通行:从 tok 起,直到下一个会开启新语义的 token(Group / } / 或另一行的 key) // .msa/.msm 是「一行一 key」,我们靠换行还原不了(已丢),改用: // key + 其后所有非 Group/非} token 直到遇到一个「像 key 的」…太脆。 // 实际这些文件每行 = key + 定长值,值都是数字 / 带引号串 / 简单标识。 // 简化:把连续的 token 收进当前行,遇到 Group/} 或「首字母大写且后面还跟 Group 结构」时断。 // 经验规则:值不会是 "Group";key 之后收 1..N 个值,遇到下一个满足「不是纯数字、不是带过引号」 // 的裸标识就当新行开始。对 dance_1.msa / warrior_w.msm 足够。 std::vector line; line.push_back(t[i++]); while (i < t.size()) { const std::string& x = t[i]; if (x == "}" || ieq(x, "Group")) break; // 判断 x 是否是「新行的 key」:不含数字开头、不含 '.'、'/'、'\\'、'"' 迹象, // 且当前行已至少有 1 个值 bool looks_key = line.size() >= 2 && (std::isalpha((unsigned char)x[0]) || x[0] == '_') && x.find_first_of("/\\.:") == std::string::npos; // 数字 / 负号 / 小数一律当值 bool is_num = !x.empty() && (x[0] == '-' || x[0] == '+' || x[0] == '.' || std::isdigit((unsigned char)x[0])); if (looks_key && !is_num) break; line.push_back(x); ++i; } node.lines.push_back(std::move(line)); } return true; // 流尾(根节点) } } // namespace const std::vector* Node::find(const char* key) const { for (const auto& l : lines) if (!l.empty() && ieq(l[0], key)) return &l; return nullptr; } std::string Node::str(const char* key, const std::string& def) const { auto* l = find(key); return (l && l->size() >= 2) ? (*l)[1] : def; } float Node::num(const char* key, float def) const { auto* l = find(key); return (l && l->size() >= 2) ? float(std::atof((*l)[1].c_str())) : def; } int Node::inum(const char* key, int def) const { auto* l = find(key); return (l && l->size() >= 2) ? std::atoi((*l)[1].c_str()) : def; } const Node* Node::group(const char* name) const { for (const auto& g : groups) if (ieq(g.name, name)) return &g; return nullptr; } bool parse_textscript(const std::string& text, Node& root, std::string* err) { root = Node{}; auto toks = tokenize_all(text); size_t i = 0; return parse_body(toks, i, root, err); } bool parse_textscript_file(const std::string& path, Node& root, std::string* err) { std::string s; if (!read_file(path, s)) { if (err) *err = "cannot open " + path; return false; } return parse_textscript(s, root, err); } } // namespace fmt