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:
shen
2026-08-31 20:02:12 +09:00
co-authored by Claude Sonnet 5
parent f4917a2b3b
commit 47baf6c0c6
414 changed files with 69568 additions and 385 deletions
+168
View File
@@ -0,0 +1,168 @@
#include "property.h"
#include "m2_tokvec.h" // read_file
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <sstream>
namespace fmt {
namespace fs = std::filesystem;
std::string Property::get(const std::string& key, const std::string& def) const {
auto it = kv.find(key);
return it == kv.end() ? def : it->second;
}
PropertyType property_type_from_string(const std::string& s) {
std::string l = s;
std::transform(l.begin(), l.end(), l.begin(), [](unsigned char c) { return (char)std::tolower(c); });
if (l == "tree") return PropertyType::Tree;
if (l == "building") return PropertyType::Building;
if (l == "effect") return PropertyType::Effect;
if (l == "ambience") return PropertyType::Ambience;
if (l == "dungeonblock") return PropertyType::DungeonBlock;
return PropertyType::Unknown;
}
bool parse_property(const std::string& text, Property& out, std::string* err) {
out = Property{};
std::istringstream in(text);
std::string line;
// 行1: YPRT
if (!std::getline(in, line)) { if (err) *err = "空文件"; return false; }
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
line.pop_back();
{
std::string t;
for (char c : line) if (!std::isspace((unsigned char)c)) t += c;
if (t != "YPRT") { if (err) *err = "首行不是 YPRT: '" + t + "'"; return false; }
}
// 行2: crc
if (!std::getline(in, line)) { if (err) *err = "缺 CRC 行"; return false; }
out.crc = (uint32_t)std::strtoul(line.c_str(), nullptr, 10);
if (out.crc == 0) { if (err) *err = "CRC 行解析为 0: '" + line + "'"; return false; }
// 行3+: key\t"value"
while (std::getline(in, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
// key = 第一段非空白
size_t i = 0, n = line.size();
while (i < n && std::isspace((unsigned char)line[i])) ++i;
if (i >= n) continue;
size_t ks = i;
while (i < n && !std::isspace((unsigned char)line[i])) ++i;
std::string key = line.substr(ks, i - ks);
std::transform(key.begin(), key.end(), key.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
// value = 引号内,或剩余去空白
while (i < n && std::isspace((unsigned char)line[i])) ++i;
std::string val;
if (i < n && line[i] == '"') {
++i;
while (i < n && line[i] != '"') val += line[i++];
} else {
val = line.substr(i);
while (!val.empty() && (val.back() == '\r' || std::isspace((unsigned char)val.back())))
val.pop_back();
}
out.kv[key] = val;
}
out.name = out.get("propertyname");
out.type_str = out.get("propertytype");
out.type = property_type_from_string(out.type_str);
return true;
}
bool parse_property_file(const std::string& path, Property& out, std::string* err) {
std::string text;
if (!read_file(path, text)) { if (err) *err = "打不开 " + path; return false; }
if (!parse_property(text, out, err)) return false;
out.source_path = path;
return true;
}
const Property* PropertyRegistry::find(uint32_t crc) const {
auto it = by_crc.find(crc);
return it == by_crc.end() ? nullptr : &it->second;
}
bool PropertyRegistry::scan(const std::string& root, std::string* err) {
std::error_code ec;
if (!fs::exists(root, ec)) { if (err) *err = "目录不存在: " + root; return false; }
static const char* kExts[] = {".prb", ".prt", ".pre", ".prd", ".pra"};
for (auto it = fs::recursive_directory_iterator(
root, fs::directory_options::skip_permission_denied, ec);
it != fs::recursive_directory_iterator(); it.increment(ec)) {
if (ec) { ec.clear(); continue; }
if (!it->is_regular_file(ec)) continue;
std::string ext = it->path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
if (std::find(std::begin(kExts), std::end(kExts), ext) == std::end(kExts)) continue;
++files_scanned;
Property p;
std::string e2;
if (!parse_property_file(it->path().string(), p, &e2)) {
++parse_failed;
failed_paths.push_back(it->path().string() + " : " + e2);
continue;
}
type_counts[p.type_str.empty() ? "<none>" : p.type_str]++;
auto [ins, ok] = by_crc.emplace(p.crc, p);
if (!ok) ++crc_collisions;
}
return true;
}
bool PropertyRegistry::scan_list(const std::string& root,
const std::vector<std::string>& rel_paths, std::string* err) {
static const char* kExts[] = {".prb", ".prt", ".pre", ".prd", ".pra"};
std::string r = root;
if (!r.empty() && r.back() == '/') r.pop_back();
for (const std::string& rel : rel_paths) {
// 只认顶层 Property/ 下的(对齐 scan(root + "/Property"),不吃 patch 覆盖)
if (rel.size() < 9) continue;
{
std::string top = rel.substr(0, 8);
std::transform(top.begin(), top.end(), top.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
if (top != "property" || (rel[8] != '/' && rel[8] != '\\')) continue;
}
std::string ext;
size_t dot = rel.find_last_of('.');
if (dot != std::string::npos) {
ext = rel.substr(dot);
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
}
if (std::find(std::begin(kExts), std::end(kExts), ext) == std::end(kExts)) continue;
++files_scanned;
Property p;
std::string e2;
std::string full = r + "/" + rel;
if (!parse_property_file(full, p, &e2)) { // read_file -> host FileAccess -> res:// ok
++parse_failed;
failed_paths.push_back(full + " : " + e2);
continue;
}
type_counts[p.type_str.empty() ? "<none>" : p.type_str]++;
auto [ins, ok] = by_crc.emplace(p.crc, p);
if (!ok) ++crc_collisions;
}
if (files_scanned == 0) {
if (err) *err = "清单里没有属性文件(.pr*";
return false;
}
return true;
}
} // namespace fmt