#include "asset_resolver.h" #include "m2_tokvec.h" // fmt::read_file(host 可注入 FileAccess) #include #include #include #include namespace fmt { namespace fs = std::filesystem; namespace { std::string lower(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return (char)std::tolower(c); }); return s; } // "a/b/./c/../d" -> "a/b/d"(不越过根;越根的 ".." 直接丢) std::string collapse(const std::string& in) { std::vector parts; std::string cur; auto flush = [&] { if (cur.empty()) return; if (cur == ".") { } else if (cur == "..") { if (!parts.empty()) parts.pop_back(); } else { parts.push_back(cur); } cur.clear(); }; for (char c : in) { if (c == '/') flush(); else cur += c; } flush(); std::string out; for (size_t i = 0; i < parts.size(); ++i) { if (i) out += '/'; out += parts[i]; } return out; } const char* kYmir = "ymir work/"; // 若 path 含 "ymir work/",返回从那里起的后缀,否则 ""。 std::string ymir_suffix(const std::string& norm) { auto p = norm.find(kYmir); if (p == std::string::npos) return ""; return norm.substr(p); } // 一级目录归类:0=基础(在 priority 里),1=普通,2=patch/补丁 int pack_rank(const std::string& top, const std::vector& prio) { std::string t = lower(top); for (size_t i = 0; i < prio.size(); ++i) if (lower(prio[i]) == t) return (int)i; // 0..N-1 if (t.rfind("metin2_patch_", 0) == 0 || t.rfind("patch", 0) == 0) return 100000; return 50000; } } // namespace std::vector AssetResolver::default_priority() { return {"Zone", "Terrain", "ETC", "PC", "Tree", "Property", "Effect", "Monster", "NPC", "season2", "season3_eu"}; } std::string AssetResolver::normalize(const std::string& vp) { std::string s = vp; for (char& c : s) if (c == '\\') c = '/'; s = lower(s); // 去盘符 "d:/..." / "d:..." if (s.size() >= 2 && std::isalpha((unsigned char)s[0]) && s[1] == ':') s = s.substr(2); while (!s.empty() && s[0] == '/') s.erase(s.begin()); return collapse(s); } bool AssetResolver::build(const std::string& root, const std::vector& prio, std::string* err) { assets_root = root; std::error_code ec; if (!fs::exists(root, ec)) { if (err) *err = "assets 根不存在: " + root; return false; } // 收集 (rank, real_path, rel_norm) 后按 rank 稳定排序 —— 保证先注册者 = 高优先级 struct Rec { int rank; std::string real, rel; }; std::vector recs; fs::path base(root); 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 rel_real = fs::relative(it->path(), base, ec).generic_string(); std::string rel = lower(rel_real); if (rel.empty()) continue; std::string top = rel.substr(0, rel.find('/')); // real_path 存**相对** assets_root(保留原始大小写);resolve() 拼回。 recs.push_back({pack_rank(top, prio), rel_real, rel}); } std::stable_sort(recs.begin(), recs.end(), [](const Rec& a, const Rec& b) { return a.rank < b.rank; }); std::vector seen_conflicts; for (auto& r : recs) { ++files_indexed; // by_rel { auto& e = by_rel[r.rel]; if (e.real_path.empty()) e.real_path = r.real; e.all_candidates.push_back(r.real); } // by_ymir std::string suf = ymir_suffix(r.rel); if (!suf.empty()) { auto& e = by_ymir[suf]; if (e.real_path.empty()) { e.real_path = r.real; } else { ++ymir_conflicts; if (seen_conflicts.size() < 50 && std::find(seen_conflicts.begin(), seen_conflicts.end(), suf) == seen_conflicts.end()) { seen_conflicts.push_back(suf); conflict_keys.push_back(suf); } } e.all_candidates.push_back(r.real); } } return true; } std::string AssetResolver::resolve(const std::string& vp, std::string* reason) const { auto join = [this](const std::string& rel) { if (rel.empty()) return std::string(); if (assets_root.empty()) return rel; std::string r = assets_root; if (r.back() != '/') r += '/'; return r + rel; }; std::string norm = normalize(vp); std::string suf = ymir_suffix(norm); if (!suf.empty()) { auto it = by_ymir.find(suf); if (it != by_ymir.end()) return join(it->second.real_path); if (reason) *reason = "未在 ymir 索引找到: " + suf; return ""; } auto it = by_rel.find(norm); if (it != by_rel.end()) return join(it->second.real_path); if (reason) *reason = "未在相对路径索引找到: " + norm; return ""; } // --- 索引烘焙 / 装载 --------------------------------------------------- std::string AssetResolver::save_index() const { // 行式:MTIDX1 头 + 每行 "Y|R\t\t"。只存被采用的 real_path。 std::ostringstream ss; ss << "MTIDX1\n"; for (const auto& kv : by_ymir) { ss << "Y\t" << kv.first << '\t' << kv.second.real_path << '\n'; } for (const auto& kv : by_rel) { ss << "R\t" << kv.first << '\t' << kv.second.real_path << '\n'; } return ss.str(); } bool AssetResolver::load_index(const std::string& root, const std::string& text, std::string* err) { by_ymir.clear(); by_rel.clear(); files_indexed = 0; ymir_conflicts = 0; conflict_keys.clear(); assets_root = root; std::istringstream in(text); std::string line; if (!std::getline(in, line) || line.rfind("MTIDX1", 0) != 0) { if (err) *err = "asset_index:坏头(期望 MTIDX1)"; return false; } while (std::getline(in, line)) { if (line.empty() || line[0] == '#') continue; // tag \t key \t rel size_t t1 = line.find('\t'); size_t t2 = (t1 == std::string::npos) ? std::string::npos : line.find('\t', t1 + 1); if (t2 == std::string::npos) continue; std::string tag = line.substr(0, t1); std::string key = line.substr(t1 + 1, t2 - t1 - 1); std::string rel = line.substr(t2 + 1); if (!rel.empty() && rel.back() == '\r') rel.pop_back(); if (key.empty() || rel.empty()) continue; Entry e; e.real_path = rel; if (tag == "Y") { by_ymir.emplace(std::move(key), std::move(e)); } else if (tag == "R") { by_rel.emplace(std::move(key), std::move(e)); } ++files_indexed; } if (by_ymir.empty() && by_rel.empty()) { if (err) *err = "asset_index:空索引"; return false; } return true; } bool AssetResolver::build_or_load(const std::string& root, const std::vector& prio, std::string* err) { std::string text; if (read_file(root + "/asset_index.txt", text) && text.rfind("MTIDX1", 0) == 0) { return load_index(root, text, err); } return build(root, prio, err); } bool AssetResolver::exists(const std::string& vp) const { return !resolve(vp, nullptr).empty(); } std::vector AssetResolver::all_rel() const { std::vector out; out.reserve(by_rel.size()); for (const auto& kv : by_rel) out.push_back(kv.second.real_path); return out; } } // namespace fmt