130 lines
5.4 KiB
C++
130 lines
5.4 KiB
C++
// 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
|