XPlor/app/main.cpp

456 lines
15 KiB
C++
Raw Normal View History

#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 <QApplication>
#include <QCoreApplication>
#include <QCommandLineParser>
#include <QCommandLineOption>
#include <QFile>
#include <QFileInfo>
#include <QDirIterator>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QTextStream>
#include <QThread>
#include <QImage>
#include <QIcon>
#include <iostream>
#ifdef Q_OS_WIN
#include <windows.h>
#include <io.h>
#include <fcntl.h>
#include <stdio.h>
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<QRgb*>(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<float>(r) / 255.0f;
// Apply the accent color with the same intensity
QColor newColor = accentColor;
newColor.setRed(static_cast<int>(accentColor.red() * intensity));
newColor.setGreen(static_cast<int>(accentColor.green() * intensity));
newColor.setBlue(static_cast<int>(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<QString, int> 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<DefinitionLoadResult> 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);
return a.exec();
}