Files
mtgodot-poc/extension/tests/port_eterpack_test.cpp
T
shenleiandClaude Opus 5 1f1004da79 port 2D step 5a: EterPack/EterPackManager + EterBase file units against the real 40250 packs
- Mirror copies: EterPack (EterPack, EterPackManager, EterPackCursor, CSHybridCrypt policy, Inline.h)
  and EterBase logic units tea/lzo/Timer/Stl/Random, with PORT-tagged width fixes (LONG index
  fields, static_assert(sizeof(TEterPackIndex) == 192), tea_word = Win32 unsigned long).
- TEA pack keys are extracted from the reference EterPack.cpp at configure time into
  build/.../EterPackKeys.generated.h; nothing key-bearing is tracked.
- Platform EterBase: FileBase (stdio, case-insensitive fallback), MappedFile (mmap /
  MapViewOfFile), CRC32, Debug, Utils StringPath/StringLowers.
- Win32Crt: CreateDirectory/DeleteFile/_access/MAKEFOURCC; Win32MinMax.h min/max (force-included
  on MinGW, whose windows.h omits them in C++).
- tests/port_eterpack_test: registers pack/Index like PackInitialize and checks 119
  registrations / 103 packs + root / 54891 entries / 52609 paths / 2282 overrides with
  first-registered-wins, and that exactly the 4 short-layout SECURITY files fail to load.
- 2R count corrected (136 -> 119 registrations, 103 distinct packs + root).

Not runtime-reachable yet (asset_io pack backend is step 5b), so port-map statuses stay TODO.
port_gate: macos/android/ios/windows PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 00:56:39 +09:00

177 lines
6.3 KiB
C++

// 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 <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;
}
// PackInitialize("pack"): line 0 is PACK, then (folder, name) pairs; every name also gets a _texcache pack.
const std::vector<std::string> 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<std::string> 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<std::string>(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<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); }
};
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[registered[i]] = static_cast<int>(i);
// 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;
}