#include "mainwindow.h" #include "splashscreen.h" #include "typeregistry.h" #include "settings.h" #include "compression.h" #include "logmanager.h" // Application metadata #define APP_NAME "XPlor" #define APP_VERSION "1.8" #define APP_ORG_NAME "RedLine Solutions LLC." #define APP_ORG_DOMAIN "redline.llc" #include #include #include #include #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) { 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(); } // Generate themed app icon by replacing red with accent color static QIcon generateThemedIcon(const QColor &accentColor) { // Try loading from Qt resource first (XPlor.png) QImage image(":/images/images/XPlor.png"); if (image.isNull()) { // Fallback: try app.ico in app directory QString iconPath = QCoreApplication::applicationDirPath() + "/app.ico"; image = QImage(iconPath); } if (image.isNull()) { return QIcon(); } // Convert to ARGB32 for pixel manipulation image = image.convertToFormat(QImage::Format_ARGB32); // Replace red-ish pixels with the accent color for (int y = 0; y < image.height(); y++) { QRgb *line = reinterpret_cast(image.scanLine(y)); for (int x = 0; x < image.width(); x++) { QColor pixel(line[x]); int r = pixel.red(); int g = pixel.green(); int b = pixel.blue(); int a = pixel.alpha(); // Detect red-ish pixels (high red, low green/blue) // The icon uses #ad0c0c (173, 12, 12) as the red color if (r > 100 && g < 80 && b < 80 && a > 0) { // Calculate how "red" this pixel is (0-1 scale) float intensity = static_cast(r) / 255.0f; // Apply the accent color with the same intensity QColor newColor = accentColor; newColor.setRed(static_cast(accentColor.red() * intensity)); newColor.setGreen(static_cast(accentColor.green() * intensity)); newColor.setBlue(static_cast(accentColor.blue() * intensity)); newColor.setAlpha(a); line[x] = newColor.rgba(); } } } return QIcon(QPixmap::fromImage(image)); } // 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.setOrganizationDomain(APP_ORG_DOMAIN); app.setOrganizationName(APP_ORG_NAME); app.setApplicationName(APP_NAME); app.setApplicationVersion(APP_VERSION); // Initialize log file for CLI output initCliLog(); QCommandLineParser parser; parser.setApplicationDescription( "XPlor - Binary File Format Explorer\n\n" "Parse and explore binary file formats using XScript definitions.\n" "Supports Call of Duty FastFiles, Asura archives, and custom formats.\n\n" "Features:\n" " - XScript DSL for defining binary structures\n" " - Hex viewer with highlighting\n" " - Audio/image preview for embedded assets\n" " - Theme support with customizable colors" ); 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 parsed data as JSON"); parser.addOption(jsonOption); QCommandLineOption gameOption(QStringList() << "game" << "g", "Game identifier (e.g., COD4, COD5, MW2)", "game", "COD4"); parser.addOption(gameOption); QCommandLineOption platformOption(QStringList() << "platform" << "t", "Target platform (PC, Xbox360, PS3)", "platform", "PC"); parser.addOption(platformOption); parser.addPositionalArgument("file", "Binary file to parse (e.g., FastFile, archive)"); 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); a.setOrganizationDomain(APP_ORG_DOMAIN); a.setOrganizationName(APP_ORG_NAME); a.setApplicationName(APP_NAME); a.setApplicationVersion(APP_VERSION); // Set themed window icon Theme currentTheme = Settings::instance().theme(); QIcon themedIcon = generateThemedIcon(QColor(currentTheme.accentColor)); if (!themedIcon.isNull()) { a.setWindowIcon(themedIcon); } // Initialize QuickBMS path from settings QString quickBmsPath = Settings::instance().quickBmsPath(); if (!quickBmsPath.isEmpty()) { Compression::setQuickBmsPath(quickBmsPath); } // Show splash screen SplashScreen splash; splash.setWaitForInteraction(false); // Normal behavior - close when finished splash.show(); a.processEvents(); splash.setStatus("Initializing..."); splash.setProgress(0, 100); a.processEvents(); // Load definitions with progress updates splash.setStatus("Loading definitions..."); splash.setProgress(10, 100); a.processEvents(); const QString definitionsDir = QCoreApplication::applicationDirPath() + "/definitions/"; // First pass: count files QStringList defFiles; QDirIterator countIt(definitionsDir, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories); while (countIt.hasNext()) { defFiles.append(countIt.next()); } // Second pass: load definitions TypeRegistry registry; QVector defResults; int loaded = 0; int total = defFiles.size(); LogManager::instance().addEntry(QString("[INIT] Loading %1 definition files...").arg(total)); for (const QString& path : defFiles) { QString fileName = QFileInfo(path).fileName(); splash.setStatus(QString("Loading: %1").arg(fileName)); splash.setProgress(10 + (loaded * 70 / qMax(1, total)), 100); a.processEvents(); QFile f(path); if (!f.open(QIODevice::ReadOnly)) { LogManager::instance().addError(QString("[DEF] Failed to open: %1").arg(fileName)); defResults.append({path, fileName, false, "Failed to open file"}); } else { try { registry.ingestScript(QString::fromUtf8(f.readAll()), path); LogManager::instance().addEntry(QString("[DEF] Loaded: %1").arg(fileName)); defResults.append({path, fileName, true, QString()}); } catch (const std::exception& e) { LogManager::instance().addError(QString("[DEF] Error in %1: %2").arg(fileName).arg(e.what())); defResults.append({path, fileName, false, QString::fromUtf8(e.what())}); } } loaded++; } int successCount = std::count_if(defResults.begin(), defResults.end(), [](const DefinitionLoadResult& r) { return r.success; }); LogManager::instance().addEntry(QString("[INIT] Loaded %1/%2 definitions successfully").arg(successCount).arg(total)); LogManager::instance().addLine(); splash.setStatus("Creating main window..."); splash.setProgress(85, 100); a.processEvents(); MainWindow w; // Pass loaded definitions to MainWindow w.setTypeRegistry(std::move(registry), defResults); splash.setStatus("Ready"); splash.setProgress(100, 100); a.processEvents(); QThread::msleep(200); // finish() will show the main window and keep splash on top if waitForInteraction is enabled splash.finish(&w); // Check for file argument to open (GUI mode) QStringList args = QCoreApplication::arguments(); for (int i = 1; i < args.size(); ++i) { const QString& arg = args[i]; // Skip options (start with -) if (arg.startsWith("-")) continue; // Try to open as file if (QFileInfo::exists(arg)) { QTimer::singleShot(100, &w, [&w, arg]() { w.openFile(arg); }); break; // Only open first file } } return a.exec(); }