Compare commits

...

5 Commits
1.1 ... main

Author SHA1 Message Date
njohnson
a398878aaa Fixed stuff. 2026-01-21 01:38:33 -05:00
njohnson
628d8a3c05 Fixed packing/unpacking localization files. 2026-01-21 00:47:04 -05:00
njohnson
bfd5d68f5b Adjust help screen. 2026-01-20 17:20:39 -05:00
njohnson
5cdd08e5d6 Made the app name dynamic. 2026-01-20 17:13:07 -05:00
njohnson
91b043929c Renamed to udk-manip. 2026-01-20 16:14:46 -05:00
4 changed files with 115 additions and 82 deletions

View File

@ -1,6 +1,6 @@
# udk-config-extractor # udk-manip
A command-line tool for extracting and repacking UDK/UE3 coalesced config and localization files. A command-line tool for manipulating UDK/UE3 coalesced config and localization files.
Tested with Army of Two: The 40th Day (Xbox 360). Tested with Army of Two: The 40th Day (Xbox 360).
@ -27,11 +27,11 @@ Tested with Army of Two: The 40th Day (Xbox 360).
## Usage ## Usage
``` ```
udk-config-extractor list <file> - List archive contents udk-manip list <file> - List archive contents
udk-config-extractor unpack <file> [output_dir] - Extract all files udk-manip unpack <file> [output_dir] - Extract all files
udk-config-extractor pack <input_dir> <output_file> - Create coalesced file udk-manip pack <input_dir> <output_file> - Create coalesced file
udk-config-extractor info <file> - Show archive info udk-manip info <file> - Show archive info
udk-config-extractor extract <file> <index> [output] - Extract single file udk-manip extract <file> <index> [output] - Extract single file
``` ```
### Drag & Drop ### Drag & Drop
@ -46,7 +46,7 @@ udk-config-extractor extract <file> <index> [output] - Extract single file
Requires Qt 6 and a C++17 compiler. Requires Qt 6 and a C++17 compiler.
```bash ```bash
qmake udk-config-extractor.pro qmake udk-manip.pro
make make
``` ```

View File

@ -48,53 +48,32 @@ bool CoalescedFile::load(const QString& path)
m_isCoalesced = true; m_isCoalesced = true;
m_loadedPath = path; m_loadedPath = path;
// Parse the coalesced format QDataStream stream(data);
const char* ptr = data.constData(); stream.setByteOrder(QDataStream::BigEndian);
const char* end = ptr + data.size();
// Read version (big-endian) quint32 entryCount;
if (ptr + 4 > end) { stream >> entryCount;
setError("Unexpected end of file reading header");
return false;
}
m_version = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(ptr)); entryCount /= 2;
ptr += 4;
// Read entries until EOF for (uint i = 0; i < entryCount; i++)
while (ptr + 4 <= end) { {
quint32 filenameLen = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(ptr)); quint32 filenameLen;
ptr += 4; stream >> filenameLen;
// Sanity check filename length - 0 or very large means we've hit end of entries
if (filenameLen == 0 || filenameLen > 1024) { if (filenameLen == 0 || filenameLen > 1024) {
break; break;
} }
// Read filename QByteArray filename(filenameLen - 1, Qt::Uninitialized);
if (ptr + filenameLen > end) { stream.readRawData(filename.data(), filenameLen - 1);
break; stream.skipRawData(1);
}
// Filename includes null terminator quint32 contentLen;
QString filename = QString::fromLatin1(ptr, filenameLen > 0 ? filenameLen - 1 : 0); stream >> contentLen;
ptr += filenameLen;
// Read content length QByteArray content(contentLen - 1, Qt::Uninitialized);
if (ptr + 4 > end) { stream.readRawData(content.data(), contentLen - 1);
break; stream.skipRawData(1);
}
quint32 contentLen = qFromBigEndian<quint32>(reinterpret_cast<const uchar*>(ptr));
ptr += 4;
// Read content
if (ptr + contentLen > end) {
break;
}
QByteArray content(ptr, contentLen);
ptr += contentLen;
IniEntry entry; IniEntry entry;
entry.filename = filename; entry.filename = filename;
@ -116,10 +95,11 @@ bool CoalescedFile::save(const QString& path) const
stream.setByteOrder(QDataStream::BigEndian); stream.setByteOrder(QDataStream::BigEndian);
// Write version // Write version
stream << m_version; stream << (quint32)m_entries.size() * 2;
// Write each entry // Write each entry
for (const IniEntry& entry : m_entries) { for (uint i = 0; i < m_entries.size(); i++) {
const IniEntry& entry = m_entries.at(i);
// Convert filename to Latin1 with null terminator // Convert filename to Latin1 with null terminator
QByteArray filenameBytes = entry.filename.toLatin1(); QByteArray filenameBytes = entry.filename.toLatin1();
filenameBytes.append('\0'); filenameBytes.append('\0');
@ -131,7 +111,7 @@ bool CoalescedFile::save(const QString& path) const
stream.writeRawData(filenameBytes.constData(), filenameBytes.size()); stream.writeRawData(filenameBytes.constData(), filenameBytes.size());
// Write content length // Write content length
stream << static_cast<quint32>(entry.content.size()); stream << static_cast<quint32>(entry.content.size() + 1);
// Write content // Write content
stream.writeRawData(entry.content.constData(), entry.content.size()); stream.writeRawData(entry.content.constData(), entry.content.size());
@ -237,10 +217,38 @@ bool CoalescedFile::packDirectory(const QString& dirPath, const QString& basePat
filters << "*.ini" << "*.int" << "*.jpn" << "*.deu" << "*.esn" filters << "*.ini" << "*.int" << "*.jpn" << "*.deu" << "*.esn"
<< "*.fra" << "*.ita" << "*.kor" << "*.pol" << "*.rus" << "*.fra" << "*.ita" << "*.kor" << "*.pol" << "*.rus"
<< "*.cze" << "*.hun" << "*.esm" << "*.ptb"; << "*.cze" << "*.hun" << "*.esm" << "*.ptb";
QDirIterator it(dirPath, filters, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) { int entryCount = 0;
QString filePath = it.next(); 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); QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) { if (!file.open(QIODevice::ReadOnly)) {

View File

@ -17,10 +17,10 @@ void printError(const QString& message)
void showUsage() void showUsage()
{ {
cout << "UDK Config Extractor v1.1\n"; cout << QString("UDK Config/Localization Manipulator %1\n").arg(QCoreApplication::applicationVersion());
cout << "Extract and pack UDK/UE3 coalesced INI/INT files\n\n"; cout << "Extract and pack UDK/UE3 coalesced Config/Localization files\n\n";
cout << "Usage:\n"; cout << "Usage:\n";
cout << " udk-config-extractor <command> [arguments]\n\n"; cout << QString(" %1 <command> [arguments]\n\n").arg(QCoreApplication::applicationName());
cout << "Commands:\n"; cout << "Commands:\n";
cout << " list <file> List files in archive\n"; cout << " list <file> List files in archive\n";
cout << " info <file> Show archive metadata\n"; cout << " info <file> Show archive metadata\n";
@ -40,7 +40,7 @@ void showUsage()
int cmdList(const QStringList& args) int cmdList(const QStringList& args)
{ {
if (args.isEmpty()) { if (args.isEmpty()) {
printError("Usage: udk-config-extractor list <file>"); printError(QString("Usage: %1 list <file>").arg(QCoreApplication::applicationName()));
return 1; return 1;
} }
@ -78,7 +78,7 @@ int cmdList(const QStringList& args)
int cmdInfo(const QStringList& args) int cmdInfo(const QStringList& args)
{ {
if (args.isEmpty()) { if (args.isEmpty()) {
printError("Usage: udk-config-extractor info <file>"); printError(QString("Usage: %1 info <file>").arg(QCoreApplication::applicationName()));
return 1; return 1;
} }
@ -120,28 +120,34 @@ int cmdInfo(const QStringList& args)
int cmdUnpack(const QStringList& args) int cmdUnpack(const QStringList& args)
{ {
if (args.isEmpty()) { if (args.isEmpty())
printError("Usage: udk-config-extractor unpack <file> [output_dir]"); {
printError(QString("Usage: %1 unpack <file> [output_dir]").arg(QCoreApplication::applicationName()));
return 1; return 1;
} }
CoalescedFile coalesced; CoalescedFile coalesced;
if (!coalesced.load(args[0])) { if (!coalesced.load(args[0]))
{
printError(coalesced.lastError()); printError(coalesced.lastError());
return 1; return 1;
} }
QString outputDir; QString outputDir;
if (args.size() > 1) { if (args.size() > 1)
{
outputDir = args[1]; outputDir = args[1];
} else { }
else
{
// Default: use filename with _ instead of . (e.g., coalesced.ini -> coalesced_ini) // Default: use filename with _ instead of . (e.g., coalesced.ini -> coalesced_ini)
QFileInfo fi(args[0]); QFileInfo fi(args[0]);
outputDir = fi.completeBaseName() + "_" + fi.suffix(); outputDir = fi.completeBaseName() + "_" + fi.suffix();
} }
QDir dir(outputDir); QDir dir(outputDir);
if (!dir.exists() && !dir.mkpath(".")) { if (!dir.exists() && !dir.mkpath("."))
{
printError(QString("Cannot create output directory: %1").arg(outputDir)); printError(QString("Cannot create output directory: %1").arg(outputDir));
return 1; return 1;
} }
@ -149,11 +155,28 @@ int cmdUnpack(const QStringList& args)
int extracted = 0; int extracted = 0;
qint64 totalBytes = 0; qint64 totalBytes = 0;
for (const IniEntry& entry : coalesced.entries()) { 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);
QString localPath = entry.filename; QString localPath = entry.filename;
// Remove leading ".." components for safety // Remove leading ".." components for safety
while (localPath.startsWith("..\\") || localPath.startsWith("../")) { while (localPath.startsWith("..\\") || localPath.startsWith("../"))
{
localPath = localPath.mid(3); localPath = localPath.mid(3);
} }
localPath.replace('\\', '/'); localPath.replace('\\', '/');
@ -161,13 +184,15 @@ int cmdUnpack(const QStringList& args)
QString fullPath = dir.filePath(localPath); QString fullPath = dir.filePath(localPath);
QFileInfo fi(fullPath); QFileInfo fi(fullPath);
if (!fi.dir().exists() && !QDir().mkpath(fi.path())) { if (!fi.dir().exists() && !QDir().mkpath(fi.path()))
{
printError(QString("Cannot create directory: %1").arg(fi.path())); printError(QString("Cannot create directory: %1").arg(fi.path()));
continue; continue;
} }
QFile file(fullPath); QFile file(fullPath);
if (!file.open(QIODevice::WriteOnly)) { if (!file.open(QIODevice::WriteOnly))
{
printError(QString("Cannot write file: %1").arg(fullPath)); printError(QString("Cannot write file: %1").arg(fullPath));
continue; continue;
} }
@ -190,7 +215,7 @@ int cmdUnpack(const QStringList& args)
int cmdPack(const QStringList& args) int cmdPack(const QStringList& args)
{ {
if (args.size() < 2) { if (args.size() < 2) {
printError("Usage: udk-config-extractor pack <input_dir> <output_file>"); printError(QString("Usage: %1 pack <input_dir> <output_file>").arg(QCoreApplication::applicationName()));
return 1; return 1;
} }
@ -216,7 +241,7 @@ int cmdPack(const QStringList& args)
int cmdExtract(const QStringList& args) int cmdExtract(const QStringList& args)
{ {
if (args.size() < 2) { if (args.size() < 2) {
printError("Usage: udk-config-extractor extract <file> <index|name> [output]"); printError(QString("Usage: %1 extract <file> <index|name> [output]").arg(QCoreApplication::applicationName()));
return 1; return 1;
} }
@ -273,8 +298,8 @@ int cmdExtract(const QStringList& args)
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
QCoreApplication app(argc, argv); QCoreApplication app(argc, argv);
QCoreApplication::setApplicationName("udk-config-extractor"); QCoreApplication::setApplicationName("udk-manip");
QCoreApplication::setApplicationVersion("1.1"); QCoreApplication::setApplicationVersion("1.2");
QStringList args = app.arguments(); QStringList args = app.arguments();
args.removeFirst(); // Remove program name args.removeFirst(); // Remove program name
@ -292,7 +317,7 @@ int main(int argc, char *argv[])
} }
if (cmd == "-v" || cmd == "--version" || cmd == "version") { if (cmd == "-v" || cmd == "--version" || cmd == "version") {
cout << QString("udk-config-extractor %1\n").arg(QCoreApplication::applicationVersion()); cout << QString("%1 %2\n").arg(QCoreApplication::applicationVersion()).arg(QCoreApplication::applicationName());
cout.flush(); cout.flush();
return 0; return 0;
} }

View File

@ -5,7 +5,7 @@ TEMPLATE = app
CONFIG += console c++17 CONFIG += console c++17
CONFIG -= app_bundle CONFIG -= app_bundle
TARGET = udk-config-extractor TARGET = udk-manip
SOURCES += \ SOURCES += \
main.cpp \ main.cpp \