#include "coalesced.h" #include #include #include #include #include #include CoalescedFile::CoalescedFile() : m_isCoalesced(false) , m_version(0x1e) // Default UE3 version { } bool CoalescedFile::load(const QString& path) { clear(); QFile file(path); if (!file.exists()) { setError(QString("File not found: %1").arg(path)); return false; } if (!file.open(QIODevice::ReadOnly)) { setError(QString("Cannot open file: %1").arg(file.errorString())); return false; } QByteArray data = file.readAll(); file.close(); if (data.size() < 4) { setError("File too small to be a valid coalesced file"); return false; } // Check if this is a coalesced file or a normal INI // Normal INI files start with '[' (0x5B) // Coalesced files start with a version (big-endian), first byte typically 0x00 if (static_cast(data[0]) == '[') { setError("This appears to be a normal INI file, not a coalesced file"); m_isCoalesced = false; return false; } m_isCoalesced = true; m_loadedPath = path; // Parse the coalesced format const char* ptr = data.constData(); const char* end = ptr + data.size(); // Read version (big-endian) if (ptr + 4 > end) { setError("Unexpected end of file reading header"); return false; } m_version = qFromBigEndian(reinterpret_cast(ptr)); ptr += 4; // Read entries until EOF while (ptr + 4 <= end) { quint32 filenameLen = qFromBigEndian(reinterpret_cast(ptr)); ptr += 4; // Sanity check filename length - 0 or very large means we've hit end of entries if (filenameLen == 0 || filenameLen > 1024) { break; } // Read filename if (ptr + filenameLen > end) { break; } // Filename includes null terminator QString filename = QString::fromLatin1(ptr, filenameLen > 0 ? filenameLen - 1 : 0); ptr += filenameLen; // Read content length if (ptr + 4 > end) { break; } quint32 contentLen = qFromBigEndian(reinterpret_cast(ptr)); ptr += 4; // Read content if (ptr + contentLen > end) { break; } QByteArray content(ptr, contentLen); ptr += contentLen; IniEntry entry; entry.filename = filename; entry.content = content; m_entries.append(entry); } return true; } bool CoalescedFile::save(const QString& path) const { QFile file(path); if (!file.open(QIODevice::WriteOnly)) { return false; } QDataStream stream(&file); stream.setByteOrder(QDataStream::BigEndian); // Write version stream << m_version; // Write each entry for (const IniEntry& entry : m_entries) { // Convert filename to Latin1 with null terminator QByteArray filenameBytes = entry.filename.toLatin1(); filenameBytes.append('\0'); // Write filename length (including null terminator) stream << static_cast(filenameBytes.size()); // Write filename stream.writeRawData(filenameBytes.constData(), filenameBytes.size()); // Write content length stream << static_cast(entry.content.size()); // Write content stream.writeRawData(entry.content.constData(), entry.content.size()); } file.close(); return true; } QList CoalescedFile::entries() const { return m_entries; } const IniEntry* CoalescedFile::entry(int index) const { if (index < 0 || index >= m_entries.count()) { return nullptr; } return &m_entries.at(index); } const IniEntry* CoalescedFile::entry(const QString& filename) const { for (const IniEntry& e : m_entries) { if (e.filename.compare(filename, Qt::CaseInsensitive) == 0) { return &e; } } return nullptr; } bool CoalescedFile::isCoalesced() const { return m_isCoalesced; } int CoalescedFile::count() const { return m_entries.count(); } qint64 CoalescedFile::totalSize() const { qint64 total = 0; for (const IniEntry& entry : m_entries) { total += entry.content.size(); } return total; } QString CoalescedFile::loadedPath() const { return m_loadedPath; } void CoalescedFile::clear() { m_entries.clear(); m_loadedPath.clear(); m_lastError.clear(); m_isCoalesced = false; m_version = 0x1e; // Default UE3 version } QString CoalescedFile::lastError() const { return m_lastError; } bool CoalescedFile::isCoalescedFile(const QString& path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return false; } char firstByte; if (file.read(&firstByte, 1) != 1) { return false; } // If first byte is '[', it's a normal INI file // Coalesced files start with a version (big-endian), first byte typically 0x00 return firstByte != '['; } bool CoalescedFile::packDirectory(const QString& dirPath, const QString& basePath) { clear(); QDir dir(dirPath); if (!dir.exists()) { setError(QString("Directory not found: %1").arg(dirPath)); return false; } QString effectiveBasePath = basePath.isEmpty() ? dirPath : basePath; // Find all config and localization files recursively // Supports: .ini, .int, .jpn, .deu, .esn, .fra, .ita, .kor, .pol, .rus, .cze, .hun, etc. QStringList filters; filters << "*.ini" << "*.int" << "*.jpn" << "*.deu" << "*.esn" << "*.fra" << "*.ita" << "*.kor" << "*.pol" << "*.rus" << "*.cze" << "*.hun" << "*.esm" << "*.ptb"; QDirIterator it(dirPath, filters, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { QString filePath = it.next(); QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { setError(QString("Cannot open file: %1").arg(filePath)); return false; } IniEntry entry; // Calculate relative path QString relativePath = QDir(effectiveBasePath).relativeFilePath(filePath); // Convert to the format used in coalesced files (with ..\) entry.filename = "..\\" + relativePath.replace('/', '\\'); entry.content = file.readAll(); m_entries.append(entry); file.close(); } if (m_entries.isEmpty()) { setError("No config/localization files found in directory"); return false; } m_isCoalesced = true; return true; } QString CoalescedFile::detectExtension() const { // Look at entries to detect the language/type // Check paths for language folder indicators (e.g., \INT\, \JPN\, \DEU\) // or file extensions for (const IniEntry& entry : m_entries) { QString path = entry.filename.toUpper(); // Check for language folder pattern like \INT\, \JPN\, etc. QStringList langs = {"INT", "JPN", "DEU", "ESN", "FRA", "ITA", "KOR", "POL", "RUS", "CZE", "HUN", "ESM", "PTB"}; for (const QString& lang : langs) { if (path.contains("\\" + lang + "\\")) { return lang.toLower(); } } // Fall back to file extension QFileInfo fi(entry.filename); QString ext = fi.suffix().toLower(); if (!ext.isEmpty() && ext != "ini") { return ext; } } return "ini"; // Default } void CoalescedFile::setError(const QString& error) { m_lastError = error; }