Files
mtgodot-poc/formats/textscript.cpp
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

127 lines
5.1 KiB
C++
Raw 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; }
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<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