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
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user