udk-manip/main.cpp

372 lines
11 KiB
C++
Raw Normal View History

2026-01-20 14:45:53 -05:00
#include <QCoreApplication>
#include <QTextStream>
#include <QFile>
#include <QDir>
#include <QFileInfo>
#include "coalesced.h"
static QTextStream cout(stdout);
static QTextStream cerr(stderr);
void printError(const QString& message)
{
cerr << "Error: " << message << "\n";
cerr.flush();
}
void showUsage()
{
2026-01-20 17:20:39 -05:00
cout << QString("UDK Config/Localization Manipulator %1\n").arg(QCoreApplication::applicationVersion());
cout << "Extract and pack UDK/UE3 coalesced Config/Localization files\n\n";
2026-01-20 14:45:53 -05:00
cout << "Usage:\n";
2026-01-20 17:13:07 -05:00
cout << QString(" %1 <command> [arguments]\n\n").arg(QCoreApplication::applicationName());
2026-01-20 14:45:53 -05:00
cout << "Commands:\n";
2026-01-20 17:20:39 -05:00
cout << " list <file> List files in archive\n";
cout << " info <file> Show archive metadata\n";
cout << " unpack <file> [output_dir] Extract all INI files\n";
cout << " pack <input_dir> <output_file> Pack directory into coalesced file\n";
cout << " extract <file> <index|name> [output] Extract single file\n\n";
2026-01-20 14:45:53 -05:00
cout << "Options:\n";
2026-01-20 17:20:39 -05:00
cout << " -h, --help Show this help message\n";
cout << " -v, --version Show version\n\n";
2026-01-20 14:45:53 -05:00
cout << "Drag & Drop:\n";
2026-01-20 17:20:39 -05:00
cout << " Drag a coalesced file onto exe Extracts to ./<name>_<ext>/\n";
cout << " Drag a folder onto exe Packs to ./<name>.<ext>\n";
cout << " (folder coalesced_ini -> coalesced.ini)\n";
2026-01-20 14:45:53 -05:00
cout.flush();
}
int cmdList(const QStringList& args)
{
if (args.isEmpty()) {
2026-01-20 17:13:07 -05:00
printError(QString("Usage: %1 list <file>").arg(QCoreApplication::applicationName()));
2026-01-20 14:45:53 -05:00
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<IniEntry>& 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()) {
2026-01-20 17:13:07 -05:00
printError(QString("Usage: %1 info <file>").arg(QCoreApplication::applicationName()));
2026-01-20 14:45:53 -05:00
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())
{
2026-01-20 17:13:07 -05:00
printError(QString("Usage: %1 unpack <file> [output_dir]").arg(QCoreApplication::applicationName()));
2026-01-20 14:45:53 -05:00
return 1;
}
CoalescedFile coalesced;
if (!coalesced.load(args[0]))
{
2026-01-20 14:45:53 -05:00
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();
}
2026-01-20 14:45:53 -05:00
QDir dir(outputDir);
if (!dir.exists() && !dir.mkpath("."))
{
2026-01-20 14:45:53 -05:00
printError(QString("Cannot create output directory: %1").arg(outputDir));
return 1;
}
int extracted = 0;
qint64 totalBytes = 0;
QFile tempFile(outputDir + "/temp");
if (!tempFile.open(QIODevice::WriteOnly))
{
printError(QString("Failed to open temp file: %1").arg(tempFile.errorString()));
return 1;
}
for (int i = 0; i < coalesced.entries().size(); i++)
{
QString entryStr(coalesced.entries().at(i).filename + "\n");
tempFile.write(entryStr.toUtf8());
}
tempFile.close();
for (int i = 0; i < coalesced.entries().size(); i++)
{
const IniEntry& entry = coalesced.entries().at(i);
2026-01-20 14:45:53 -05:00
QString localPath = entry.filename;
// Remove leading ".." components for safety
while (localPath.startsWith("..\\") || localPath.startsWith("../"))
{
2026-01-20 14:45:53 -05:00
localPath = localPath.mid(3);
}
localPath.replace('\\', '/');
QString fullPath = dir.filePath(localPath);
QFileInfo fi(fullPath);
if (!fi.dir().exists() && !QDir().mkpath(fi.path()))
{
2026-01-20 14:45:53 -05:00
printError(QString("Cannot create directory: %1").arg(fi.path()));
continue;
}
QFile file(fullPath);
if (!file.open(QIODevice::WriteOnly))
{
2026-01-20 14:45:53 -05:00
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) {
2026-01-20 17:13:07 -05:00
printError(QString("Usage: %1 pack <input_dir> <output_file>").arg(QCoreApplication::applicationName()));
2026-01-20 14:45:53 -05:00
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) {
2026-01-20 17:13:07 -05:00
printError(QString("Usage: %1 extract <file> <index|name> [output]").arg(QCoreApplication::applicationName()));
2026-01-20 14:45:53 -05:00
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);
2026-01-20 17:13:07 -05:00
QCoreApplication::setApplicationName("udk-manip");
QCoreApplication::setApplicationVersion("1.2");
2026-01-20 14:45:53 -05:00
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") {
2026-01-20 17:13:07 -05:00
cout << QString("%1 %2\n").arg(QCoreApplication::applicationVersion()).arg(QCoreApplication::applicationName());
2026-01-20 14:45:53 -05:00
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;
2026-01-20 14:45:53 -05:00
} else if (QFile::exists(cmd)) {
// File dragged onto exe - unpack to folder named after file
return cmdUnpack(QStringList() << cmd);
2026-01-20 14:45:53 -05:00
} else {
printError(QString("Unknown command: %1").arg(cmd));
showUsage();
return 1;
}
}