From f06ef3402e53f06ac268573f8af1767c15d664e8 Mon Sep 17 00:00:00 2001 From: Xenius97 Date: Fri, 13 Mar 2026 11:44:36 +0100 Subject: [PATCH] Initial commit --- Client/mods/deathmatch/logic/CClientIMG.cpp | 246 +++++++++++++++++- Client/mods/deathmatch/logic/CClientIMG.h | 10 + .../logic/luadefs/CLuaEngineDefs.cpp | 24 +- 3 files changed, 273 insertions(+), 7 deletions(-) diff --git a/Client/mods/deathmatch/logic/CClientIMG.cpp b/Client/mods/deathmatch/logic/CClientIMG.cpp index bd366725db8..1a5336fa33e 100644 --- a/Client/mods/deathmatch/logic/CClientIMG.cpp +++ b/Client/mods/deathmatch/logic/CClientIMG.cpp @@ -18,7 +18,13 @@ struct tImgHeader }; CClientIMG::CClientIMG(class CClientManager* pManager, ElementID ID) - : ClassInit(this), CClientEntity(ID), m_pImgManager(pManager->GetIMGManager()), m_ucArchiveID(INVALID_ARCHIVE_ID), m_LargestFileSizeBlocks(0) + : ClassInit(this), + CClientEntity(ID), + m_pImgManager(pManager->GetIMGManager()), + m_ucArchiveID(INVALID_ARCHIVE_ID), + m_LargestFileSizeBlocks(0), + m_bFolderMode(false), + m_bTempImgCreated(false) { m_pManager = pManager; SetTypeName("img"); @@ -36,7 +42,7 @@ void CClientIMG::Unlink() if (IsStreamed()) StreamDisable(); - if (m_ifs.is_open()) + if (m_ifs.is_open() || m_bFolderMode) Unload(); } @@ -101,6 +107,39 @@ void CClientIMG::Unload() m_fileInfos.clear(); m_fileInfos.shrink_to_fit(); m_ifs.close(); + + if (m_bFolderMode) + { + m_folderFilePaths.clear(); + m_folderFilePaths.shrink_to_fit(); + + if (m_bTempImgCreated && !m_tempImgPath.empty()) + { + std::error_code ec; + fs::remove(m_tempImgPath, ec); + m_tempImgPath.clear(); + } + + m_bTempImgCreated = false; + m_bFolderMode = false; + } + + if (!m_tempImgDirPath.empty()) + { + std::error_code ec; + for (const auto& entry : fs::directory_iterator(m_tempImgDirPath, ec)) + { + if (ec) + break; + + if (!entry.is_regular_file()) + continue; + + const auto filename = entry.path().filename().string(); + if (filename.find("runtime-image") != std::string::npos) + fs::remove(entry.path(), ec); + } + } } bool CClientIMG::GetFile(size_t fileID, std::string& buffer) @@ -120,6 +159,17 @@ bool CClientIMG::GetFile(size_t fileID, std::string& buffer) throw std::invalid_argument("Out of memory"); } + if (!m_ifs.is_open()) + { + m_ifs = std::ifstream(m_filePath, std::ios::binary); + if (m_ifs.fail()) + { + m_ifs.close(); + return false; + } + } + + m_ifs.clear(); m_ifs.seekg((std::streampos)pFileInfo->uiOffset * 2048); m_ifs.read(buffer.data(), toReadBytes); @@ -148,6 +198,192 @@ bool CClientIMG::IsStreamed() return m_ucArchiveID != INVALID_ARCHIVE_ID; } +bool CClientIMG::LoadFolder(fs::path folderPath, fs::path tempDirPath) +{ + if (!m_fileInfos.empty()) + return false; + + if (folderPath.empty()) + return false; + + if (!fs::is_directory(folderPath)) + return false; + + m_tempImgDirPath = std::move(tempDirPath); + + if (!m_tempImgDirPath.empty()) + { + std::error_code ec; + fs::create_directories(m_tempImgDirPath, ec); + if (ec) + return false; + + for (const auto& entry : fs::directory_iterator(m_tempImgDirPath, ec)) + { + if (ec) + break; + + if (!entry.is_regular_file()) + continue; + + const auto filename = entry.path().filename().string(); + if (filename.find("runtime-image") != std::string::npos) + fs::remove(entry.path(), ec); + } + } + + // Collect regular files and sort alphabetically for deterministic ordering + std::vector entries; + for (const auto& entry : fs::directory_iterator(folderPath)) + { + if (entry.is_regular_file()) + entries.push_back(entry); + } + std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { return a.path().filename() < b.path().filename(); }); + + CResourceManager* pResourceManager = m_pManager ? m_pManager->GetResourceManager() : nullptr; + + unsigned int uiCurrentOffset = 0; + for (const auto& entry : entries) + { + const auto& filePath = entry.path(); + const auto filename = filePath.filename().string(); + + // tImgFileInfo::szFileName is 24 bytes (23 usable chars + null terminator) + if (filename.size() > 23) + continue; + + if (pResourceManager) + { + SString strFilePath = SharedUtil::ToUTF8(filePath.wstring()); + SString strConformed = PathConform(strFilePath).ToLower(); + + // Include only files known to the resource download manager + if (!pResourceManager->GetDownloadableResourceFile(strConformed)) + continue; + } + + std::error_code ec; + const auto fileSize = entry.file_size(ec); + if (ec || fileSize == 0) + continue; + + const auto sizeInBlocks = static_cast((fileSize + 2047) / 2048); + + tImgFileInfo fileInfo{}; + fileInfo.uiOffset = uiCurrentOffset; + fileInfo.usSize = sizeInBlocks; + fileInfo.usUnpackedSize = 0; + std::strncpy(fileInfo.szFileName, filename.c_str(), 23); + fileInfo.szFileName[23] = '\0'; + + m_fileInfos.push_back(fileInfo); + m_folderFilePaths.push_back(filePath); + + uiCurrentOffset += sizeInBlocks; + } + + if (m_fileInfos.empty()) + return false; + + m_bFolderMode = true; + return true; +} + +bool CClientIMG::BuildTempIMG() +{ + if (m_tempImgDirPath.empty()) + return false; + + std::error_code ec; + fs::create_directories(m_tempImgDirPath, ec); + if (ec) + return false; + + const unsigned long long base = static_cast(GetTickCount64_()); + const unsigned long long pid = static_cast(GetCurrentProcessId()); + const std::wstring fileName = L"runtime-image-" + std::to_wstring(base) + L"-" + std::to_wstring(pid) + L".tmp"; + m_tempImgPath = m_tempImgDirPath / fileName; + + ec.clear(); + if (fs::exists(m_tempImgPath, ec) || ec) + return false; + + std::ofstream ofs(m_tempImgPath, std::ios::binary | std::ios::trunc); + if (!ofs.is_open()) + { + fs::remove(m_tempImgPath); + return false; + } + + std::string readBuf; + std::vector zeroPad; + + for (size_t i = 0; i < m_folderFilePaths.size(); i++) + { + const auto& filePath = m_folderFilePaths[i]; + const auto& fileInfo = m_fileInfos[i]; + + std::error_code ec; + const auto fileSize = fs::file_size(filePath, ec); + if (ec) + { + ofs.close(); + fs::remove(m_tempImgPath); + return false; + } + + try + { + readBuf.resize(fileSize); + } + catch (const std::bad_alloc&) + { + ofs.close(); + fs::remove(m_tempImgPath); + return false; + } + + std::ifstream ifs(filePath, std::ios::binary); + if (!ifs.is_open()) + { + ofs.close(); + fs::remove(m_tempImgPath); + return false; + } + + ifs.read(readBuf.data(), fileSize); + if (ifs.fail()) + { + ofs.close(); + fs::remove(m_tempImgPath); + return false; + } + + ofs.write(readBuf.data(), fileSize); + + const size_t paddedSize = static_cast(fileInfo.usSize) * 2048; + const size_t paddingNeeded = paddedSize - fileSize; + if (paddingNeeded > 0) + { + zeroPad.assign(paddingNeeded, '\0'); + ofs.write(zeroPad.data(), paddingNeeded); + } + + if (ofs.fail()) + { + ofs.close(); + fs::remove(m_tempImgPath); + return false; + } + } + + ofs.close(); + m_filePath = m_tempImgPath; + m_bTempImgCreated = true; + return true; +} + bool CClientIMG::StreamEnable() { if (m_fileInfos.empty()) @@ -156,6 +392,12 @@ bool CClientIMG::StreamEnable() if (IsStreamed()) return false; + if (m_bFolderMode && !m_bTempImgCreated) + { + if (!BuildTempIMG()) + return false; + } + if (m_LargestFileSizeBlocks == 0) { for (const auto& fileInfo : m_fileInfos) diff --git a/Client/mods/deathmatch/logic/CClientIMG.h b/Client/mods/deathmatch/logic/CClientIMG.h index 7408c3407cd..2069acc6298 100644 --- a/Client/mods/deathmatch/logic/CClientIMG.h +++ b/Client/mods/deathmatch/logic/CClientIMG.h @@ -61,6 +61,7 @@ class CClientIMG : public CClientEntity auto GetLargestFileSizeBlocks() const { return m_LargestFileSizeBlocks; } bool Load(fs::path filePath); + bool LoadFolder(fs::path folderPath, fs::path tempDirPath = {}); void Unload(); tImgFileInfo* GetFileInfo(size_t fileID); @@ -70,11 +71,14 @@ class CClientIMG : public CClientEntity bool StreamEnable(); bool StreamDisable(); bool IsStreamed(); + bool IsFolderMode() const noexcept { return m_bFolderMode; } bool LinkModel(unsigned int usModelID, size_t fileID); bool UnlinkModel(unsigned int usModelID); private: + bool BuildTempIMG(); + class CClientIMGManager* m_pImgManager; std::ifstream m_ifs; @@ -83,5 +87,11 @@ class CClientIMG : public CClientEntity std::vector m_fileInfos; size_t m_LargestFileSizeBlocks; // The size of the largest file [in streaming blocks/sectors] + bool m_bFolderMode; + bool m_bTempImgCreated; + fs::path m_tempImgPath; + fs::path m_tempImgDirPath; + std::vector m_folderFilePaths; + std::vector m_restoreInfo; }; diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp index 972ddf93f87..6aeed1e6497 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaEngineDefs.cpp @@ -726,21 +726,35 @@ CClientIMG* CLuaEngineDefs::EngineLoadIMG(lua_State* const luaVM, std::string st // Create the img handle CClientIMG* pImg = new CClientIMG(m_pManager, INVALID_ELEMENT_ID); - // Fix path encoding for sdt::filesystem + // Fix path encoding for std::filesystem std::wstring utf8Path = SharedUtil::MbUTF8ToUTF16(strFullPath); - // Attempt loading the file - if (pImg->Load(std::move(utf8Path))) + std::error_code ec; + const bool bIsFolder = std::filesystem::is_directory(utf8Path, ec) && !ec; + + // Detect if this is a folder IMG or a regular file IMG and call the appropriate loading function. + bool bLoaded = false; + if (bIsFolder) + { + const SString strResourceRoot = pResource->GetResourceDirectoryPath(); + const std::filesystem::path resourceRootPath = SharedUtil::MbUTF8ToUTF16(strResourceRoot); + bLoaded = pImg->LoadFolder(std::move(utf8Path), resourceRootPath); + } + else + { + bLoaded = pImg->Load(std::move(utf8Path)); + } + + if (bLoaded) { // Success. Make it a child of the resource img root pImg->SetParent(pRoot); - return pImg; } else { delete pImg; - throw std::invalid_argument("Error loading IMG"); + throw std::invalid_argument(bIsFolder ? "Error creating runtime IMG from folder" : "Error loading IMG"); } }