#include #include #include #include #include #include "coalesced.h" static QTextStream cout(stdout); static QTextStream cerr(stderr); void printError(const QString& message) { cerr << "Error: " << message << "\n"; cerr.flush(); } void showUsage() { cout << QString("UDK Config/Localization Manipulator %1\n").arg(QCoreApplication::applicationVersion()); cout << "Extract and pack UDK/UE3 coalesced Config/Localization files\n\n"; cout << "Usage:\n"; cout << QString(" %1 [arguments]\n\n").arg(QCoreApplication::applicationName()); cout << "Commands:\n"; cout << " list List files in archive\n"; cout << " info Show archive metadata\n"; cout << " unpack [output_dir] Extract all INI files\n"; cout << " pack Pack directory into coalesced file\n"; cout << " extract [output] Extract single file\n\n"; cout << "Options:\n"; cout << " -h, --help Show this help message\n"; cout << " -v, --version Show version\n\n"; cout << "Drag & Drop:\n"; cout << " Drag a coalesced file onto exe Extracts to ./_/\n"; cout << " Drag a folder onto exe Packs to ./.\n"; cout << " (folder coalesced_ini -> coalesced.ini)\n"; cout.flush(); } int cmdList(const QStringList& args) { if (args.isEmpty()) { printError(QString("Usage: %1 list ").arg(QCoreApplication::applicationName())); return 1; } CoalescedFile coalesced; if (!coalesced.load(args[0])) { printError(coalesced.lastError()); return 1; } cout << QString("%1 %2 %3\n") .arg("Index", 5) .arg("Size", 8) .arg("Filename"); cout << QString("%1 %2 %3\n") .arg(QString("-").repeated(5), 5) .arg(QString("-").repeated(8), 8) .arg(QString("-").repeated(50)); const QList& entries = coalesced.entries(); for (int i = 0; i < entries.count(); ++i) { const IniEntry& entry = entries[i]; cout << QString("%1 %2 %3\n") .arg(i, 5) .arg(entry.content.size(), 8) .arg(entry.filename); } cout << QString("\nTotal: %1 files, %2 bytes\n") .arg(coalesced.count()) .arg(coalesced.totalSize()); cout.flush(); return 0; } int cmdInfo(const QStringList& args) { if (args.isEmpty()) { printError(QString("Usage: %1 info ").arg(QCoreApplication::applicationName())); return 1; } CoalescedFile coalesced; if (!coalesced.load(args[0])) { printError(coalesced.lastError()); return 1; } cout << "Archive Information:\n"; cout << QString(" File: %1\n").arg(coalesced.loadedPath()); cout << QString(" Entries: %1\n").arg(coalesced.count()); cout << QString(" Total content size: %1 bytes\n").arg(coalesced.totalSize()); qint64 minSize = LLONG_MAX; qint64 maxSize = 0; QString minFile, maxFile; for (const IniEntry& entry : coalesced.entries()) { if (entry.content.size() < minSize) { minSize = entry.content.size(); minFile = entry.filename; } if (entry.content.size() > maxSize) { maxSize = entry.content.size(); maxFile = entry.filename; } } if (coalesced.count() > 0) { cout << QString(" Smallest: %1 bytes (%2)\n").arg(minSize).arg(minFile); cout << QString(" Largest: %1 bytes (%2)\n").arg(maxSize).arg(maxFile); cout << QString(" Average: %1 bytes\n").arg(coalesced.totalSize() / coalesced.count()); } cout.flush(); return 0; } int cmdUnpack(const QStringList& args) { if (args.isEmpty()) { printError(QString("Usage: %1 unpack [output_dir]").arg(QCoreApplication::applicationName())); return 1; } CoalescedFile coalesced; if (!coalesced.load(args[0])) { printError(coalesced.lastError()); return 1; } QString outputDir; if (args.size() > 1) { outputDir = args[1]; } else { // Default: use filename with _ instead of . (e.g., coalesced.ini -> coalesced_ini) QFileInfo fi(args[0]); outputDir = fi.completeBaseName() + "_" + fi.suffix(); } QDir dir(outputDir); if (!dir.exists() && !dir.mkpath(".")) { printError(QString("Cannot create output directory: %1").arg(outputDir)); return 1; } int extracted = 0; qint64 totalBytes = 0; for (const IniEntry& entry : coalesced.entries()) { QString localPath = entry.filename; // Remove leading ".." components for safety while (localPath.startsWith("..\\") || localPath.startsWith("../")) { localPath = localPath.mid(3); } localPath.replace('\\', '/'); QString fullPath = dir.filePath(localPath); QFileInfo fi(fullPath); if (!fi.dir().exists() && !QDir().mkpath(fi.path())) { printError(QString("Cannot create directory: %1").arg(fi.path())); continue; } QFile file(fullPath); if (!file.open(QIODevice::WriteOnly)) { printError(QString("Cannot write file: %1").arg(fullPath)); continue; } file.write(entry.content); file.close(); extracted++; totalBytes += entry.content.size(); } cout << QString("Extracted %1 files (%2 bytes) to %3\n") .arg(extracted) .arg(totalBytes) .arg(QDir(outputDir).absolutePath()); cout.flush(); return 0; } int cmdPack(const QStringList& args) { if (args.size() < 2) { printError(QString("Usage: %1 pack ").arg(QCoreApplication::applicationName())); return 1; } CoalescedFile packer; if (!packer.packDirectory(args[0])) { printError(packer.lastError()); return 1; } if (!packer.save(args[1])) { printError(QString("Cannot write output file: %1").arg(args[1])); return 1; } cout << QString("Packed %1 files (%2 bytes) to %3\n") .arg(packer.count()) .arg(packer.totalSize()) .arg(args[1]); cout.flush(); return 0; } int cmdExtract(const QStringList& args) { if (args.size() < 2) { printError(QString("Usage: %1 extract [output]").arg(QCoreApplication::applicationName())); return 1; } CoalescedFile coalesced; if (!coalesced.load(args[0])) { printError(coalesced.lastError()); return 1; } QString target = args[1]; const IniEntry* entry = nullptr; bool ok; int index = target.toInt(&ok); if (ok) { entry = coalesced.entry(index); if (!entry) { printError(QString("Index %1 out of range (0-%2)").arg(index).arg(coalesced.count() - 1)); return 1; } } else { entry = coalesced.entry(target); if (!entry) { printError(QString("File not found: %1").arg(target)); return 1; } } QString outputPath; if (args.size() > 2) { outputPath = args[2]; } else { QFileInfo fi(entry->filename); outputPath = fi.fileName(); } QFile file(outputPath); if (!file.open(QIODevice::WriteOnly)) { printError(QString("Cannot write file: %1").arg(outputPath)); return 1; } file.write(entry->content); file.close(); cout << QString("Extracted '%1' to '%2' (%3 bytes)\n") .arg(entry->filename) .arg(outputPath) .arg(entry->content.size()); cout.flush(); return 0; } int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationName("udk-manip"); QCoreApplication::setApplicationVersion("1.2"); QStringList args = app.arguments(); args.removeFirst(); // Remove program name if (args.isEmpty()) { showUsage(); return 1; } QString cmd = args.takeFirst().toLower(); if (cmd == "-h" || cmd == "--help" || cmd == "help") { showUsage(); return 0; } if (cmd == "-v" || cmd == "--version" || cmd == "version") { cout << QString("%1 %2\n").arg(QCoreApplication::applicationVersion()).arg(QCoreApplication::applicationName()); cout.flush(); return 0; } if (cmd == "list") { return cmdList(args); } else if (cmd == "info") { return cmdInfo(args); } else if (cmd == "unpack") { return cmdUnpack(args); } else if (cmd == "pack") { return cmdPack(args); } else if (cmd == "extract") { return cmdExtract(args); } else if (QFileInfo(cmd).isDir()) { // Directory dragged onto exe - derive output filename from folder name // Folder "coalesced_ini" -> output "coalesced.ini" CoalescedFile packer; if (!packer.packDirectory(cmd)) { printError(packer.lastError()); return 1; } // Get folder name and replace last _ with . QString folderName = QFileInfo(cmd).fileName(); int lastUnderscore = folderName.lastIndexOf('_'); QString outputFile; if (lastUnderscore > 0) { outputFile = folderName.left(lastUnderscore) + "." + folderName.mid(lastUnderscore + 1); } else { // Fallback: use detected extension outputFile = folderName + "." + packer.detectExtension(); } if (!packer.save(outputFile)) { printError(QString("Cannot write output file: %1").arg(outputFile)); return 1; } cout << QString("Packed %1 files (%2 bytes) to %3\n") .arg(packer.count()) .arg(packer.totalSize()) .arg(outputFile); cout.flush(); return 0; } else if (QFile::exists(cmd)) { // File dragged onto exe - unpack to folder named after file return cmdUnpack(QStringList() << cmd); } else { printError(QString("Unknown command: %1").arg(cmd)); showUsage(); return 1; } }