#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; QDataStream stream(data); stream.setByteOrder(QDataStream::BigEndian); quint32 entryCount; stream >> entryCount; entryCount /= 2; for (uint i = 0; i < entryCount; i++) { quint32 filenameLen; stream >> filenameLen; if (filenameLen == 0 || filenameLen > 1024) { break; } QByteArray filename(filenameLen - 1, Qt::Uninitialized); stream.readRawData(filename.data(), filenameLen - 1); stream.skipRawData(1); quint32 contentLen; stream >> contentLen; QByteArray content(contentLen - 1, Qt::Uninitialized); stream.readRawData(content.data(), contentLen - 1); stream.skipRawData(1); 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 << (quint32)m_entries.size() * 2; // Write each entry for (uint i = 0; i < m_entries.size(); i++) { const IniEntry& entry = m_entries.at(i); // 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() + 1); // 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"; int entryCount = 0; QDirIterator entryIt(dirPath, filters, QDir::Files, QDirIterator::Subdirectories); while (entryIt.hasNext()) { entryIt.next(); entryCount++; } // Read temp file for entry records QFile tempFile(dirPath + "/temp"); if (!tempFile.open(QIODevice::ReadOnly)) { setError(QString("Failed to open temp file: %1").arg(tempFile.errorString())); return 1; } // Parse filenames QStringList entryStrings; for (int i = 0; i < entryCount; i++) { QByteArray entryStr(64, Qt::Uninitialized); qint64 lineLen = tempFile.readLine(entryStr.data(), 64); entryStrings.push_back(entryStr.mid(0, lineLen - 1)); } tempFile.close(); for (int i = 0; i < entryStrings.size(); i++) { QString entryString = entryStrings.at(i); const QString& filePath = dirPath + entryString.replace("..", ""); 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; }