Files

130 lines
5.4 KiB
C++
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.
// M2 T1 —— Metin2 文本脚本 token 树解析。
#include "textscript.h"
#include "m2_tokvec.h" // fmt::read_file (host-injectable; res:// on mobile)
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
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<std::string> tokenize_all(const std::string& text) {
std::vector<std::string> 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<std::string>& t, size_t& i, Node& node, std::string* err) {
while (i < t.size()) {
const std::string& tok = t[i];
if (tok == "}") { ++i; return true; }
// `Group Name { ... }` 和 `List Name { ... }`CTextFileLoader 的 GetTokenVector
// 都当子节点:Group 体是 key/子组,List 体是一串裸值(收进子节点的单行)。
if (ieq(tok, "Group") || ieq(tok, "List")) {
const char* kind = ieq(tok, "Group") ? "Group" : "List";
if (i + 2 >= t.size()) { if (err) *err = std::string(kind) + " 缺名字 / {"; return false; }
Node sub;
sub.name = t[i + 1];
if (t[i + 2] != "{") { if (err) *err = std::string(kind) + " '" + 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<std::string> 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<std::string>* 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