// port/EterPack against the real 40250 Client/pack (batch 2D). Registers every pack in pack/Index order the way // UserInterface.cpp PackInitialize does, then checks the numbers batch 2R measured with tools/epk_scan // (audit/packs/2R-scan.json): 103 packs + root / 54891 entries / 52609 paths, first-registered pack wins, and exactly // the 4 short-layout SECURITY files 40250 rejects fail to load. // // port_eterpack_test <40250 Client dir> // // Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which turns that into a failure. #include "EterPack/StdAfx.h" #include "EterPack/EterPackManager.h" #include "EterBase/CRC32.h" #include #include #include #include #include #include #include #include #include #include static int g_failures = 0; #define CHECK(cond) \ do { \ if (!(cond)) { \ std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \ ++g_failures; \ } \ } while (0) // Index names are spelled as on the NTFS original ("Etc" for ETC.eix); find the on-disk spelling. static std::string on_disk_name(const std::string& dir, const std::string& name) { std::string found = name; if (DIR* d = opendir(dir.c_str())) { const std::string want = name + ".eix"; while (dirent* e = readdir(d)) if (strcasecmp(e->d_name, want.c_str()) == 0) found = std::string(e->d_name, want.size() - 4); closedir(d); } return found; } static std::vector read_lines(const std::string& path) { std::vector lines; std::ifstream in(path, std::ios::binary); std::string line; while (std::getline(in, line)) { while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) line.pop_back(); lines.push_back(line); } return lines; } int main(int argc, char** argv) { const char* strict = std::getenv("MT_ASSETS_STRICT"); const bool is_strict = strict && std::string(strict) == "1"; const std::string client = argc > 1 ? argv[1] : ""; if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0) { std::fprintf(stderr, "port_eterpack_test: no 40250 Client/pack at '%s'\n", client.c_str()); return is_strict ? 1 : 77; } // PackInitialize("pack"): line 0 is PACK, then (folder, name) pairs; every name also gets a _texcache pack. const std::vector lines = read_lines("pack/Index"); CHECK(!lines.empty() && lines[0] == "PACK"); CEterPackManager manager; // CSingleton: the instance registers itself CEterPackManager& mgr = CEterPackManager::Instance(); mgr.SetCacheMode(); mgr.SetSearchMode(true); std::vector registered; for (size_t i = 1; i + 1 < lines.size(); i += 2) { const std::string& folder = lines[i]; for (const std::string& name : {lines[i + 1], lines[i + 1] + "_texcache"}) { const std::string db = "pack/" + on_disk_name("pack", name); if (mgr.RegisterPack(db.c_str(), folder.c_str())) registered.push_back(db); } } mgr.RegisterRootPack("pack/root"); // 124 Index pairs; 119 registrations find a pack (some names are listed twice), 103 distinct packs, + root. CHECK(registered.size() == 119); CHECK(std::set(registered.begin(), registered.end()).size() == 103); // The whole dict (one entry per pack index record, overrides included). struct Access : CEterPackManager { static const CEterFileDict::TDict& dict(CEterPackManager& m) { return static_cast(m).m_FileDict.GetDict(); } static CEterFileDict::Item* item(CEterPackManager& m, DWORD h, const char* n) { return static_cast(m).m_FileDict.GetItem(h, n); } }; const CEterFileDict::TDict& dict = Access::dict(mgr); CHECK(dict.size() == 54891); std::map order; for (size_t i = 0; i < registered.size(); ++i) order[registered[i]] = static_cast(i); // First registration per path, and CRC32 of the name equal to the stored filename_crc. std::map first; for (const auto& kv : dict) { const std::string name = kv.second.pkInfo->filename; const std::string pack = kv.second.pkPack->GetDBName(); CHECK(GetCRC32(name.c_str(), name.size()) == kv.first); auto it = first.find(name); if (it == first.end() || order[pack] < order[it->second]) first[name] = pack; } CHECK(first.size() == 52609); size_t overridden = 0, wrong_winner = 0; std::map per_name; for (const auto& kv : dict) ++per_name[kv.second.pkInfo->filename]; for (const auto& [name, count] : per_name) { if (count < 2) continue; overridden += count - 1; CEterFileDict::Item* item = Access::item(mgr, GetCRC32(name.c_str(), name.size()), name.c_str()); if (!item || first[name] != item->pkPack->GetDBName()) { if (wrong_winner < 5) std::fprintf(stderr, "winner %s: %s, expected %s\n", name.c_str(), item ? item->pkPack->GetDBName() : "(none)", first[name].c_str()); ++wrong_winner; } } CHECK(overridden == 2282); CHECK(wrong_winner == 0); // Every path loads through CEterPackManager::Get, except the 4 short-layout SECURITY files. const std::set rejected_expected = { "d:/ymir work/npc/mineral2/wait.msa", "d:/ymir work/effect/background/metinstone_loop_redblack.mse", "d:/ymir work/monster2/spider_spawn/wait.msa", "d:/ymir work/monster2/spider_spawn/line1.mse", }; std::set rejected; unsigned long long bytes = 0; for (const auto& [name, pack] : first) { CMappedFile file; LPCVOID data = nullptr; if (mgr.Get(file, name.c_str(), &data)) bytes += file.Size(); else rejected.insert(name); } for (const std::string& name : rejected) if (!rejected_expected.count(name)) std::fprintf(stderr, "unexpected load failure: %s\n", name.c_str()); CHECK(rejected == rejected_expected); std::printf("packs %zu, entries %zu, paths %zu, overrides %zu, loaded %zu (%llu bytes), rejected %zu\n", registered.size(), dict.size(), first.size(), overridden, first.size() - rejected.size(), bytes, rejected.size()); if (g_failures) std::fprintf(stderr, "%d failure(s)\n", g_failures); return g_failures ? 1 : 0; }