Platform UserInterface PackInitialize/PackSingletons (40250 bootstrap, on-disk dbname resolution), EterBase/FileLoader.cpp mirror, mtpack40250 backend behind asset_io's pack:// scheme and a Metin2Pack node. port_eterpack_test now registers through PackInitialize. Nothing reads pack:// at runtime yet; port-map stays TODO. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
175 lines
6.5 KiB
C++
175 lines
6.5 KiB
C++
// port/EterPack against the real 40250 Client/pack (batch 2D). Registers the packs through the platform
|
|
// PackInitialize("pack") (40250 UserInterface.cpp), after the singletons Main() creates, 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 "../src/platform/UserInterface/UserInterface.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <fstream>
|
|
#include <map>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <dirent.h>
|
|
#include <strings.h>
|
|
#include <unistd.h>
|
|
|
|
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<std::string> read_lines(const std::string& path)
|
|
{
|
|
std::vector<std::string> 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;
|
|
}
|
|
|
|
PackSingletons();
|
|
CHECK(PackInitialize("pack"));
|
|
CEterPackManager& mgr = CEterPackManager::Instance();
|
|
|
|
// Expected registration order, from pack/Index independently of PackInitialize: line 0 is PACK, then
|
|
// (folder, name) pairs; every name also gets a _texcache pack; a pack is registered where it exists on disk.
|
|
const std::vector<std::string> lines = read_lines("pack/Index");
|
|
CHECK(!lines.empty() && lines[0] == "PACK");
|
|
std::vector<std::string> registered;
|
|
for (size_t i = 1; i + 1 < lines.size(); i += 2)
|
|
for (const std::string& name : {lines[i + 1], lines[i + 1] + "_texcache"})
|
|
{
|
|
const std::string db = "pack/" + on_disk_name("pack", name);
|
|
if (access((db + ".eix").c_str(), 0) == 0 && access((db + ".epk").c_str(), 0) == 0)
|
|
registered.push_back(db);
|
|
}
|
|
|
|
// 124 Index pairs; 119 registrations find a pack (some names are listed twice), 103 distinct packs, + root.
|
|
CHECK(registered.size() == 119);
|
|
CHECK(std::set<std::string>(registered.begin(), registered.end()).size() == 103);
|
|
|
|
// The whole dict (one entry per pack index record, overrides included).
|
|
struct Access : CEterPackManager
|
|
{
|
|
static size_t packs(CEterPackManager& m) { return static_cast<Access&>(m).m_PackMap.size(); }
|
|
static const CEterFileDict::TDict& dict(CEterPackManager& m) { return static_cast<Access&>(m).m_FileDict.GetDict(); }
|
|
static CEterFileDict::Item* item(CEterPackManager& m, DWORD h, const char* n) { return static_cast<Access&>(m).m_FileDict.GetItem(h, n); }
|
|
};
|
|
CHECK(Access::packs(mgr) == 103);
|
|
const CEterFileDict::TDict& dict = Access::dict(mgr);
|
|
CHECK(dict.size() == 54891);
|
|
|
|
std::map<std::string, int> order;
|
|
for (size_t i = 0; i < registered.size(); ++i)
|
|
order.emplace(registered[i], static_cast<int>(i)); // first registration
|
|
|
|
// First registration per path, and CRC32 of the name equal to the stored filename_crc.
|
|
std::map<std::string, std::string> 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<std::string, int> 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<std::string> 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<std::string> 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;
|
|
}
|