#include "mainwindow.h" #include "typeregistry.h" #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN #include #include #include #include static bool g_consoleAllocated = false; // True if we created our own console (need to wait at end) // Attach to parent console on Windows for CLI mode static void attachWindowsConsole() { // Try to attach to parent console (works when launched from cmd.exe) if (AttachConsole(ATTACH_PARENT_PROCESS)) { // Redirect stdout FILE* fp; freopen_s(&fp, "CONOUT$", "w", stdout); setvbuf(stdout, NULL, _IONBF, 0); // Redirect stderr freopen_s(&fp, "CONOUT$", "w", stderr); setvbuf(stderr, NULL, _IONBF, 0); // Redirect stdin freopen_s(&fp, "CONIN$", "r", stdin); // Fix C++ streams std::cout.clear(); std::cerr.clear(); std::cin.clear(); } else { // No parent console - allocate our own console window if (AllocConsole()) { g_consoleAllocated = true; FILE* fp; freopen_s(&fp, "CONOUT$", "w", stdout); setvbuf(stdout, NULL, _IONBF, 0); freopen_s(&fp, "CONOUT$", "w", stderr); setvbuf(stderr, NULL, _IONBF, 0); freopen_s(&fp, "CONIN$", "r", stdin); std::cout.clear(); std::cerr.clear(); std::cin.clear(); // Set console title SetConsoleTitleA("XPlor CLI"); } } } #endif // Convert QVariantMap to JSON recursively static QJsonValue variantToJson(const QVariant& v) { if (v.typeId() == QMetaType::QVariantMap) { QJsonObject obj; const QVariantMap map = v.toMap(); for (auto it = map.begin(); it != map.end(); ++it) { if (it.key().startsWith("_") && it.key() != "_name" && it.key() != "_type") continue; obj[it.key()] = variantToJson(it.value()); } return obj; } else if (v.typeId() == QMetaType::QVariantList) { QJsonArray arr; for (const QVariant& item : v.toList()) { arr.append(variantToJson(item)); } return arr; } else if (v.typeId() == QMetaType::QString) { return v.toString(); } else if (v.typeId() == QMetaType::Int || v.typeId() == QMetaType::LongLong) { return v.toLongLong(); } else if (v.typeId() == QMetaType::UInt || v.typeId() == QMetaType::ULongLong) { return v.toLongLong(); } else if (v.typeId() == QMetaType::Double || v.typeId() == QMetaType::Float) { return v.toDouble(); } else if (v.typeId() == QMetaType::Bool) { return v.toBool(); } else if (v.typeId() == QMetaType::QByteArray) { return QString("0x") + v.toByteArray().toHex(); } else if (v.isNull()) { return QJsonValue::Null; } return v.toString(); } // CLI output - writes to both console and log file for reliability static QFile* g_logFile = nullptr; static void initCliLog() { QString logPath = QCoreApplication::applicationDirPath() + "/xplor_cli.log"; g_logFile = new QFile(logPath); g_logFile->open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text); } static void cliOut(const QString& msg) { std::cout << msg.toStdString() << std::flush; if (g_logFile && g_logFile->isOpen()) { g_logFile->write(msg.toUtf8()); g_logFile->flush(); } } static void cliErr(const QString& msg) { std::cerr << msg.toStdString() << std::flush; if (g_logFile && g_logFile->isOpen()) { g_logFile->write(msg.toUtf8()); g_logFile->flush(); } } static int runCli(const QString& filePath, const QString& game, const QString& platform, bool jsonOutput) { TypeRegistry registry; const QString definitionsDir = QCoreApplication::applicationDirPath() + "/definitions/"; QDirIterator it(definitionsDir, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories); int defCount = 0; while (it.hasNext()) { const QString path = it.next(); const QString fileName = QFileInfo(path).fileName(); QFile f(path); if (!f.open(QIODevice::ReadOnly)) { cliErr("[ERROR] Cannot open definition: " + path + "\n"); continue; } try { registry.ingestScript(QString::fromUtf8(f.readAll()), path); defCount++; } catch (const std::exception& e) { cliErr("[ERROR] Loading " + fileName + ": " + e.what() + "\n"); return 1; } } if (defCount == 0) { cliErr("[ERROR] No definitions found in: " + definitionsDir + "\n"); return 1; } if (!jsonOutput) { cliErr("[INFO] Loaded " + QString::number(defCount) + " definitions\n"); } QFile inputFile(filePath); if (!inputFile.open(QIODevice::ReadOnly)) { cliErr("[ERROR] Cannot open file: " + filePath + "\n"); return 1; } const QString rootType = registry.chooseType(&inputFile, filePath); if (rootType.isEmpty()) { cliErr("[ERROR] No matching definition for file: " + filePath + "\n"); return 1; } if (!jsonOutput) { cliErr("[INFO] Matched type: " + rootType + "\n"); cliErr("[INFO] Parsing...\n"); } QVariantMap result; try { result = registry.parse(rootType, &inputFile, filePath, nullptr); } catch (const std::exception& e) { cliErr("[ERROR] Parse failed: " + QString(e.what()) + "\n"); if (jsonOutput) { QJsonObject errorObj; errorObj["success"] = false; errorObj["error"] = QString(e.what()); errorObj["file"] = filePath; errorObj["type"] = rootType; cliOut(QJsonDocument(errorObj).toJson(QJsonDocument::Compact) + "\n"); } return 1; } if (jsonOutput) { QJsonObject output; output["success"] = true; output["file"] = filePath; output["type"] = rootType; output["data"] = variantToJson(result).toObject(); cliOut(QJsonDocument(output).toJson(QJsonDocument::Indented) + "\n"); } else { cliErr("[SUCCESS] Parsed " + filePath + " as " + rootType + "\n"); if (result.contains("asset_count")) { cliErr("[INFO] Asset count: " + result["asset_count"].toString() + "\n"); } if (result.contains("parsed_assets")) { QVariantList assets = result["parsed_assets"].toList(); QHash typeCounts; for (const QVariant& a : assets) { QVariantMap am = a.toMap(); QString t = am.value("_type", "unknown").toString(); typeCounts[t]++; } cliErr("[INFO] Parsed assets:\n"); for (auto it = typeCounts.begin(); it != typeCounts.end(); ++it) { cliErr(" " + it.key() + ": " + QString::number(it.value()) + "\n"); } } cliOut("OK\n"); } return 0; } int main(int argc, char *argv[]) { bool cliMode = false; for (int i = 1; i < argc; i++) { QString arg(argv[i]); if (arg == "--cli" || arg == "--parse" || arg == "-p" || arg == "--json") { cliMode = true; break; } } if (cliMode) { #ifdef Q_OS_WIN attachWindowsConsole(); #endif QCoreApplication app(argc, argv); app.setApplicationName("XPlor"); app.setApplicationVersion("1.0"); // Initialize log file for CLI output initCliLog(); QCommandLineParser parser; parser.setApplicationDescription("XPlor - Call of Duty FastFile Explorer"); parser.addHelpOption(); parser.addVersionOption(); QCommandLineOption cliOption(QStringList() << "cli" << "parse" << "p", "Run in CLI mode (no GUI)"); parser.addOption(cliOption); QCommandLineOption jsonOption(QStringList() << "json" << "j", "Output results as JSON"); parser.addOption(jsonOption); QCommandLineOption gameOption(QStringList() << "game" << "g", "Game identifier", "game", "COD4"); parser.addOption(gameOption); QCommandLineOption platformOption(QStringList() << "platform" << "t", "Platform (PC, Xbox360, PS3)", "platform", "PC"); parser.addOption(platformOption); parser.addPositionalArgument("file", "FastFile to parse"); parser.process(app); const QStringList args = parser.positionalArguments(); if (args.isEmpty()) { cliErr("Error: No input file specified\n\n"); cliErr(parser.helpText() + "\n"); return 1; } int result = runCli(args.first(), parser.value(gameOption), parser.value(platformOption), parser.isSet(jsonOption)); #ifdef Q_OS_WIN // If we allocated our own console window, wait for user input before closing if (g_consoleAllocated) { cliErr("\nPress Enter to exit..."); std::cin.get(); } #endif return result; } QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }