udk-manip/coalesced.cpp

315 lines
7.9 KiB
C++
Raw Normal View History

2026-01-20 14:45:53 -05:00
#include "coalesced.h"
#include <QFile>
#include <QDir>
#include <QDirIterator>
#include <QFileInfo>
#include <QDataStream>
#include <QtEndian>
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<unsigned char>(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);
2026-01-20 14:45:53 -05:00
quint32 entryCount;
stream >> entryCount;
2026-01-20 14:45:53 -05:00
entryCount /= 2;
2026-01-20 14:45:53 -05:00
for (uint i = 0; i < entryCount; i++)
{
quint32 filenameLen;
stream >> filenameLen;
2026-01-20 14:45:53 -05:00
if (filenameLen == 0 || filenameLen > 1024) {
break;
}
QByteArray filename(filenameLen - 1, Qt::Uninitialized);
stream.readRawData(filename.data(), filenameLen - 1);
stream.skipRawData(1);
2026-01-20 14:45:53 -05:00
quint32 contentLen;
stream >> contentLen;
2026-01-20 14:45:53 -05:00
QByteArray content(contentLen - 1, Qt::Uninitialized);
stream.readRawData(content.data(), contentLen - 1);
stream.skipRawData(1);
2026-01-20 14:45:53 -05:00
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;
2026-01-20 14:45:53 -05:00
// Write each entry
for (uint i = 0; i < m_entries.size(); i++) {
const IniEntry& entry = m_entries.at(i);
2026-01-20 14:45:53 -05:00
// Convert filename to Latin1 with null terminator
QByteArray filenameBytes = entry.filename.toLatin1();
filenameBytes.append('\0');
// Write filename length (including null terminator)
stream << static_cast<quint32>(filenameBytes.size());
// Write filename
stream.writeRawData(filenameBytes.constData(), filenameBytes.size());
QByteArray contentBytes = entry.content;
contentBytes.append('\0');
2026-01-20 14:45:53 -05:00
// Write content length
stream << static_cast<quint32>(contentBytes.size());
2026-01-20 14:45:53 -05:00
// Write content
stream.writeRawData(contentBytes.constData(), contentBytes.size());
2026-01-20 14:45:53 -05:00
}
file.close();
return true;
}
QList<IniEntry> 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";
2026-01-20 14:45:53 -05:00
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("..", "");
2026-01-20 14:45:53 -05:00
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");
2026-01-20 14:45:53 -05:00
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
}
2026-01-20 14:45:53 -05:00
void CoalescedFile::setError(const QString& error)
{
m_lastError = error;
}