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
169 lines
6.4 KiB
C++
169 lines
6.4 KiB
C++
#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
|