Files
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

169 lines
6.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.
#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