diff --git a/src/PackLib/M2Pack.cpp b/src/PackLib/M2Pack.cpp new file mode 100644 index 0000000..6fdf39d --- /dev/null +++ b/src/PackLib/M2Pack.cpp @@ -0,0 +1,275 @@ +#include "M2Pack.h" +#include "M2PackRuntimeKeyProvider.h" + +#include + +#include + +#include "EterBase/Debug.h" +#include "EterLib/BufferPool.h" + +namespace +{ + +thread_local ZSTD_DCtx* g_m2pack_zstd_dctx = nullptr; + +ZSTD_DCtx* GetThreadLocalZstdContext() +{ + if (!g_m2pack_zstd_dctx) + { + g_m2pack_zstd_dctx = ZSTD_createDCtx(); + } + + return g_m2pack_zstd_dctx; +} + +template +bool ReadPod(const uint8_t* bytes, std::size_t size, std::size_t& offset, T& out) +{ + if (offset + sizeof(T) > size) + { + return false; + } + + memcpy(&out, bytes + offset, sizeof(T)); + offset += sizeof(T); + return true; +} + +} + +bool CM2Pack::Load(const std::string& path) +{ + if (!HasM2PackRuntimeKeysForArchiveLoad()) + { + TraceError("CM2Pack::Load: runtime master key required for '%s'", path.c_str()); + return false; + } + + std::error_code ec; + m_file.map(path, ec); + + if (ec) + { + TraceError("CM2Pack::Load: map failed for '%s': %s", path.c_str(), ec.message().c_str()); + return false; + } + + if (m_file.size() < sizeof(TM2PackHeader)) + { + TraceError("CM2Pack::Load: file too small '%s'", path.c_str()); + return false; + } + + memcpy(&m_header, m_file.data(), sizeof(TM2PackHeader)); + static constexpr char kMagic[M2PACK_MAGIC_SIZE] = {'M', '2', 'P', 'A', 'C', 'K', '2', '\0'}; + if (memcmp(m_header.magic, kMagic, sizeof(kMagic)) != 0) + { + TraceError("CM2Pack::Load: invalid magic in '%s'", path.c_str()); + return false; + } + + if (m_header.version != 1) + { + TraceError("CM2Pack::Load: unsupported version %u in '%s'", m_header.version, path.c_str()); + return false; + } + + if (m_header.manifest_offset + m_header.manifest_size > m_file.size()) + { + TraceError("CM2Pack::Load: manifest out of bounds in '%s'", path.c_str()); + return false; + } + + m_manifest_bytes.assign( + m_file.data() + m_header.manifest_offset, + m_file.data() + m_header.manifest_offset + m_header.manifest_size); + + if (!ValidateManifest()) + { + TraceError("CM2Pack::Load: manifest validation failed for '%s'", path.c_str()); + return false; + } + + return true; +} + +bool CM2Pack::ValidateManifest() +{ + std::array manifest_hash {}; + crypto_generichash( + manifest_hash.data(), + manifest_hash.size(), + m_manifest_bytes.data(), + m_manifest_bytes.size(), + nullptr, + 0); + + if (memcmp(manifest_hash.data(), m_header.manifest_hash, manifest_hash.size()) != 0) + { + TraceError("CM2Pack::ValidateManifest: manifest hash mismatch"); + return false; + } + + if (crypto_sign_verify_detached( + m_header.manifest_signature, + m_manifest_bytes.data(), + m_manifest_bytes.size(), + GetM2PackActivePublicKey().data()) != 0) + { + TraceError("CM2Pack::ValidateManifest: manifest signature mismatch"); + return false; + } + + std::size_t offset = 0; + TM2PackManifestHeader manifest_header {}; + if (!ReadPod(m_manifest_bytes.data(), m_manifest_bytes.size(), offset, manifest_header)) + { + return false; + } + + m_index.clear(); + m_index.reserve(manifest_header.entry_count); + + for (uint32_t i = 0; i < manifest_header.entry_count; ++i) + { + TM2PackManifestEntryFixed fixed {}; + if (!ReadPod(m_manifest_bytes.data(), m_manifest_bytes.size(), offset, fixed)) + { + return false; + } + + if (offset + fixed.path_size > m_manifest_bytes.size()) + { + return false; + } + + TM2PackEntry entry; + entry.path.assign(reinterpret_cast(m_manifest_bytes.data() + offset), fixed.path_size); + offset += fixed.path_size; + entry.compression = fixed.compression; + entry.data_offset = fixed.data_offset; + entry.original_size = fixed.original_size; + entry.stored_size = fixed.stored_size; + memcpy(entry.nonce.data(), fixed.nonce, entry.nonce.size()); + memcpy(entry.plaintext_hash.data(), fixed.plaintext_hash, entry.plaintext_hash.size()); + + const uint64_t payload_begin = sizeof(TM2PackHeader); + const uint64_t payload_end = m_header.manifest_offset; + const uint64_t begin = payload_begin + entry.data_offset; + const uint64_t end = begin + entry.stored_size; + + if (entry.path.empty() || entry.path.find("..") != std::string::npos || entry.path[0] == '/') + { + TraceError("CM2Pack::ValidateManifest: invalid path '%s'", entry.path.c_str()); + return false; + } + + if (begin > payload_end || end > payload_end || end < begin) + { + TraceError("CM2Pack::ValidateManifest: invalid entry bounds '%s'", entry.path.c_str()); + return false; + } + + m_index.push_back(std::move(entry)); + } + + return true; +} + +bool CM2Pack::DecryptEntryPayload(const TM2PackEntry& entry, std::vector& decrypted, CBufferPool* pPool) +{ + const uint64_t begin = sizeof(TM2PackHeader) + entry.data_offset; + const auto* ciphertext = reinterpret_cast(m_file.data() + begin); + + std::vector ciphertext_copy; + if (pPool) + { + ciphertext_copy = pPool->Acquire(entry.stored_size); + } + ciphertext_copy.resize(entry.stored_size); + memcpy(ciphertext_copy.data(), ciphertext, entry.stored_size); + + decrypted.resize(entry.stored_size); + unsigned long long written = 0; + if (crypto_aead_xchacha20poly1305_ietf_decrypt( + decrypted.data(), + &written, + nullptr, + ciphertext_copy.data(), + ciphertext_copy.size(), + reinterpret_cast(entry.path.data()), + entry.path.size(), + entry.nonce.data(), + GetM2PackActiveMasterKey().data()) != 0) + { + if (pPool) + { + pPool->Release(std::move(ciphertext_copy)); + } + return false; + } + + decrypted.resize(static_cast(written)); + if (pPool) + { + pPool->Release(std::move(ciphertext_copy)); + } + return true; +} + +bool CM2Pack::GetFile(const TM2PackEntry& entry, std::vector& result) +{ + return GetFileWithPool(entry, result, nullptr); +} + +bool CM2Pack::GetFileWithPool(const TM2PackEntry& entry, std::vector& result, CBufferPool* pPool) +{ + std::vector compressed; + if (!DecryptEntryPayload(entry, compressed, pPool)) + { + TraceError("CM2Pack::GetFileWithPool: decrypt failed for '%s'", entry.path.c_str()); + return false; + } + + switch (entry.compression) + { + case 0: + result = std::move(compressed); + break; + + case 1: + { + result.resize(entry.original_size); + ZSTD_DCtx* dctx = GetThreadLocalZstdContext(); + size_t written = ZSTD_decompressDCtx(dctx, result.data(), result.size(), compressed.data(), compressed.size()); + if (ZSTD_isError(written) || written != entry.original_size) + { + TraceError("CM2Pack::GetFileWithPool: zstd failed for '%s'", entry.path.c_str()); + return false; + } + break; + } + + default: + TraceError("CM2Pack::GetFileWithPool: unsupported compression %u for '%s'", entry.compression, entry.path.c_str()); + return false; + } + + std::array plain_hash {}; + crypto_generichash( + plain_hash.data(), + plain_hash.size(), + result.data(), + result.size(), + nullptr, + 0); + + if (memcmp(plain_hash.data(), entry.plaintext_hash.data(), plain_hash.size()) != 0) + { + TraceError("CM2Pack::GetFileWithPool: plaintext hash mismatch for '%s'", entry.path.c_str()); + return false; + } + + return true; +} diff --git a/src/PackLib/M2Pack.h b/src/PackLib/M2Pack.h new file mode 100644 index 0000000..51230cb --- /dev/null +++ b/src/PackLib/M2Pack.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +class CBufferPool; + +constexpr std::size_t M2PACK_MAGIC_SIZE = 8; +constexpr std::size_t M2PACK_HASH_SIZE = 32; +constexpr std::size_t M2PACK_SIGNATURE_SIZE = 64; +constexpr std::size_t M2PACK_KEY_SIZE = crypto_aead_xchacha20poly1305_ietf_KEYBYTES; +constexpr std::size_t M2PACK_NONCE_SIZE = crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; +constexpr std::size_t M2PACK_PUBLIC_KEY_SIZE = crypto_sign_PUBLICKEYBYTES; + +#pragma pack(push, 1) +struct TM2PackHeader +{ + char magic[M2PACK_MAGIC_SIZE]; + uint32_t version; + uint32_t flags; + uint64_t manifest_offset; + uint64_t manifest_size; + uint8_t manifest_hash[M2PACK_HASH_SIZE]; + uint8_t manifest_signature[M2PACK_SIGNATURE_SIZE]; + uint8_t reserved[64]; +}; + +struct TM2PackManifestHeader +{ + uint32_t entry_count; + uint32_t flags; +}; + +struct TM2PackManifestEntryFixed +{ + uint16_t path_size; + uint8_t compression; + uint8_t flags; + uint64_t data_offset; + uint64_t original_size; + uint64_t stored_size; + uint8_t nonce[M2PACK_NONCE_SIZE]; + uint8_t plaintext_hash[M2PACK_HASH_SIZE]; +}; +#pragma pack(pop) + +struct TM2PackEntry +{ + std::string path; + uint8_t compression = 0; + uint64_t data_offset = 0; + uint64_t original_size = 0; + uint64_t stored_size = 0; + std::array nonce {}; + std::array plaintext_hash {}; +}; + +class CM2Pack : public std::enable_shared_from_this +{ +public: + CM2Pack() = default; + ~CM2Pack() = default; + + bool Load(const std::string& path); + const std::vector& GetIndex() const { return m_index; } + + bool GetFile(const TM2PackEntry& entry, std::vector& result); + bool GetFileWithPool(const TM2PackEntry& entry, std::vector& result, CBufferPool* pPool); + +private: + bool ValidateManifest(); + bool DecryptEntryPayload(const TM2PackEntry& entry, std::vector& decrypted, CBufferPool* pPool); + +private: + TM2PackHeader m_header {}; + std::vector m_manifest_bytes; + std::vector m_index; + mio::mmap_source m_file; +}; + +using TM2PackFileMapEntry = std::pair, TM2PackEntry>; +using TM2PackFileMap = std::unordered_map; + +#include "M2PackKeys.h" diff --git a/src/PackLib/M2PackKeys.h b/src/PackLib/M2PackKeys.h new file mode 100644 index 0000000..0ee6804 --- /dev/null +++ b/src/PackLib/M2PackKeys.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +// Generated by m2pack export-client-config. +// Do not edit manually. +// Runtime master key delivery is required for .m2p loading. + +constexpr bool M2PACK_RUNTIME_MASTER_KEY_REQUIRED = true; + +constexpr std::array M2PACK_MASTER_KEY = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +constexpr std::array M2PACK_SIGN_PUBLIC_KEY = { + 0x22, 0x69, 0x26, 0xd0, 0xa9, 0xa5, 0x53, 0x4c, + 0x95, 0x45, 0xec, 0xba, 0xe9, 0x32, 0x46, 0xc9, + 0x43, 0x80, 0x5c, 0x1a, 0x2c, 0x57, 0xc0, 0x03, + 0xd9, 0x72, 0x41, 0x19, 0xea, 0x0b, 0xc6, 0xa4 +}; diff --git a/src/PackLib/M2PackRuntimeKeyProvider.cpp b/src/PackLib/M2PackRuntimeKeyProvider.cpp new file mode 100644 index 0000000..a8d32ae --- /dev/null +++ b/src/PackLib/M2PackRuntimeKeyProvider.cpp @@ -0,0 +1,249 @@ +#include "M2PackRuntimeKeyProvider.h" + +#include +#include +#include + +#include + +#include "EterBase/Debug.h" +#include "EterBase/Utils.h" + +namespace +{ + +constexpr char M2PACK_SHARED_KEY_MAGIC[8] = {'M', '2', 'K', 'E', 'Y', 'S', '1', '\0'}; +constexpr const char* M2PACK_DEFAULT_MAP_NAME = "Local\\M2PackSharedKeys"; +constexpr const char* M2PACK_ENV_MASTER_KEY = "M2PACK_MASTER_KEY_HEX"; +constexpr const char* M2PACK_ENV_PUBLIC_KEY = "M2PACK_SIGN_PUBKEY_HEX"; +constexpr const char* M2PACK_ENV_MAP_NAME = "M2PACK_KEY_MAP"; + +#pragma pack(push, 1) +struct M2PackSharedKeys +{ + char magic[8]; + uint32_t version; + uint32_t flags; + uint8_t master_key[M2PACK_KEY_SIZE]; + uint8_t sign_public_key[M2PACK_PUBLIC_KEY_SIZE]; +}; +#pragma pack(pop) + +struct RuntimeKeyState +{ + std::array master_key {}; + std::array public_key = M2PACK_SIGN_PUBLIC_KEY; + bool runtime_master_key = false; + bool runtime_public_key = false; + bool initialized = false; +}; + +RuntimeKeyState g_state; + +uint8_t HexNibble(char ch) +{ + if (ch >= '0' && ch <= '9') + return static_cast(ch - '0'); + if (ch >= 'a' && ch <= 'f') + return static_cast(10 + ch - 'a'); + if (ch >= 'A' && ch <= 'F') + return static_cast(10 + ch - 'A'); + return 0xff; +} + +template +bool ParseHexInto(std::string value, std::array& out) +{ + value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) { + return std::isspace(ch) != 0; + }), value.end()); + + if (value.size() != N * 2) + return false; + + for (std::size_t i = 0; i < N; ++i) + { + const uint8_t hi = HexNibble(value[i * 2]); + const uint8_t lo = HexNibble(value[i * 2 + 1]); + if (hi == 0xff || lo == 0xff) + return false; + out[i] = static_cast((hi << 4) | lo); + } + + return true; +} + +std::string GetEnvString(const char* name) +{ + char buffer[512]; + const DWORD len = GetEnvironmentVariableA(name, buffer, sizeof(buffer)); + if (len == 0 || len >= sizeof(buffer)) + return {}; + return std::string(buffer, buffer + len); +} + +bool LoadFromSharedMapping(const std::string& mappingName) +{ + const HANDLE mapping = OpenFileMappingA(FILE_MAP_READ, FALSE, mappingName.c_str()); + if (!mapping) + return false; + + void* view = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, sizeof(M2PackSharedKeys)); + if (!view) + { + CloseHandle(mapping); + return false; + } + + M2PackSharedKeys blob {}; + memcpy(&blob, view, sizeof(blob)); + + UnmapViewOfFile(view); + CloseHandle(mapping); + + if (memcmp(blob.magic, M2PACK_SHARED_KEY_MAGIC, sizeof(blob.magic)) != 0 || blob.version != 1) + { + TraceError("M2Pack key mapping exists but has invalid header"); + return false; + } + + memcpy(g_state.master_key.data(), blob.master_key, g_state.master_key.size()); + memcpy(g_state.public_key.data(), blob.sign_public_key, g_state.public_key.size()); + g_state.runtime_master_key = true; + g_state.runtime_public_key = true; + return true; +} + +void ApplyCommandLineOption(const std::string& key, const std::string& value) +{ + if (key == "--m2pack-key-hex") + { + if (ParseHexInto(value, g_state.master_key)) + { + g_state.runtime_master_key = true; + } + else + { + TraceError("Invalid value for --m2pack-key-hex"); + } + return; + } + + if (key == "--m2pack-pubkey-hex") + { + if (ParseHexInto(value, g_state.public_key)) + { + g_state.runtime_public_key = true; + } + else + { + TraceError("Invalid value for --m2pack-pubkey-hex"); + } + return; + } + + if (key == "--m2pack-key-map") + { + LoadFromSharedMapping(value); + return; + } +} + +} // namespace + +bool InitializeM2PackRuntimeKeyProvider(const char* commandLine) +{ + if (g_state.initialized) + return true; + + g_state = RuntimeKeyState {}; + + const std::string mapName = GetEnvString(M2PACK_ENV_MAP_NAME); + if (!mapName.empty()) + { + LoadFromSharedMapping(mapName); + } + else + { + LoadFromSharedMapping(M2PACK_DEFAULT_MAP_NAME); + } + + const std::string envMaster = GetEnvString(M2PACK_ENV_MASTER_KEY); + if (!envMaster.empty()) + { + if (ParseHexInto(envMaster, g_state.master_key)) + g_state.runtime_master_key = true; + else + TraceError("Invalid M2PACK_MASTER_KEY_HEX value"); + } + + const std::string envPublic = GetEnvString(M2PACK_ENV_PUBLIC_KEY); + if (!envPublic.empty()) + { + if (ParseHexInto(envPublic, g_state.public_key)) + g_state.runtime_public_key = true; + else + TraceError("Invalid M2PACK_SIGN_PUBKEY_HEX value"); + } + + int argc = 0; + PCHAR* argv = CommandLineToArgv(const_cast(commandLine ? commandLine : ""), &argc); + if (argv) + { + for (int i = 0; i < argc; ++i) + { + const std::string key = argv[i]; + if ((key == "--m2pack-key-hex" || key == "--m2pack-pubkey-hex" || key == "--m2pack-key-map") && i + 1 < argc) + { + ApplyCommandLineOption(key, argv[i + 1]); + ++i; + } + } + + SAFE_FREE_GLOBAL(argv); + } + + if (g_state.runtime_master_key || g_state.runtime_public_key) + { + Tracef("M2Pack runtime key provider: override active (master=%d public=%d)\n", + g_state.runtime_master_key ? 1 : 0, + g_state.runtime_public_key ? 1 : 0); + } + else + { + Tracef("M2Pack runtime key provider: no runtime master key available; .m2p loading will be denied\n"); + } + + g_state.initialized = true; + return true; +} + +const std::array& GetM2PackActiveMasterKey() +{ + return g_state.master_key; +} + +const std::array& GetM2PackActivePublicKey() +{ + return g_state.public_key; +} + +bool HasM2PackRuntimeMasterKey() +{ + return g_state.runtime_master_key; +} + +bool HasM2PackRuntimeKeysForArchiveLoad() +{ + return g_state.runtime_master_key; +} + +bool IsM2PackUsingRuntimeMasterKey() +{ + return g_state.runtime_master_key; +} + +bool IsM2PackUsingRuntimePublicKey() +{ + return g_state.runtime_public_key; +} diff --git a/src/PackLib/M2PackRuntimeKeyProvider.h b/src/PackLib/M2PackRuntimeKeyProvider.h new file mode 100644 index 0000000..8f30880 --- /dev/null +++ b/src/PackLib/M2PackRuntimeKeyProvider.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#include "M2Pack.h" + +bool InitializeM2PackRuntimeKeyProvider(const char* commandLine); +const std::array& GetM2PackActiveMasterKey(); +const std::array& GetM2PackActivePublicKey(); +bool HasM2PackRuntimeMasterKey(); +bool HasM2PackRuntimeKeysForArchiveLoad(); +bool IsM2PackUsingRuntimeMasterKey(); +bool IsM2PackUsingRuntimePublicKey(); diff --git a/src/PackLib/PackManager.cpp b/src/PackLib/PackManager.cpp index 118a451..61feea3 100644 --- a/src/PackLib/PackManager.cpp +++ b/src/PackLib/PackManager.cpp @@ -1,7 +1,8 @@ #include "PackManager.h" #include "EterLib/BufferPool.h" -#include #include +#include +#include #include "EterBase/Debug.h" CPackManager::CPackManager() @@ -22,6 +23,25 @@ CPackManager::~CPackManager() bool CPackManager::AddPack(const std::string& path) { + const std::filesystem::path packPath(path); + if (packPath.extension() == ".m2p") + { + std::shared_ptr pack = std::make_shared(); + if (!pack->Load(path)) + { + return false; + } + + std::lock_guard lock(m_mutex); + const auto& index = pack->GetIndex(); + for (const auto& entry : index) + { + m_m2_entries[entry.path] = std::make_pair(pack, entry); + } + + return true; + } + std::shared_ptr pack = std::make_shared(); if (!pack->Load(path)) @@ -51,6 +71,11 @@ bool CPackManager::GetFileWithPool(std::string_view path, TPackFile& result, CBu // First try to load from pack if (m_load_from_pack) { + auto m2it = m_m2_entries.find(buf); + if (m2it != m_m2_entries.end()) { + return m2it->second.first->GetFileWithPool(m2it->second.second, result, pPool); + } + auto it = m_entries.find(buf); if (it != m_entries.end()) { return it->second.first->GetFileWithPool(it->second.second, result, pPool); @@ -86,6 +111,11 @@ bool CPackManager::IsExist(std::string_view path) const // First check in pack entries if (m_load_from_pack) { + auto m2it = m_m2_entries.find(buf); + if (m2it != m_m2_entries.end()) { + return true; + } + auto it = m_entries.find(buf); if (it != m_entries.end()) { return true; diff --git a/src/PackLib/PackManager.h b/src/PackLib/PackManager.h index f6b712e..98c53e3 100644 --- a/src/PackLib/PackManager.h +++ b/src/PackLib/PackManager.h @@ -3,6 +3,7 @@ #include #include "EterBase/Singleton.h" +#include "M2Pack.h" #include "Pack.h" class CBufferPool; @@ -29,6 +30,7 @@ private: private: bool m_load_from_pack = true; TPackFileMap m_entries; + TM2PackFileMap m_m2_entries; CBufferPool* m_pBufferPool; mutable std::mutex m_mutex; // Thread safety for parallel pack loading }; diff --git a/src/UserInterface/UserInterface.cpp b/src/UserInterface/UserInterface.cpp index 1cd7cf2..a15abd0 100644 --- a/src/UserInterface/UserInterface.cpp +++ b/src/UserInterface/UserInterface.cpp @@ -14,6 +14,7 @@ #include "EterBase/lzo.h" #include "PackLib/PackManager.h" +#include "PackLib/M2PackRuntimeKeyProvider.h" #include #include @@ -57,6 +58,33 @@ bool PackInitialize(const char * c_pszFolder) if (_access(c_pszFolder, 0) != 0) return false; + auto AddPreferredPack = [c_pszFolder](const std::string& packName, bool required) -> bool + { + const std::string m2Path = std::format("{}/{}.m2p", c_pszFolder, packName); + const std::string legacyPath = std::format("{}/{}.pck", c_pszFolder, packName); + + if (_access(m2Path.c_str(), 0) == 0) + { + Tracef("PackInitialize: Loading %s\n", m2Path.c_str()); + if (CPackManager::instance().AddPack(m2Path)) + return true; + + TraceError("PackInitialize: Failed to load %s", m2Path.c_str()); + return false; + } + + if (_access(legacyPath.c_str(), 0) == 0) + { + Tracef("PackInitialize: Loading %s\n", legacyPath.c_str()); + if (CPackManager::instance().AddPack(legacyPath)) + return true; + + TraceError("PackInitialize: Failed to load %s", legacyPath.c_str()); + } + + return !required; + }; + std::vector packFiles = { "patch1", "season3_eu", @@ -149,18 +177,16 @@ bool PackInitialize(const char * c_pszFolder) "uiloading", }; - Tracef("PackInitialize: Loading root.pck\n"); DWORD dwStartTime = GetTickCount(); - if (!CPackManager::instance().AddPack(std::format("{}/root.pck", c_pszFolder))) + if (!AddPreferredPack("root", true)) { - TraceError("Failed to load root.pck"); + TraceError("Failed to load root pack"); return false; } Tracef("PackInitialize: Loading %d pack files...", packFiles.size()); for (const std::string& packFileName : packFiles) { - Tracef("PackInitialize: Loading %s.pck\n", packFileName.c_str()); - CPackManager::instance().AddPack(std::format("{}/{}.pck", c_pszFolder, packFileName)); + AddPreferredPack(packFileName, false); } Tracef("PackInitialize: done. Time taken: %d ms\n", GetTickCount() - dwStartTime); return true; @@ -326,5 +352,7 @@ int Setup(LPSTR lpCmdLine) Callback.Function = nullptr; Callback.UserData = 0; GrannySetLogCallback(&Callback); + + InitializeM2PackRuntimeKeyProvider(lpCmdLine); return 1; }