From 5a26e93f078a406ddce06a8bfc1fd33233ea3ce4 Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 23 Sep 2026 01:31:17 +0900 Subject: [PATCH] port 2D step 5b: PackInitialize + pack:// asset_io backend over the ported EterPackManager 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 --- audit/history.jsonl | 1 + extension/CMakeLists.txt | 1 + extension/src/asset_io.cpp | 32 ++ extension/src/asset_io.h | 5 + extension/src/pack40250_node.cpp | 35 ++ extension/src/pack40250_node.h | 28 ++ .../src/platform/EterBase/FileLoader.cpp | 303 +++++++++++++----- extension/src/platform/PackBackend.cpp | 60 ++++ extension/src/platform/PackBackend.h | 23 ++ .../platform/UserInterface/UserInterface.cpp | 132 ++++++++ .../platform/UserInterface/UserInterface.h | 11 + extension/src/register_types.cpp | 2 + extension/tests/port_eterpack_test.cpp | 32 +- 13 files changed, 574 insertions(+), 91 deletions(-) create mode 100644 extension/src/pack40250_node.cpp create mode 100644 extension/src/pack40250_node.h create mode 100644 extension/src/platform/PackBackend.cpp create mode 100644 extension/src/platform/PackBackend.h create mode 100644 extension/src/platform/UserInterface/UserInterface.cpp create mode 100644 extension/src/platform/UserInterface/UserInterface.h diff --git a/audit/history.jsonl b/audit/history.jsonl index fc66cdf4..8a93258d 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -489,3 +489,4 @@ {"time":"2026-09-22T15:00:00Z","event":"port_map_rebaseline","scope":"all units","totals":{"logic":3343,"python":5175,"platform":2091,"all":10609,"done":0,"divergent":1},"previous":{"logic":3285,"python":4890,"platform":2229,"all":10404},"changes":["cpp_functions: strip comments, accept indented qualified definitions inside namespace blocks, long parameter/initializer lists (40-line window), all-caps qualified ctors, skip constructor initializer entries","EterBase split by unit: tea/lzo/cipher/Random/Stl/Timer/Poly are logic, CPostIt/CRC32/Debug/FileBase/FileDir/FileLoader/MappedFile/TempFile/Utils/error stay platform; 8 obsolete platform stub files removed","audit/slices regenerated (layer fields; EterLib/Dimm.h now platform)"],"reason":"PORT-PLAN 2A step 6; inventory cross-checked against the clang-derived platform skeleton: every stub is inventoried except 22 declarations 40250 never defines and namespace/typedef spelling variants","tests":["port_map.py check","script/port_gate.sh (macos/android/ios/windows PASS, linux BLOCKED)"]} {"time":"2026-09-22T16:00:00Z","event":"pack_inventory","scope":"Client/pack (batch 2R)","tool":"tools/epk_scan/epk_scan.py","report":"audit/packs/2R-scan.json","totals":{"registered_packs":136,"entries":54891,"unique_paths":52609,"NONE":20727,"COMPRESS":29812,"SECURITY":4352,"PANAMA":0,"HYBRIDCRYPT":0,"decode_failures":0,"overrides":2282,"overrides_different":2194,"case_collisions":0},"findings":["first-registered pack wins (CEterFileDict multimap, VS2022 STL)","short-layout packer in metin2_patch_{sd,mineral,mundi}: 4 SECURITY files rejected by 40250 CRC check","no server-delivered pack keys needed"]} {"time":"2026-09-23T10:00:00Z","event":"port_round","unit":"EterPack/*, EterBase/{tea,lzo,Timer,Stl,Random}.cpp + platform EterBase/{CRC32,FileBase,MappedFile,Debug,Utils}","ported":["EterPack/EterPack.cpp","EterPack/EterPackManager.cpp","EterPack/EterPackCursor.cpp","EterPack/EterPackPolicy_CSHybridCrypt.cpp","EterBase/tea.cpp","EterBase/lzo.cpp","EterBase/Timer.cpp","EterBase/Stl.cpp","EterBase/Random.cpp"],"status_note":"mirror copies + platform FileBase/MappedFile/CRC32/Debug/Utils; not runtime-reachable until the asset_io pack backend (PORT-PLAN §5 step 5), so port-map statuses stay TODO","divergent":[],"needs_live":[],"corrections":["2R registered_packs 136 was wrong: 124 Index pairs -> 119 RegisterPack successes, 103 distinct packs + root (PORT-PLAN, memory)"],"findings":["libc++ unordered_multimap also keeps first-registered-wins: 0 wrong winners over 2282 overrides","exactly the 4 short-layout SECURITY files fail CEterPackManager::Get","pack/Index spells Etc for ETC.eix: CEterPack::CreateIndexFile fopens directly, so the bootstrap must resolve the on-disk dbname"],"keys":"EterPack TEA keys generated at configure time into the build tree (EterPackKeys.generated.h), never committed","tests":["extension/tests/port_eterpack_test.cpp PASS (macOS, 52605 files / 2023459178 bytes)","script/port_gate.sh macos/android/ios/windows PASS"]} +{"time":"2026-09-23T12:00:00Z","event":"port_round","unit":"platform UserInterface PackInitialize + EterBase/FileLoader.cpp + pack:// asset_io backend (PORT-PLAN §5 step 5b)","ported":["EterBase/FileLoader.cpp"],"deleted":[],"divergent":[],"needs_live":[],"status_note":"PackInitialize (platform, on-disk dbname resolution, no LogBoxf) + mtpack40250 backend + Metin2Pack node; no runtime reader uses pack:// until the proto/GDScript switch, so port-map statuses stay TODO","tests":["extension/tests/port_eterpack_test.cpp via PackSingletons+PackInitialize(\"pack\"): 103 packs, oracle order, PASS"],"commit":"5b"} diff --git a/extension/CMakeLists.txt b/extension/CMakeLists.txt index 553c6149..4c351f24 100644 --- a/extension/CMakeLists.txt +++ b/extension/CMakeLists.txt @@ -188,6 +188,7 @@ add_library(mtgodot ${MT_LIB_KIND} src/texture_util.cpp src/net/m2_client.cpp src/proto/proto_node.cpp + src/pack40250_node.cpp ) target_compile_features(mtgodot PRIVATE cxx_std_20) target_link_libraries(mtgodot PRIVATE godot::cpp xrender::libgr2 xrender::formats mtnet mtproto port_platform) diff --git a/extension/src/asset_io.cpp b/extension/src/asset_io.cpp index 43bfa58f..5d831f35 100644 --- a/extension/src/asset_io.cpp +++ b/extension/src/asset_io.cpp @@ -2,19 +2,51 @@ #include +#include "platform/PackBackend.h" + +#include +#include +#include + using namespace godot; namespace mtgodot { +namespace { +bool pack_name(const String &path, std::string &name) { + if (!path.begins_with(PACK_SCHEME)) { + return false; + } + name = path.substr(String(PACK_SCHEME).length()).utf8().get_data(); + return true; +} +} // namespace + PackedByteArray read_file(const String &path) { if (path.is_empty()) { return PackedByteArray(); } + std::string name; + if (pack_name(path, name)) { + std::vector bytes; + PackedByteArray out; + if (mtpack40250::read(name, bytes)) { + out.resize((int64_t)bytes.size()); + if (!bytes.empty()) { + std::memcpy(out.ptrw(), bytes.data(), bytes.size()); + } + } + return out; + } // FileAccess::get_file_as_bytes handles res:// / user:// / absolute OS paths. return FileAccess::get_file_as_bytes(path); } bool file_exists(const String &path) { + std::string name; + if (pack_name(path, name)) { + return mtpack40250::exists(name); + } return !path.is_empty() && FileAccess::file_exists(path); } diff --git a/extension/src/asset_io.h b/extension/src/asset_io.h index 8e7ec706..2d2c3561 100644 --- a/extension/src/asset_io.h +++ b/extension/src/asset_io.h @@ -7,6 +7,9 @@ // - user:// (extracted cache) // - absolute OS paths (dev: AssetRoot points at mtgodot-poc/assets/) // godot::FileAccess handles all three transparently. +// - pack://<40250 path> (e.g. pack://locale/en/item_proto, pack://d:/ymir work/pc/...): the 40250 packs, +// read through the ported CEterPackManager once Metin2Pack.initialize() has run +// (platform/PackBackend.h). The path after pack:// is passed as UTF-8. #pragma once #include @@ -20,6 +23,8 @@ namespace mtgodot { +inline constexpr const char *PACK_SCHEME = "pack://"; + // Whole-file read. Empty PackedByteArray on failure (path missing / unreadable). godot::PackedByteArray read_file(const godot::String &path); bool file_exists(const godot::String &path); // FileAccess::file_exists wrapper diff --git a/extension/src/pack40250_node.cpp b/extension/src/pack40250_node.cpp new file mode 100644 index 00000000..831b3d1d --- /dev/null +++ b/extension/src/pack40250_node.cpp @@ -0,0 +1,35 @@ +#include "pack40250_node.h" + +#include + +#include "asset_io.h" +#include "platform/PackBackend.h" + +using namespace godot; + +namespace mtgodot { + +bool Metin2Pack::initialize(const String &client_dir) { + return mtpack40250::initialize(client_dir.utf8().get_data()); +} + +bool Metin2Pack::is_ready() { + return mtpack40250::ready(); +} + +bool Metin2Pack::exists(const String &name) { + return file_exists(String(PACK_SCHEME) + name); +} + +PackedByteArray Metin2Pack::get_bytes(const String &name) { + return read_file(String(PACK_SCHEME) + name); +} + +void Metin2Pack::_bind_methods() { + ClassDB::bind_static_method("Metin2Pack", D_METHOD("initialize", "client_dir"), &Metin2Pack::initialize); + ClassDB::bind_static_method("Metin2Pack", D_METHOD("is_ready"), &Metin2Pack::is_ready); + ClassDB::bind_static_method("Metin2Pack", D_METHOD("exists", "name"), &Metin2Pack::exists); + ClassDB::bind_static_method("Metin2Pack", D_METHOD("get_bytes", "name"), &Metin2Pack::get_bytes); +} + +} // namespace mtgodot diff --git a/extension/src/pack40250_node.h b/extension/src/pack40250_node.h new file mode 100644 index 00000000..36b85aea --- /dev/null +++ b/extension/src/pack40250_node.h @@ -0,0 +1,28 @@ +#pragma once +// Metin2Pack — GDScript access to the 40250 packs (platform/PackBackend.h). +// +// Metin2Pack.initialize(AssetRoot.client_path()) # registers /pack/Index once +// var b := Metin2Pack.get_bytes("locale/en/item_proto") +// +// Every native reader that takes a path accepts "pack://<40250 path>" through asset_io. + +#include +#include +#include + +namespace mtgodot { + +class Metin2Pack : public godot::Object { + GDCLASS(Metin2Pack, godot::Object) + +public: + static bool initialize(const godot::String &client_dir); + static bool is_ready(); + static bool exists(const godot::String &name); + static godot::PackedByteArray get_bytes(const godot::String &name); + +protected: + static void _bind_methods(); +}; + +} // namespace mtgodot diff --git a/extension/src/platform/EterBase/FileLoader.cpp b/extension/src/platform/EterBase/FileLoader.cpp index 57c1ad3c..c3bb13b9 100644 --- a/extension/src/platform/EterBase/FileLoader.cpp +++ b/extension/src/platform/EterBase/FileLoader.cpp @@ -1,135 +1,290 @@ -// Platform skeleton for EterBase/FileLoader.h (40250 EterBase/FileLoader.cpp), generated by platform_stub.py. -// Every MT_PLATFORM_STUB() body is unimplemented: replace it with the platform implementation. +// Platform implementation of EterBase/FileLoader.h: the 40250 EterBase/FileLoader.cpp, which is portable C/stdio. +// Only change: the CP949 lead-byte test casts to signed char (char is unsigned on Android/Linux arm64). #include "EterBase/StdAfx.h" #include "EterBase/FileLoader.h" - -#include "../PlatformStub.h" +#include CMemoryTextFileLoader::CMemoryTextFileLoader() { - MT_PLATFORM_STUB(); } CMemoryTextFileLoader::~CMemoryTextFileLoader() { - MT_PLATFORM_STUB(); } -auto CMemoryTextFileLoader::Bind(int, const void *) -> void +bool CMemoryTextFileLoader::SplitLineByTab(DWORD dwLine, CTokenVector* pstTokenVector) { - MT_PLATFORM_STUB(); + pstTokenVector->reserve(10); + pstTokenVector->clear(); + + const std::string & c_rstLine = GetLineString(dwLine); + const int c_iLineLength = c_rstLine.length(); + + if (0 == c_iLineLength) + return false; + + int basePos = 0; + + do + { + int beginPos = c_rstLine.find_first_of("\t", basePos); + + pstTokenVector->push_back(c_rstLine.substr(basePos, beginPos-basePos)); + + basePos = beginPos+1; + } while (basePos < c_iLineLength && basePos > 0); + + return true; } -auto CMemoryTextFileLoader::GetLineCount() -> DWORD +int CMemoryTextFileLoader::SplitLine2(DWORD dwLine, CTokenVector* pstTokenVector, const char * c_szDelimeter) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + pstTokenVector->reserve(10); + pstTokenVector->clear(); + + std::string stToken; + const std::string & c_rstLine = GetLineString(dwLine); + + DWORD basePos = 0; + + do + { + int beginPos = c_rstLine.find_first_not_of(c_szDelimeter, basePos); + + if (beginPos < 0) + return -1; + + int endPos; + + if (c_rstLine[beginPos] == '"') + { + ++beginPos; + endPos = c_rstLine.find_first_of("\"", beginPos); + + if (endPos < 0) + return -2; + + basePos = endPos + 1; + } + else + { + endPos = c_rstLine.find_first_of(c_szDelimeter, beginPos); + basePos = endPos; + } + + pstTokenVector->push_back(c_rstLine.substr(beginPos, endPos - beginPos)); + + // 추가 코드. 맨뒤에 탭이 있는 경우를 체크한다. - [levites] + if (int(c_rstLine.find_first_not_of(c_szDelimeter, basePos)) < 0) + break; + } while (basePos < c_rstLine.length()); + + return 0; } -auto CMemoryTextFileLoader::CheckLineIndex(DWORD) -> bool +bool CMemoryTextFileLoader::SplitLine(DWORD dwLine, CTokenVector* pstTokenVector, const char * c_szDelimeter) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + pstTokenVector->reserve(10); + pstTokenVector->clear(); + + std::string stToken; + const std::string & c_rstLine = GetLineString(dwLine); + + DWORD basePos = 0; + + do + { + int beginPos = c_rstLine.find_first_not_of(c_szDelimeter, basePos); + if (beginPos < 0) + return false; + + int endPos; + + if (c_rstLine[beginPos] == '"') + { + ++beginPos; + endPos = c_rstLine.find_first_of("\"", beginPos); + + if (endPos < 0) + return false; + + basePos = endPos + 1; + } + else + { + endPos = c_rstLine.find_first_of(c_szDelimeter, beginPos); + basePos = endPos; + } + + pstTokenVector->push_back(c_rstLine.substr(beginPos, endPos - beginPos)); + + // 추가 코드. 맨뒤에 탭이 있는 경우를 체크한다. - [levites] + if (int(c_rstLine.find_first_not_of(c_szDelimeter, basePos)) < 0) + break; + } while (basePos < c_rstLine.length()); + + return true; } -auto CMemoryTextFileLoader::SplitLine(DWORD, CTokenVector *, const char *) -> bool +DWORD CMemoryTextFileLoader::GetLineCount() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + return m_stLineVector.size(); } -auto CMemoryTextFileLoader::SplitLine2(DWORD, CTokenVector *, const char *) -> int +bool CMemoryTextFileLoader::CheckLineIndex(DWORD dwLine) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if (dwLine >= m_stLineVector.size()) + return false; + + return true; } -auto CMemoryTextFileLoader::SplitLineByTab(DWORD, CTokenVector *) -> bool +const std::string & CMemoryTextFileLoader::GetLineString(DWORD dwLine) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + assert(CheckLineIndex(dwLine)); + return m_stLineVector[dwLine]; } -auto CMemoryTextFileLoader::GetLineString(DWORD) -> const std::string & +void CMemoryTextFileLoader::Bind(int bufSize, const void* c_pvBuf) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + m_stLineVector.reserve(128); + m_stLineVector.clear(); + + const char * c_pcBuf = (const char *)c_pvBuf; + std::string stLine; + int pos = 0; + + while (pos < bufSize) + { + const char c = c_pcBuf[pos++]; + + if ('\n' == c || '\r' == c) + { + if (pos < bufSize) + if ('\n' == c_pcBuf[pos] || '\r' == c_pcBuf[pos]) + ++pos; + + m_stLineVector.push_back(stLine); + stLine = ""; + } + else if (static_cast(c) < 0) // PORT: Win32 char is signed + { + stLine.append(c_pcBuf + (pos-1), 2); + ++pos; + } + else + { + stLine += c; + } + } + + m_stLineVector.push_back(stLine); } -CMemoryFileLoader::CMemoryFileLoader(int, const void *) +////////////////////////////////////////////////////////////////////////////////////////////////// +int CMemoryFileLoader::GetSize() { - MT_PLATFORM_STUB(); + return m_size; +} + +int CMemoryFileLoader::GetPosition() +{ + return m_pos; +} + +bool CMemoryFileLoader::IsReadableSize(int size) +{ + if (m_pos + size > m_size) + return false; + + return true; +} + +bool CMemoryFileLoader::Read(int size, void* pvDst) +{ + if (!IsReadableSize(size)) + return false; + + memcpy(pvDst, GetCurrentPositionPointer(), size); + m_pos += size; + return true; +} + +const char* CMemoryFileLoader::GetCurrentPositionPointer() +{ + assert(m_pcBase != NULL); + return (m_pcBase + m_pos); +} + +CMemoryFileLoader::CMemoryFileLoader(int size, const void* c_pvMemoryFile) +{ + assert(c_pvMemoryFile != NULL); + + m_pos = 0; + m_size = size; + m_pcBase = (const char *) c_pvMemoryFile; } CMemoryFileLoader::~CMemoryFileLoader() { - MT_PLATFORM_STUB(); } -auto CMemoryFileLoader::Read(int, void *) -> bool +////////////////////////////////////////////////////////////////////////////////////////////////// +int CDiskFileLoader::GetSize() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + return m_size; } -auto CMemoryFileLoader::GetPosition() -> int +bool CDiskFileLoader::Read(int size, void* pvDst) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + assert(m_fp != NULL); + + int ret = fread(pvDst, size, 1, m_fp); + + if (ret <= 0) + return false; + + return true; } -auto CMemoryFileLoader::GetSize() -> int +bool CDiskFileLoader::Open(const char* c_szFileName) { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + Close(); + + if (!c_szFileName[0]) + return false; + + m_fp = fopen(c_szFileName, "rb"); + + if (!m_fp) + return false; + + fseek(m_fp, 0, SEEK_END); + m_size = ftell(m_fp); + fseek(m_fp, 0, SEEK_SET); + return true; } -auto CMemoryFileLoader::IsReadableSize(int) -> bool +void CDiskFileLoader::Close() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + if (m_fp) + fclose(m_fp); + + Initialize(); } -auto CMemoryFileLoader::GetCurrentPositionPointer() -> const char * +void CDiskFileLoader::Initialize() { - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); + m_fp = NULL; + m_size = 0; } CDiskFileLoader::CDiskFileLoader() { - MT_PLATFORM_STUB(); + Initialize(); } CDiskFileLoader::~CDiskFileLoader() { - MT_PLATFORM_STUB(); -} - -auto CDiskFileLoader::Close() -> void -{ - MT_PLATFORM_STUB(); -} - -auto CDiskFileLoader::Open(const char *) -> bool -{ - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); -} - -auto CDiskFileLoader::Read(int, void *) -> bool -{ - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); -} - -auto CDiskFileLoader::GetSize() -> int -{ - MT_PLATFORM_STUB(); - return mt_platform_stub_return(); -} - -auto CDiskFileLoader::Initialize() -> void -{ - MT_PLATFORM_STUB(); + Close(); } diff --git a/extension/src/platform/PackBackend.cpp b/extension/src/platform/PackBackend.cpp new file mode 100644 index 00000000..00d22e1a --- /dev/null +++ b/extension/src/platform/PackBackend.cpp @@ -0,0 +1,60 @@ +#include "EterBase/StdAfx.h" +#include "EterBase/MappedFile.h" +#include "EterPack/EterPackManager.h" +#include "UserInterface/UserInterface.h" +#include "PackBackend.h" + +#include + +#if defined(_WIN32) +#include // _access; elsewhere the shim declares it +#endif + +namespace mtpack40250 { + +namespace { +std::mutex g_init_mutex; +bool g_initialized = false; +bool g_ready = false; +} + +bool initialize(const std::string& client_dir) +{ + std::lock_guard lock(g_init_mutex); + if (g_initialized) + return g_ready; + const std::string folder = client_dir + "/pack"; + if (_access((folder + "/Index").c_str(), 0) != 0) + return false; // nothing registered; a later call with the right directory may still succeed + g_initialized = true; + PackSingletons(); + g_ready = PackInitialize(folder.c_str()); + return g_ready; +} + +bool ready() +{ + std::lock_guard lock(g_init_mutex); + return g_ready; +} + +bool read(const std::string& name, std::vector& out) +{ + out.clear(); + if (!ready()) + return false; + CMappedFile file; + LPCVOID data = nullptr; + if (!CEterPackManager::Instance().Get(file, name.c_str(), &data)) + return false; + const BYTE* bytes = static_cast(data); + out.assign(bytes, bytes + file.Size()); + return true; +} + +bool exists(const std::string& name) +{ + return ready() && CEterPackManager::Instance().isExist(name.c_str()); +} + +} // namespace mtpack40250 diff --git a/extension/src/platform/PackBackend.h b/extension/src/platform/PackBackend.h new file mode 100644 index 00000000..b90f3a28 --- /dev/null +++ b/extension/src/platform/PackBackend.h @@ -0,0 +1,23 @@ +#pragma once +// PackBackend — the 40250 pack file system for the extension (docs/PORT-PLAN.md 2R item 6). Registers /pack +// through the platform PackInitialize and reads with the ported CEterPackManager (pack first, then the loose file). +// Names are 40250 paths ("d:/ymir work/pc/warrior/...", "locale/en/item_proto"), normalized by ConvertFileName. +// Standard types only, so Godot-side code (asset_io) includes it without the Win32 shims. + +#include +#include +#include + +namespace mtpack40250 { + +// Registers /pack/Index once per process; false when /pack/Index is missing. +// Later calls return whether the first one succeeded. +bool initialize(const std::string& client_dir); +bool ready(); + +// CEterPackManager::Get into `out`; false when the name is in no pack, is rejected (CRC) and has no loose file. +bool read(const std::string& name, std::vector& out); +// CEterPackManager::isExist. +bool exists(const std::string& name); + +} // namespace mtpack40250 diff --git a/extension/src/platform/UserInterface/UserInterface.cpp b/extension/src/platform/UserInterface/UserInterface.cpp new file mode 100644 index 00000000..c77c58d3 --- /dev/null +++ b/extension/src/platform/UserInterface/UserInterface.cpp @@ -0,0 +1,132 @@ +// Platform implementation of the pack bootstrap from 40250 UserInterface/UserInterface.cpp. PackInitialize is the 40250 +// function with three platform differences: +// - pack names in Index are matched to the on-disk .eix/.epk case-insensitively ("Etc" is ETC.eix on NTFS, and +// CEterPack::CreateIndexFile fopens the name as given), so the registered dbname carries the on-disk spelling; +// - no LogBoxf (a Win32 message box), only the TraceError next to it; +// - CTextFileLoader::SetCacheMode and CSoundData::SetPackMode are not called: EterLib/TextFileLoader and MilesLib +// are not ported yet, and neither affects which files the pack manager registers or returns. +// Built as in the Distribute configuration (_DISTRIBUTE): pack first. +#include "EterBase/StdAfx.h" +#include "EterBase/FileLoader.h" +#include "EterBase/MappedFile.h" +#include "EterBase/Debug.h" +#include "EterBase/lzo.h" +#include "EterPack/EterPackManager.h" +#include "UserInterface.h" + +#include +#include + +#if defined(_WIN32) +#include +#else +#include +#endif + +namespace +{ +// Lowercased file name -> on-disk spelling, for the files directly in `folder`. +std::map list_folder(const std::string& folder) +{ + std::map names; + auto add = [&](const std::string& name) { + std::string lower = name; + for (char& c : lower) + c = (char) tolower((unsigned char) c); + names.emplace(lower, name); + }; +#if defined(_WIN32) + _finddata_t fd; + intptr_t h = _findfirst((folder + "*").c_str(), &fd); + if (h != -1) + { + do + add(fd.name); + while (_findnext(h, &fd) == 0); + _findclose(h); + } +#else + if (DIR* d = opendir(folder.c_str())) + { + while (dirent* e = readdir(d)) + add(e->d_name); + closedir(d); + } +#endif + return names; +} + +// with `name` in the spelling of the on-disk .eix, or unchanged when there is none. +std::string on_disk(const std::map& names, const std::string& folder, const std::string& name) +{ + std::string lower = name + ".eix"; + for (char& c : lower) + c = (char) tolower((unsigned char) c); + auto it = names.find(lower); + if (it == names.end()) + return folder + name; + return folder + it->second.substr(0, it->second.size() - 4); +} +} + +void PackSingletons() +{ + static CLZO lzo; + static CEterPackManager EterPackManager; +} + +// 40250: PackInitialize +bool PackInitialize(const char * c_pszFolder) +{ + if (_access(c_pszFolder, 0) != 0) + return true; + + std::string stFolder(c_pszFolder); + stFolder += "/"; + + std::string stFileName(stFolder); + stFileName += "Index"; + + CMappedFile file; + LPCVOID pvData; + + if (!file.Create(stFileName.c_str(), &pvData, 0, 0)) + { + TraceError("FATAL ERROR! File not exist: %s", stFileName.c_str()); + return true; + } + + CMemoryTextFileLoader TextLoader; + TextLoader.Bind(file.Size(), pvData); + + bool bPackFirst = TRUE; + + const std::string& strPackType = TextLoader.GetLineString(0); + + if (strPackType.compare("FILE") && strPackType.compare("PACK")) + { + TraceError("Pack/Index has invalid syntax. First line must be 'PACK' or 'FILE'"); + return false; + } + + CEterPackManager::Instance().SetCacheMode(); + CEterPackManager::Instance().SetSearchMode(bPackFirst); + + const std::map names = list_folder(stFolder); + + std::string strPackName, strTexCachePackName; + for (DWORD i = 1; i < TextLoader.GetLineCount() - 1; i += 2) + { + const std::string & c_rstFolder = TextLoader.GetLineString(i); + const std::string & c_rstName = TextLoader.GetLineString(i + 1); + + strPackName = on_disk(names, stFolder, c_rstName); + strTexCachePackName = on_disk(names, stFolder, c_rstName + "_texcache"); + + CEterPackManager::Instance().RegisterPack(strPackName.c_str(), c_rstFolder.c_str()); + CEterPackManager::Instance().RegisterPack(strTexCachePackName.c_str(), c_rstFolder.c_str()); + } + + CEterPackManager::Instance().RegisterRootPack(on_disk(names, stFolder, "root").c_str()); + return true; +} diff --git a/extension/src/platform/UserInterface/UserInterface.h b/extension/src/platform/UserInterface/UserInterface.h new file mode 100644 index 00000000..2cc01477 --- /dev/null +++ b/extension/src/platform/UserInterface/UserInterface.h @@ -0,0 +1,11 @@ +#pragma once +// Platform-layer pieces of 40250 UserInterface/UserInterface.cpp (the WinMain unit). 40250 declares these nowhere: +// Main() in the same file calls them. The extension's pack bootstrap calls them instead. + +// 40250: PackInitialize. Registers /Index the way the 40250 client does at startup. The CLZO and +// CEterPackManager singletons it needs must already exist (see PackSingletons). +bool PackInitialize(const char * c_pszFolder); + +// The two function-static singletons 40250 Main() creates before PackInitialize("pack") +// (`static CLZO lzo; static CEterPackManager EterPackManager;`), created on first use and alive until exit. +void PackSingletons(); diff --git a/extension/src/register_types.cpp b/extension/src/register_types.cpp index c18ce49d..5858f37f 100644 --- a/extension/src/register_types.cpp +++ b/extension/src/register_types.cpp @@ -10,6 +10,7 @@ #include "metin2_world.h" #include "m2_material.h" #include "net/m2_client.h" +#include "pack40250_node.h" #include "proto/proto_node.h" #include "static_object.h" #include "terrain_splat.h" @@ -43,6 +44,7 @@ void initialize_mtgodot_module(ModuleInitializationLevel p_level) { GDREGISTER_CLASS(mtgodot::Metin2World); GDREGISTER_CLASS(mtgodot::M2Client); GDREGISTER_CLASS(mtgodot::Metin2Proto); + GDREGISTER_CLASS(mtgodot::Metin2Pack); } void uninitialize_mtgodot_module(ModuleInitializationLevel p_level) { diff --git a/extension/tests/port_eterpack_test.cpp b/extension/tests/port_eterpack_test.cpp index 4d9c8009..525b3fa1 100644 --- a/extension/tests/port_eterpack_test.cpp +++ b/extension/tests/port_eterpack_test.cpp @@ -1,7 +1,7 @@ -// 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 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> // @@ -9,6 +9,7 @@ #include "EterPack/StdAfx.h" #include "EterPack/EterPackManager.h" #include "EterBase/CRC32.h" +#include "../src/platform/UserInterface/UserInterface.h" #include #include @@ -71,27 +72,22 @@ int main(int argc, char** argv) return is_strict ? 1 : 77; } - // PackInitialize("pack"): line 0 is PACK, then (folder, name) pairs; every name also gets a _texcache pack. + 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 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())) + if (access((db + ".eix").c_str(), 0) == 0 && access((db + ".epk").c_str(), 0) == 0) 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); @@ -100,15 +96,17 @@ int main(int argc, char** argv) // The whole dict (one entry per pack index record, overrides included). struct Access : CEterPackManager { + static size_t packs(CEterPackManager& m) { return static_cast(m).m_PackMap.size(); } 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); } }; + CHECK(Access::packs(mgr) == 103); 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); + order.emplace(registered[i], static_cast(i)); // first registration // First registration per path, and CRC32 of the name equal to the stored filename_crc. std::map first;