From e0c040f66e6178d99e00269eff08d15ee9158ce1 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 17 Jan 2025 00:23:50 -0500 Subject: [PATCH] Uplifted tf out of the ui. Minecraft nbt style --- Data.qrc | 12 + FastFile_WaW.pro | 13 +- FastFile_WaW.pro.user | 8 +- XPlor.ico | Bin 0 -> 1486 bytes aboutdialog.cpp | 14 + aboutdialog.h | 22 + aboutdialog.ui | 241 ++++++ compression.h | 34 + data/images/XPlor.png | Bin 0 -> 1755 bytes data/images/copy.svg | 1 + data/images/cut.svg | 1 + data/images/multiple.png | Bin 0 -> 136462 bytes data/images/new_file.svg | 1 + data/images/open_file.svg | 1 + data/images/open_folder.svg | 1 + data/images/paste.svg | 1 + data/images/refresh.svg | 1 + data/images/save.svg | 1 + enums.h | 19 + ffparser.h | 103 +++ mainwindow.cpp | 1277 +++++--------------------------- mainwindow.h | 111 +-- mainwindow.ui | 1395 +++++++++-------------------------- modelviewer.cpp | 73 ++ modelviewer.h | 53 ++ structs.h | 110 +++ utils.h | 65 ++ zfparser.h | 523 +++++++++++++ 28 files changed, 1850 insertions(+), 2231 deletions(-) create mode 100644 XPlor.ico create mode 100644 aboutdialog.cpp create mode 100644 aboutdialog.h create mode 100644 aboutdialog.ui create mode 100644 compression.h create mode 100644 data/images/XPlor.png create mode 100644 data/images/copy.svg create mode 100644 data/images/cut.svg create mode 100644 data/images/multiple.png create mode 100644 data/images/new_file.svg create mode 100644 data/images/open_file.svg create mode 100644 data/images/open_folder.svg create mode 100644 data/images/paste.svg create mode 100644 data/images/refresh.svg create mode 100644 data/images/save.svg create mode 100644 ffparser.h create mode 100644 modelviewer.cpp create mode 100644 modelviewer.h create mode 100644 zfparser.h diff --git a/Data.qrc b/Data.qrc index 0325361..0372e66 100644 --- a/Data.qrc +++ b/Data.qrc @@ -21,4 +21,16 @@ data/d3dbsp/asset_viewer.d3dbsp data/d3dbsp/barebones.d3dbsp + + data/images/XPlor.png + data/images/copy.svg + data/images/cut.svg + data/images/new_file.svg + data/images/open_file.svg + data/images/open_folder.svg + data/images/paste.svg + data/images/refresh.svg + data/images/save.svg + data/images/multiple.png + diff --git a/FastFile_WaW.pro b/FastFile_WaW.pro index c155886..2ebf94b 100644 --- a/FastFile_WaW.pro +++ b/FastFile_WaW.pro @@ -1,6 +1,7 @@ QT += core gui 3dcore 3drender 3dinput 3dextras greaterThan(QT_MAJOR_VERSION, 4): QT += widgets +RC_ICONS = XPlor.ico CONFIG += c++17 @@ -13,16 +14,24 @@ CONFIG += c++17 #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 SOURCES += \ + aboutdialog.cpp \ main.cpp \ - mainwindow.cpp + mainwindow.cpp \ + modelviewer.cpp HEADERS += \ + aboutdialog.h \ + compression.h \ enums.h \ + ffparser.h \ mainwindow.h \ + modelviewer.h \ structs.h \ - utils.h + utils.h \ + zfparser.h FORMS += \ + aboutdialog.ui \ mainwindow.ui # Default rules for deployment. diff --git a/FastFile_WaW.pro.user b/FastFile_WaW.pro.user index 4e96dc1..7fd21e9 100644 --- a/FastFile_WaW.pro.user +++ b/FastFile_WaW.pro.user @@ -1,6 +1,6 @@ - + EnvironmentId @@ -244,10 +244,10 @@ false -e cpu-cycles --call-graph "dwarf,4096" -F 250 - + FastFile_WaW2 Qt4ProjectManager.Qt4RunConfiguration: - C:/Users/njohnson/Projects/FastFile_WaW/FastFile_WaW.pro - false + C:/Users/njohnson/Projects/XPlor/FastFile_WaW.pro + true true true true diff --git a/XPlor.ico b/XPlor.ico new file mode 100644 index 0000000000000000000000000000000000000000..68c58cd1077db18572af8d3d5a6ebd61de6509b9 GIT binary patch literal 1486 zcmV;<1u^;n0096206;(h0096X0JsGJ02TlM0EtjeM-2)Z3IG5A4M|8uQUCw|KmY&$ zKnMl^0063Kaozv`1%F9IK~#90?VCMp97PnzpHKKhHcsRYk%G7g2~HFVT9gKXA`2S& z2%5ktT?iO_mx%Uv~tjyTlR8@wyx5aBwOLjRodFH z5e6h^r5{q1)q)m2qYO&WE3K9;)^&z;XR#H&!N-9(#YdE{B)#Y3T6al#Md@|xYpnFL zuBDu|!p9?ViYJwu85J7JN=ex#t!;!})=^@ed2GE&U+d)iCX_x<{!%8C=aoK(P8;c? z?g`fQAg8Tm`dTM{D|d4$HNy+a&nkzN9%rrxx(-u!S+WoPFn8%|o!o0uakzys#GV#6 zi((gbO-lX|TH8_g;wox(1Q-yKeoTuEXh8y&?PF|ECmv&j1X>sJ~ z!%D%z`2x$H6L%vr+$oFnwNCn#cO*zHT~>;`{I&%1_$j43!TD~Mnc+Pfj%`_`kNu4d zz~%z^GB!Kb5kb4F@}}~cqFcwxCyLfmd1k8&=I9D9}X%Choph!tKB8H(YDo#mMFf<)JZUS1nx znfs<;FHl!l_cC?wO?Mo&V;U{fr-Nb{ns+O&D;JeNC1Erii*Y_|=y9cctNcS{O&M1X zC{K_Bytc+N)q9So=suR->#%)Jc}*;z2N=OZgdvH%dqNTxquB-q8WxmMWhmM!zbezp z9x}~q)zLP@dlpi3&n7oOnYQnvFS(Mk)Nr(vVkEevyrm3o-DlQ>&&i`XKghC|Ec7nL zJKjwAjg`cNWY)q^%;7~kHWW4V4yiWm5X&q|{k*pv0WK1-SZSD&p0qS}g}sWQ-Cb{Z z_1Y-Q*y=l3-}A0g@K}l6lpsvSMS(`>M!s@X83-9az-uK-9VQBd!mOF|E;nJZQoAj! zCMpP5MM+<@OwY}+d_O7jnoZB6j<+QeUN@w5+wfR14qXG( zFz9t6H_ABuYWSR8TnaeFaR$kG6h)2p$I z^kuh=<4#|W;>G4RzZ~794_D~ZNLm9pc|DH5E?JLuB_Urr|K8@JgYL3og#YOHMK01s4v3Gw-5{6P6#T=deNLi5R o(ODe(#h&7238qa)I#MJ510sPAg_C``PXGV_07*qoM6N<$f*y#*$^ZZW literal 0 HcmV?d00001 diff --git a/aboutdialog.cpp b/aboutdialog.cpp new file mode 100644 index 0000000..c3ba1b4 --- /dev/null +++ b/aboutdialog.cpp @@ -0,0 +1,14 @@ +#include "aboutdialog.h" +#include "ui_aboutdialog.h" + +AboutDialog::AboutDialog(QWidget *parent) + : QDialog(parent) + , ui(new Ui::AboutDialog) +{ + ui->setupUi(this); +} + +AboutDialog::~AboutDialog() +{ + delete ui; +} diff --git a/aboutdialog.h b/aboutdialog.h new file mode 100644 index 0000000..4474ece --- /dev/null +++ b/aboutdialog.h @@ -0,0 +1,22 @@ +#ifndef ABOUTDIALOG_H +#define ABOUTDIALOG_H + +#include + +namespace Ui { +class AboutDialog; +} + +class AboutDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AboutDialog(QWidget *parent = nullptr); + ~AboutDialog(); + +private: + Ui::AboutDialog *ui; +}; + +#endif // ABOUTDIALOG_H diff --git a/aboutdialog.ui b/aboutdialog.ui new file mode 100644 index 0000000..27665f5 --- /dev/null +++ b/aboutdialog.ui @@ -0,0 +1,241 @@ + + + AboutDialog + + + + 0 + 0 + 350 + 200 + + + + + 350 + 200 + + + + + 350 + 200 + + + + About XPlor + + + + + + + + + 80 + 80 + + + + + 80 + 80 + + + + + + + :/images/data/images/XPlor.png + + + true + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 0 + 0 + + + + + Roboto + 16 + false + + + + XPlor v1.5 + + + + + + + + 0 + 0 + + + + + Roboto + + + + Copyright © 2024 RedLine Solutions LLC + + + + + + + + 0 + 0 + + + + + Roboto + + + + For more, check out redline.llc + + + + + + + Qt::Orientation::Vertical + + + QSizePolicy::Policy::Fixed + + + + 20 + 10 + + + + + + + + + 0 + 0 + + + + + Roboto + + + + With Help From: + + + + + + + + 0 + 0 + + + + + Roboto + + + + - Paging Red + + + + + + + + 0 + 0 + + + + + Roboto + + + + - ISOCheated + + + + + + + + 0 + 0 + + + + + Roboto + + + + - SureShotIan + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + diff --git a/compression.h b/compression.h new file mode 100644 index 0000000..7f97f83 --- /dev/null +++ b/compression.h @@ -0,0 +1,34 @@ +#ifndef COMPRESSION_H +#define COMPRESSION_H + +#include "utils.h" +#include "QtZlib/zlib.h" + +#include +#include + +class Compressor { +public: + static QByteArray DecompressZLIB(QByteArray compressedData) { + QByteArray decompressedData; + uLongf decompressedSize = compressedData.size() * 4; + decompressedData.resize(static_cast(decompressedSize)); + + Bytef *destination = reinterpret_cast(decompressedData.data()); + uLongf *destLen = &decompressedSize; + const Bytef *source = reinterpret_cast(compressedData.data()); + uLong sourceLen = compressedData.size(); + + int result = uncompress(destination, destLen, source, sourceLen); + + if (result == Z_OK) { + decompressedData.resize(static_cast(decompressedSize)); + } else { + decompressedData.clear(); + qDebug() << QString("In DecompressZLIB: %1").arg(Utils::ZLibErrorToString(result)).toLatin1(); + } + return decompressedData; + } +}; + +#endif // COMPRESSION_H diff --git a/data/images/XPlor.png b/data/images/XPlor.png new file mode 100644 index 0000000000000000000000000000000000000000..babe14dfd90eaffbe2ec78626b98f4f0e7dccc59 GIT binary patch literal 1755 zcmd^9_g52E6uuFGgcU`oCV*fqBQmup3RG4Ci3DURB4`Aoph6p#Mga*4BFeCp$`T^Q zVg(^U!iNsssMmG ziHP@+qvJv&738z9KIyd_5Yawv&WPT7W{Glvq7eh40aRBkGy?JqR>?24dXfDI^27gO ztpT6_yIQ7}I1ahrD19{;abAC9<~*@Kre{^Y9oCUK+qsH4@&*k6uvgKlYQa+p_-Z{cUc# zv(9FUC0bZAX>Gv|FDLkl^9(O}nhPA=NW= z?P1uvBj;rC1i>)2@(%{aUqy)U-Q`fcgs7IJwFgrM!fY-s1?5yz;znh(UXJeI6T1}J z_kZ7eDJ}BiJWsmgv5V3&M(KI4V&$SlC5AyrlV6R7@~W1o0kdt;`wr3YC{i%{s_tBmG^+UCrvT{1_hc%o=yk`8y{BR_m z(4iPQ++CPUdm+gsm{rVNe0ZvnL&{;NG6dzR6sFicKvytb9}=^f>fsV`KAT-<<$a0H z;a1~rVA*Ac+-*kRyfRickZyc7W_)XS=GsbyjN{a$f}2|FDHin7qt;j6vu^uMFyHYH z>^>QyCkfxrwlcKu&QzfY6P(uHY(=%D%`sfZWMmIl)MJ~WG8Dt2-xf14-opt|x0x?a z?qT!LcVigm@o&1y#@8|~&H9*P&T32&I{V0CjKH{Ym3@VuH5&QwK`4K~E=uKCZ-wme znS-lo2d=nL%)91fVO(JWNg@or{$QTlFn#Y_vZX|GH7*9VrKq7Ein4SaX9J%)k_l>l zriS0HeVxc|5qW1w-r{x>5$rH&LWYgfpuU(Y%GJFar_-SmOg?rT_ehX1XZgW)w$^{^ zP@j6g3NxC^TU+A9IzaV`yTHPph=^<5Xzya#yLX7+Bnl$+Nm@JjZzTR_f^6y|!d>m+ zZdP0;sJ4mILrrS#912cY|6bh3YhoVSloB{mFn9ZnM_1c3Y4L2>IfvW=?1s8PF-@Hl zyESb9QRN^^U>IUvCwsYbyF1@LAd4=0UL5{z3ss?zk9aumDAnfPTVXVY27`|)@hp>k z*T0-K&4+`|~=7;%ehfhpe?>ZPe0h3mJqkm6h9UEUB)~mD+ ziqxLRq?N(QROU`Gozkn^aHakkedX4DNd3rUbbz4E{v~T9&SAXyeQ@ARgJt~7WzFx9 z6#mQ+O4n4FQfytr5&aFDB3Eu#ri-3uv{Nb4q`meLL`!z6REr?2Y&KQ)0V&H|4{dst rH+*+LEqUB7Ggj?>*^XD&EP@#}E_)g&b=Gz*3!fQ@;ECrr?_>T0oBfY- literal 0 HcmV?d00001 diff --git a/data/images/copy.svg b/data/images/copy.svg new file mode 100644 index 0000000..f09d902 --- /dev/null +++ b/data/images/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/images/cut.svg b/data/images/cut.svg new file mode 100644 index 0000000..c563fb2 --- /dev/null +++ b/data/images/cut.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/images/multiple.png b/data/images/multiple.png new file mode 100644 index 0000000000000000000000000000000000000000..a18bbb4102fab799f66a07239eb3ac7144920598 GIT binary patch literal 136462 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

VFpT&e0Uug9j-QiK;W`94}5__ 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 literal 0 HcmV?d00001 diff --git a/data/images/new_file.svg b/data/images/new_file.svg new file mode 100644 index 0000000..7c19795 --- /dev/null +++ b/data/images/new_file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/images/open_file.svg b/data/images/open_file.svg new file mode 100644 index 0000000..ee0ee85 --- /dev/null +++ b/data/images/open_file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/images/open_folder.svg b/data/images/open_folder.svg new file mode 100644 index 0000000..3e7b372 --- /dev/null +++ b/data/images/open_folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/images/paste.svg b/data/images/paste.svg new file mode 100644 index 0000000..bb4e796 --- /dev/null +++ b/data/images/paste.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/images/refresh.svg b/data/images/refresh.svg new file mode 100644 index 0000000..4e6d30c --- /dev/null +++ b/data/images/refresh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/images/save.svg b/data/images/save.svg new file mode 100644 index 0000000..4f76105 --- /dev/null +++ b/data/images/save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/enums.h b/enums.h index 415bf63..46f68fc 100644 --- a/enums.h +++ b/enums.h @@ -98,4 +98,23 @@ enum BSPVERSION_TYPE BSPVERSION_COD_BO = 45 }; +enum FF_COMPANY { + COMPANY_NONE = 0x00, + COMPANY_INFINITY_WARD = 0x01, + COMPANY_TREYARCH = 0x02, + COMPANY_SLEDGEHAMMER = 0x03, + COMPANY_NEVERSOFT = 0x04 +}; + +enum FF_FILETYPE { + FILETYPE_NONE = 0x00, + FILETYPE_FAST_FILE = 0x01 +}; + +enum FF_SIGNAGE { + SIGNAGE_NONE = 0x00, + SIGNAGE_SIGNED = 0x01, + SIGNAGE_UNSIGNED = 0x02 +}; + #endif // ENUMS_H diff --git a/ffparser.h b/ffparser.h new file mode 100644 index 0000000..03056e2 --- /dev/null +++ b/ffparser.h @@ -0,0 +1,103 @@ +#ifndef FFPARSER_H +#define FFPARSER_H + +#include "enums.h" +#include "structs.h" + +#include +#include + +class FastFileParser { +public: + static FF_COMPANY ParseFFCompany(QDataStream *afastFileStream) { + // Check for null datastream ptr + if (!afastFileStream) { return COMPANY_NONE; } + // Parse company + QByteArray companyData(2, Qt::Uninitialized); + afastFileStream->readRawData(companyData.data(), 2); + if (companyData == "IW") { + qDebug() << "Company found: 'INFINITY_WARD'"; + return COMPANY_INFINITY_WARD; + } else if (companyData == "TA") { + qDebug() << "Company found: 'TREYARCH'"; + return COMPANY_TREYARCH; + } else if (companyData == "Sl") { + qDebug() << "Company found: 'SLEDGEHAMMER'"; + return COMPANY_SLEDGEHAMMER; + } else if (companyData == "NX") { + qDebug() << "Company found: 'NEVERSOFT'"; + return COMPANY_NEVERSOFT; + } else { + qDebug() << QString("Failed to find company, found '%1'!").arg(companyData); + } + return COMPANY_NONE; + } + + static FF_FILETYPE ParseFFFileType(QDataStream *afastFileStream) { + // Parse filetype + QByteArray fileTypeData(2, Qt::Uninitialized); + afastFileStream->readRawData(fileTypeData.data(), 2); + if (fileTypeData == "ff") { + qDebug() << "File type found: 'FAST_FILE'"; + return FILETYPE_FAST_FILE; + } else { + qDebug() << "Failed to find file type!"; + } + return FILETYPE_NONE; + } + + static FF_SIGNAGE ParseFFSignage(QDataStream *afastFileStream) { + // Parse filetype + QByteArray signedData(1, Qt::Uninitialized); + afastFileStream->readRawData(signedData.data(), 1); + if (signedData == "u") { + qDebug() << "Found valid signage: Unsigned"; + return SIGNAGE_UNSIGNED; + } else if (signedData == "0") { + qDebug() << "Found valid signage: Signed"; + return SIGNAGE_SIGNED; + } else { + qDebug() << "Failed to determine signage of fastfile!"; + } + return SIGNAGE_NONE; + } + + static QString ParseFFMagic(QDataStream *afastFileStream) { + // Parse magic + QByteArray magicData(3, Qt::Uninitialized); + afastFileStream->readRawData(magicData.data(), 3); + if (magicData == "100") { + qDebug() << QString("Found valid magic: '%1'").arg(magicData); + return magicData; + } else { + qDebug() << "Magic invalid!"; + } + return ""; + } + + static quint32 ParseFFVersion(QDataStream *afastFileStream) { + // Parse version + quint32 version; + *afastFileStream >> version; + qDebug() << "Version:" << version; + if (version == 387) { + qDebug() << QString("Found valid version: '%1'").arg(version); + return 387; + } else { + qDebug() << "Version invalid!"; + } + return -1; + } + + static FastFile ParseFFHeader(QDataStream *afastFileStream) { + FastFile fastFile; + fastFile.company = ParseFFCompany(afastFileStream); + fastFile.fileType = ParseFFFileType(afastFileStream); + fastFile.signage = ParseFFSignage(afastFileStream); + fastFile.magic = ParseFFMagic(afastFileStream); + fastFile.version = ParseFFVersion(afastFileStream); + return fastFile; + } +}; + +#endif // FFPARSER_H diff --git a/mainwindow.cpp b/mainwindow.cpp index 8cd2ff6..0555716 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,4 +1,5 @@ #include "mainwindow.h" +#include "qheaderview.h" #include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) @@ -15,98 +16,37 @@ MainWindow::MainWindow(QWidget *parent) mDiskLumpCount = 0; mDiskLumpOrder = QVector(); mLumps = QMap(); + mTreeWidget = new QTreeWidget(); + mRootItem = mTreeWidget->invisibleRootItem(); + mScriptEditor = new QPlainTextEdit(); + mModelViewer = new ModelViewer(); - connect(ui->treeWidget_Scripts, &QTreeWidget::itemSelectionChanged, this, &MainWindow::ScriptSelected); - connect(ui->comboBox_StringTable, &QComboBox::currentTextChanged, this, &MainWindow::StrTableSelected); + // Connect Help > About dialog + connect(ui->actionAbout, &QAction::triggered, this, [this](bool checked) { + Q_UNUSED(checked); - // Initialize Asset Index Table - ui->tableWidget_Index->setColumnCount(3); - ui->tableWidget_Index->setHorizontalHeaderLabels({"Asset Type", "Asset Name", "Asset Count"}); - ui->tableWidget_Index->verticalHeader()->setVisible(false); - ui->tableWidget_Index->setEditTriggers(QAbstractItemView::NoEditTriggers); - ui->tableWidget_Index->setSelectionBehavior(QAbstractItemView::SelectRows); - ui->tableWidget_Index->setSelectionMode(QAbstractItemView::SingleSelection); - ui->tableWidget_Index->setShowGrid(false); - ui->tableWidget_Index->setStyleSheet("QTableView {selection-background-color: red;}"); + AboutDialog *aboutDialog = new AboutDialog(this); + aboutDialog->exec(); - // Initialize Asset Order Table - ui->tableWidget_Order->setColumnCount(3); - ui->tableWidget_Order->setHorizontalHeaderLabels({"Asset Type", "Asset Name", "Asset Count"}); - ui->tableWidget_Order->verticalHeader()->setVisible(false); - ui->tableWidget_Order->setEditTriggers(QAbstractItemView::NoEditTriggers); - ui->tableWidget_Order->setSelectionBehavior(QAbstractItemView::SelectRows); - ui->tableWidget_Order->setSelectionMode(QAbstractItemView::SingleSelection); - ui->tableWidget_Order->setShowGrid(false); - ui->tableWidget_Order->setStyleSheet("QTableView {selection-background-color: red;}"); + delete aboutDialog; + }); - Qt3DExtras::Qt3DWindow *view = new Qt3DExtras::Qt3DWindow(); - view->defaultFrameGraph()->setClearColor(QColor(QRgb(0x4d4d4f))); + connect(ui->actionOpen_Fast_File, &QAction::triggered, this, [this](bool checked) { + Q_UNUSED(checked); + OpenFastFile(); + }); + connect(ui->actionOpen_Zone_File, &QAction::triggered, this, [this](bool checked) { + Q_UNUSED(checked); + OpenZoneFile(); + }); - QWidget *container = QWidget::createWindowContainer(view); - QSize screenSize = view->screen()->size(); - container->setMinimumSize(QSize(200, 100)); - container->setMaximumSize(screenSize); - QHBoxLayout *hLayout = new QHBoxLayout(ui->frame_Scene); - QVBoxLayout *vLayout = new QVBoxLayout(); - vLayout->setAlignment(Qt::AlignTop); - hLayout->addWidget(container, 1); - hLayout->addLayout(vLayout); + QDockWidget *treeDockWidget = new QDockWidget(this); + mTreeWidget->header()->hide(); + treeDockWidget->setWidget(mTreeWidget); + addDockWidget(Qt::LeftDockWidgetArea, treeDockWidget); - // Root entity - Qt3DCore::QEntity *rootEntity = new Qt3DCore::QEntity(); - - // Camera - Qt3DRender::QCamera *cameraEntity = view->camera(); - - cameraEntity->lens()->setPerspectiveProjection(45.0f, 16.0f/9.0f, 0.1f, 1000.0f); - cameraEntity->setPosition(QVector3D(0, 0, 50.0f)); // Move farther along Z-axis - cameraEntity->setUpVector(QVector3D(0, 1, 0)); - cameraEntity->setViewCenter(QVector3D(0, 0, 0)); - - Qt3DCore::QEntity *lightEntity = new Qt3DCore::QEntity(rootEntity); - Qt3DRender::QPointLight *light = new Qt3DRender::QPointLight(lightEntity); - light->setColor("white"); - light->setIntensity(1); - lightEntity->addComponent(light); - - Qt3DCore::QTransform *lightTransform = new Qt3DCore::QTransform(lightEntity); - lightTransform->setTranslation(cameraEntity->position()); - lightEntity->addComponent(lightTransform); - - // For camera controls - Qt3DExtras::QFirstPersonCameraController *camController = new Qt3DExtras::QFirstPersonCameraController(rootEntity); - camController->setCamera(cameraEntity); - - // Set root object of the scene - view->setRootEntity(rootEntity); - - // Load custom 3D model - Qt3DRender::QMesh *customMesh = new Qt3DRender::QMesh(); - customMesh->setSource(QUrl::fromLocalFile(":/obj/data/obj/defaultactor_LOD0.obj")); - - // Adjust the model transformation - Qt3DCore::QTransform *customTransform = new Qt3DCore::QTransform(); - customTransform->setRotationX(-90); - customTransform->setRotationY(-90); - - // Keep translation if necessary - customTransform->setTranslation(QVector3D(0.0f, -100.0f, -200.0f)); - - Qt3DExtras::QNormalDiffuseMapMaterial *customMaterial = new Qt3DExtras::QNormalDiffuseMapMaterial(); - - Qt3DRender::QTextureLoader *normalMap = new Qt3DRender::QTextureLoader(); - normalMap->setSource(QUrl::fromLocalFile(":/obj/data/obj/normalmap.png")); - customMaterial->setNormal(normalMap); - - Qt3DRender::QTextureLoader *diffuseMap = new Qt3DRender::QTextureLoader(); - diffuseMap->setSource(QUrl::fromLocalFile(":/obj/data/obj/diffusemap.png")); - customMaterial->setDiffuse(diffuseMap); - - Qt3DCore::QEntity *m_torusEntity = new Qt3DCore::QEntity(rootEntity); - m_torusEntity->addComponent(customMesh); - m_torusEntity->addComponent(customMaterial); - m_torusEntity->addComponent(customTransform); + setCentralWidget(mScriptEditor); LoadFile_D3DBSP(":/d3dbsp/data/d3dbsp/barebones.d3dbsp"); @@ -118,72 +58,8 @@ MainWindow::~MainWindow() { } void MainWindow::Reset() { - // Reset tabwidget to 'General' tab - ui->tabWidget->setCurrentIndex(0); - - // Reset 'General' tab fields - ui->lineEdit_FastFile->clear(); - ui->comboBox_Company->setCurrentIndex(0); - ui->comboBox_FileType->setCurrentIndex(0); - ui->checkBox_Signed->setChecked(false); - ui->lineEdit_Magic->clear(); - ui->spinBox_Magic->clear(); - ui->spinBox_TagCount->clear(); - ui->spinBox_FileSize->clear(); - ui->spinBox_RecordCount->clear(); - - // Reset 'Unknowns' tab fields - ui->lineEdit_U1->clear(); - ui->spinBox_U1->clear(); - ui->lineEdit_U2->clear(); - ui->spinBox_U2->clear(); - ui->lineEdit_U3->clear(); - ui->spinBox_U3->clear(); - ui->lineEdit_U4->clear(); - ui->spinBox_U4->clear(); - ui->lineEdit_U5->clear(); - ui->spinBox_U5->clear(); - ui->lineEdit_U6->clear(); - ui->spinBox_U6->clear(); - ui->lineEdit_U7->clear(); - ui->spinBox_U7->clear(); - ui->lineEdit_U8->clear(); - ui->spinBox_U8->clear(); - ui->lineEdit_U9->clear(); - ui->spinBox_U9->clear(); - ui->lineEdit_U10->clear(); - ui->spinBox_U10->clear(); - ui->lineEdit_U11->clear(); - ui->spinBox_U11->clear(); - - // Reset 'Tags' tab fields - ui->listWidget_Tags->clear(); - - // Reset 'Localized Strings' tab fields - ui->listWidget_LocalString->clear(); - - // Reset 'Asset Index/Order' tab fields - ui->tableWidget_Index->clear(); - ui->tableWidget_Order->clear(); - - // Reset 'Raw Files' tab fields - ui->treeWidget_Scripts->clear(); - ui->plainTextEdit_Scripts->clear(); - - // Reset 'Tech Sets' tab fields - ui->listWidget_TechSets->clear(); - - // Reset 'Zone Dump' tab fields - ui->spinBox_DumpIndex->clear(); - ui->comboBox_DumpAsset->setCurrentIndex(0); - ui->plainTextEdit_ZoneDump->clear(); - - // Reset 'String Tables' tab fields - ui->comboBox_StringTable->setCurrentIndex(0); - ui->tableWidget_StringTable->clear(); - - // Reset '3D Scene' tab fields - ui->treeWidget_Models->clear(); + // Clear data tree + mTreeWidget->clear(); // Reset class vars mTypeMap.clear(); @@ -195,62 +71,73 @@ void MainWindow::Reset() { mStrTableMap.clear(); } -void MainWindow::StrTableSelected(QString aStrTableName) { - ui->tableWidget_StringTable->clear(); +/* + OpenFastFile() - ui->tableWidget_StringTable->setColumnCount(2); - int entryIndex = 0; - for (auto strTableEntry : mStrTableMap[aStrTableName]) { - ui->tableWidget_StringTable->insertRow(ui->tableWidget_StringTable->rowCount() + 1); - ui->tableWidget_StringTable->setItem(entryIndex, 0, new QTableWidgetItem(strTableEntry.first)); - ui->tableWidget_StringTable->setItem(entryIndex, 1, new QTableWidgetItem(Utils::AssetTypeToString(strTableEntry.second))); + Opens a file dialog in the steam folder, + and opens the selected file. +*/ +bool MainWindow::OpenFastFile(const QString aFastFilePath) { + if (aFastFilePath.isEmpty()) { return false; } - entryIndex++; - } -} + // Reset dialog before opening new file + Reset(); -void MainWindow::ScriptSelected() { - QTreeWidgetItem *selectedItem = ui->treeWidget_Scripts->selectedItems()[0]; - if (!selectedItem) { - qDebug() << "Attempted to load invalid tree item!"; - return; + // Add fast file as tree widget root + const QString fastFileStem = aFastFilePath.split('/').last(); + QTreeWidgetItem *fastFileItem = new QTreeWidgetItem(mRootItem); + fastFileItem->setText(0, fastFileStem); + mRootItem = fastFileItem; + + // Check fastfile can be read + QFile *fastFileObj = new QFile(aFastFilePath); + if (!fastFileObj->open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, "Warning!", QString("Failed to open FastFile: %1!") + .arg(aFastFilePath)); + return false; } - const QString itemName = selectedItem->text(0); - const QStringList scriptExts = {"gsc", "csc", "atr", "shock", "vision", "rmb"}; - if (!scriptExts.contains(itemName.split('.').last())) { - qDebug() << QString("Attempted to parse invalid raw file: %1!").arg(itemName); - return; + // Decompress fastfile and close + const QByteArray fastFileData = fastFileObj->readAll(); + const QByteArray decompressedData = Compressor::DecompressZLIB(fastFileData); + + // Open zone file as little endian stream + QDataStream fastFileStream(fastFileData); + fastFileStream.setByteOrder(QDataStream::LittleEndian); + + // Parse data from fast file header + FastFile fastFile = FastFileParser::ParseFFHeader(&fastFileStream); + + QTreeWidgetItem *metaDataItem = new QTreeWidgetItem(mRootItem); + metaDataItem->setText(0, "Metadata"); + + QTreeWidgetItem *companyItem = new QTreeWidgetItem(metaDataItem); + companyItem->setText(0, "Company: " + Utils::CompanyEnumToStr(fastFile.company)); + QTreeWidgetItem *fileTypeItem = new QTreeWidgetItem(metaDataItem); + fileTypeItem->setText(0, "File Type: " + Utils::FileTypeEnumToStr(fastFile.fileType)); + QTreeWidgetItem *signageItem = new QTreeWidgetItem(metaDataItem); + signageItem->setText(0, "Signage: " + Utils::SignageEnumToStr(fastFile.signage)); + QTreeWidgetItem *magicItem = new QTreeWidgetItem(metaDataItem); + magicItem->setText(0, "Magic: " + fastFile.magic); + QTreeWidgetItem *versionItem = new QTreeWidgetItem(metaDataItem); + versionItem->setText(0, "Version: " + QString::number(fastFile.version)); + + const QString zoneFilePath = fastFileObj->fileName().replace(".ff", ".zone"); + fastFileObj->close(); + + // Check zone file is writeable + QFile *zoneFile = new QFile(zoneFilePath); + if (!zoneFile->open(QIODevice::ReadWrite)) { + qDebug() << QString("Zone file could not be written to: '%1'").arg(zoneFilePath); + return false; } - ui->plainTextEdit_Scripts->clear(); - for (auto [scriptName, scriptContents] : mRawFileMap.asKeyValueRange()) { - if (scriptName.contains(itemName)) { - ui->plainTextEdit_Scripts->setPlainText(scriptContents); - return; - } - } -} + // Write zone data + zoneFile->write(decompressedData); + zoneFile->close(); -QByteArray MainWindow::DecompressZLIB(QByteArray compressedData) { - QByteArray decompressedData; - uLongf decompressedSize = compressedData.size() * 4; - decompressedData.resize(static_cast(decompressedSize)); - - Bytef *destination = reinterpret_cast(decompressedData.data()); - uLongf *destLen = &decompressedSize; - const Bytef *source = reinterpret_cast(compressedData.data()); - uLong sourceLen = compressedData.size(); - - int result = uncompress(destination, destLen, source, sourceLen); - - if (result == Z_OK) { - decompressedData.resize(static_cast(decompressedSize)); - } else { - decompressedData.clear(); - qDebug() << QString("In DecompressZLIB: %1").arg(Utils::ZLibErrorToString(result)).toLatin1(); - } - return decompressedData; + // Open zone file after decompressing ff and writing + return OpenZoneFile(zoneFilePath); } /* @@ -259,29 +146,13 @@ QByteArray MainWindow::DecompressZLIB(QByteArray compressedData) { Opens a file dialog in the steam folder, and opens the selected file. */ -QFile* MainWindow::OpenFastFile() { - // Reset dialog before opening new file - Reset(); - - // Open file dialog to steam apps - const QString steamPath = "C:/Program Files (x86)/Steam/steamapps/common/Call of Duty World at War/zone/english/"; - const QString fastFilePath = QFileDialog::getOpenFileName(this, "Open FastFile", steamPath, "FastFile (*.ff);;All Files (*.*)"); - if (!QFile::exists(fastFilePath)) { - QMessageBox::warning(this, "Warning!", QString("%1 does not exist!.").arg(fastFilePath)); - return nullptr; +bool MainWindow::OpenFastFile() { + const QString fastFileName = Utils::GetOpenFastFileName(); + if (!OpenFastFile(fastFileName)) { + qDebug() << "Failed to open Fast file!"; + return false; } - ui->lineEdit_FastFile->setText(fastFilePath); - - const QString fastFileStem = fastFilePath.split('/').last(); - setWindowTitle(QString("FastFile Wizard - %1").arg(fastFileStem)); - - // Check fastfile can be read - QFile *fastFile = new QFile(fastFilePath); - if (!fastFile->open(QIODevice::ReadOnly)) { - QMessageBox::warning(this, "Warning!", QString("%1 could not be read!.").arg(fastFilePath)); - return nullptr; - } - return fastFile; + return true; } /* @@ -290,884 +161,145 @@ QFile* MainWindow::OpenFastFile() { Opens a file dialog in the steam folder, and opens the selected file. */ -QFile* MainWindow::OpenZoneFile() { +bool MainWindow::OpenZoneFile(const QString aZoneFilePath) { + if (aZoneFilePath.isEmpty()) { return false; } + // Reset dialog before opening new file - Reset(); + //Reset(); - // Open file dialog to steam apps - const QString steamPath = "C:/Program Files (x86)/Steam/steamapps/common/Call of Duty World at War/zone/english/"; - const QString zoneFilePath = QFileDialog::getOpenFileName(this, "Open ZoneFile", steamPath, "ZoneFile (*.zone);;All Files (*.*)"); - if (!QFile::exists(zoneFilePath)) { - QMessageBox::warning(this, "Warning!", QString("%1 does not exist!.").arg(zoneFilePath)); - return nullptr; + //ui->lineEdit_ZoneFile->setText(zoneFilePath); + + const QString zoneFileStem = aZoneFilePath.split('/').last(); + QTreeWidgetItem *zoneItem = new QTreeWidgetItem(mRootItem); + zoneItem->setText(0, zoneFileStem); + mRootItem = zoneItem; + + //setWindowTitle(QString("FastFile Wizard - %1").arg(zoneFileStem)); + + // Check zone file can be read + QFile *zoneFileObj = new QFile(aZoneFilePath); + if (!zoneFileObj->open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, "Warning!", QString("%1 could not be read!.").arg(aZoneFilePath)); + return false; } - ui->lineEdit_ZoneFile->setText(zoneFilePath); + const QByteArray decompressedData = zoneFileObj->readAll(); - const QString zoneFileStem = zoneFilePath.split('/').last(); - setWindowTitle(QString("FastFile Wizard - %1").arg(zoneFileStem)); + // Open zone file as little endian stream + QDataStream zoneFileStream(decompressedData); + zoneFileStream.setByteOrder(QDataStream::LittleEndian); - // Check fastfile can be read - QFile *zoneFile = new QFile(zoneFilePath); - if (!zoneFile->open(QIODevice::ReadOnly)) { - QMessageBox::warning(this, "Warning!", QString("%1 could not be read!.").arg(zoneFilePath)); - return nullptr; - } - return zoneFile; -} + // Parse data from zone file header + ZoneFile zoneFile = ZoneFileParser::ParseZoneHeader(&zoneFileStream); -void MainWindow::ParseFFCompany(QDataStream *afastFileStream) { - // Check for null datastream ptr - if (!afastFileStream) { return; } - // Parse company - QByteArray companyData(2, Qt::Uninitialized); - afastFileStream->readRawData(companyData.data(), 2); - if (companyData == "IW") { - qDebug() << "Company found: 'INFINITY_WARD'"; - ui->comboBox_Company->setCurrentIndex(1); - } else if (companyData == "TA") { - qDebug() << "Company found: 'TREYARCH'"; - ui->comboBox_Company->setCurrentIndex(2); - } else if (companyData == "Sl") { - qDebug() << "Company found: 'SLEDGEHAMMER'"; - ui->comboBox_Company->setCurrentIndex(3); - } else if (companyData == "NX") { - qDebug() << "Company found: 'NEVERSOFT'"; - ui->comboBox_Company->setCurrentIndex(4); - } else { - qDebug() << QString("Failed to find company, found '%1'!").arg(companyData); - return; - } -} - -void MainWindow::ParseFFFileType(QDataStream *afastFileStream) { - // Parse filetype - QByteArray fileTypeData(2, Qt::Uninitialized); - afastFileStream->readRawData(fileTypeData.data(), 2); - if (fileTypeData == "ff") { - qDebug() << "File type found: 'FAST_FILE'"; - ui->comboBox_FileType->setCurrentIndex(1); - } else { - qDebug() << "Failed to find file type!"; - return; - } -} - -void MainWindow::ParseFFSignage(QDataStream *afastFileStream) { - // Parse filetype - QByteArray signedData(1, Qt::Uninitialized); - afastFileStream->readRawData(signedData.data(), 1); - if (signedData == "u") { - qDebug() << "Found valid signage: Unsigned"; - ui->checkBox_Signed->setChecked(false); - } else if (signedData == "0") { - qDebug() << "Found valid signage: Signed"; - ui->checkBox_Signed->setChecked(true); - } else { - qDebug() << "Failed to determine signage of fastfile!"; - return; - } -} - -void MainWindow::ParseFFMagic(QDataStream *afastFileStream) { - // Parse magic - QByteArray magicData(3, Qt::Uninitialized); - afastFileStream->readRawData(magicData.data(), 3); - if (magicData == "100") { - qDebug() << QString("Found valid magic: '%1'").arg(magicData); - ui->lineEdit_Magic->setText(magicData.toHex()); - ui->spinBox_Magic->setValue(magicData.toInt()); - } else { - qDebug() << "Magic invalid!"; - return; - } -} - -void MainWindow::ParseFFVersion(QDataStream *afastFileStream) { - // Parse version - quint32 version; - *afastFileStream >> version; - qDebug() << "Version:" << version; - if (version == 387) { - qDebug() << QString("Found valid version: '%1'").arg(version); - ui->spinBox_Version->setValue(version); - } else { - qDebug() << "Version invalid!"; - return; - } -} - -void MainWindow::ParseFFHeader(QFile *aFastFilePtr) { - // Open stream to fastfile - QDataStream afastFileStream(aFastFilePtr); - afastFileStream.setByteOrder(QDataStream::LittleEndian); - - ParseFFCompany(&afastFileStream); - ParseFFFileType(&afastFileStream); - ParseFFSignage(&afastFileStream); - ParseFFMagic(&afastFileStream); - ParseFFVersion(&afastFileStream); -} - -void MainWindow::ParseZoneHeader(QDataStream *aZoneFileStream) { - ParseZoneSize(aZoneFileStream); - ParseZoneUnknownsA(aZoneFileStream); - - ParseZoneTagCount(aZoneFileStream); - ParseZoneUnknownsB(aZoneFileStream); - - ParseZoneRecordCount(aZoneFileStream); - - if (mTagCount) { - ParseZoneUnknownsC(aZoneFileStream); - ParseZoneTags(aZoneFileStream); - } else { - aZoneFileStream->skipRawData(4); - } -} - -void MainWindow::ParseZoneSize(QDataStream *aZoneFileStream) { - // Byte 0-3: (unsigned int?) correlates to the fastfile's - // size after decompression minus 36 bytes (24h) - quint32 zoneFileSize; - *aZoneFileStream >> zoneFileSize; - if (zoneFileSize <= 0) { - qDebug() << "Tried to open empty zone file!"; - exit(-1); - } - zoneFileSize += 36; - ui->spinBox_FileSize->setValue(zoneFileSize); - - qDebug() << QString("Zone file size: '%1'").arg(zoneFileSize); -} - -/* - ParseZoneUnknownsA() - - Parses the 1st section of unknowns as hex vals and uint32s -*/ -void MainWindow::ParseZoneUnknownsA(QDataStream *aZoneFileStream) { - // Byte 4-7, 8-11, 12-15: unknown - QByteArray unknown1(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown1.data(), 4); - ui->lineEdit_U1->setText(unknown1.toHex()); - ui->spinBox_U1->setValue(unknown1.toUInt()); - - QByteArray unknown2(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown2.data(), 4); - ui->lineEdit_U2->setText(unknown2.toHex()); - ui->spinBox_U2->setValue(unknown2.toUInt()); - - QByteArray unknown3(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown3.data(), 4); - ui->lineEdit_U3->setText(unknown3.toHex()); - ui->spinBox_U3->setValue(unknown3.toUInt()); - - // Byte 16-19, 20-23: empty/unknown - QByteArray unknown4(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown4.data(), 4); - ui->lineEdit_U4->setText(unknown4.toHex()); - ui->spinBox_U4->setValue(unknown4.toUInt()); - - QByteArray unknown5(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown5.data(), 4); - ui->lineEdit_U5->setText(unknown5.toHex()); - ui->spinBox_U5->setValue(unknown5.toUInt()); - - // Byte 24-27: somehow related to the filesize, but smaller value - QByteArray unknown6(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown6.data(), 4); - ui->lineEdit_U6->setText(unknown6.toHex()); - ui->spinBox_U6->setValue(unknown6.toUInt()); - - // Byte 28-31, 32-35: unknown - QByteArray unknown7(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown7.data(), 4); - ui->lineEdit_U7->setText(unknown7.toHex()); - ui->spinBox_U7->setValue(unknown7.toUInt()); - - QByteArray unknown8(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown8.data(), 4); - ui->lineEdit_U8->setText(unknown8.toHex()); - ui->spinBox_U8->setValue(unknown8.toUInt()); - - qDebug() << QString("Unknowns A: '%1''%2''%3''%4''%5''%6''%7''%8'") - .arg(unknown1.toHex()) - .arg(unknown2.toHex()) - .arg(unknown3.toHex()) - .arg(unknown4.toHex()) - .arg(unknown5.toHex()) - .arg(unknown6.toHex()) - .arg(unknown7.toHex()) - .arg(unknown8.toHex()); -} - -/* - ParseZoneTagCount() - - Parses the number of string tags in the zone index -*/ -void MainWindow::ParseZoneTagCount(QDataStream *aZoneFileStream) { - // Byte 36-39: might indicate where the index record starts, - // calculation unknown - *aZoneFileStream >> mTagCount; - ui->spinBox_TagCount->setValue(mTagCount); - qDebug() << QString("Tag count: '%1'").arg(mTagCount); -} - -/* - ParseZoneRecordCount() - - Parses the number of records in the zone index -*/ -void MainWindow::ParseZoneRecordCount(QDataStream *aZoneFileStream) { - // Byte 44-47: (unsigned int) number of records - *aZoneFileStream >> mRecordCount; - ui->spinBox_RecordCount->setValue(mRecordCount); - qDebug() << QString("Record count: '%1'").arg(mRecordCount); -} - -/* - ParseZoneUnknownsB() - - Parses the 2nd section of unknowns as hex vals and uint32s -*/ -void MainWindow::ParseZoneUnknownsB(QDataStream *aZoneFileStream) { - // Byte 44-47: Unknown/empty? - QByteArray unknown9(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown9.data(), 4); - ui->lineEdit_U9->setText(unknown9.toHex()); - ui->spinBox_U9->setValue(unknown9.toUInt()); - - qDebug() << QString("Unknowns B: \n\t'%1'") - .arg(unknown9.toHex()); -} - -/* - ParseZoneUnknownsC() - - Parses the 3rd section of unknowns as hex vals and uint32s -*/ -void MainWindow::ParseZoneUnknownsC(QDataStream *aZoneFileStream) { - // Byte 40-43: Unknown/empty? - QByteArray unknown10(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown10.data(), 4); - ui->lineEdit_U10->setText(unknown10.toHex()); - ui->spinBox_U10->setValue(unknown10.toUInt()); - - // Byte 44-47: Unknown/empty? - QByteArray unknown11(4, Qt::Uninitialized); - aZoneFileStream->readRawData(unknown11.data(), 4); - ui->lineEdit_U11->setText(unknown11.toHex()); - ui->spinBox_U11->setValue(unknown11.toUInt()); - - qDebug() << QString("Unknowns C: \n\t'%1'\n\t'%2'") - .arg(unknown10.toHex()) - .arg(unknown11.toHex()); -} - -/* - ParseZoneTags() - - Parses the string tags ate the start of zone file -*/ -void MainWindow::ParseZoneTags(QDataStream *aZoneFileStream) { - // Byte 48-51: Repeated separators? ÿÿÿÿ x i - aZoneFileStream->skipRawData(4 * (mTagCount - 1)); - - // Parse tags/strings before index - QString zoneTag; - char zoneTagChar; - for (quint32 i = 0; i < mTagCount - 1; i++) { - *aZoneFileStream >> zoneTagChar; - while (zoneTagChar != 0) { - zoneTag += zoneTagChar; - *aZoneFileStream >> zoneTagChar; + if (zoneFile.tagCount) { + QTreeWidgetItem *tagsItem = new QTreeWidgetItem(mRootItem); + tagsItem->setText(0, QString("Tags [%1]") + .arg(zoneFile.tags.length())); + foreach (const QString tag, zoneFile.tags) { + QTreeWidgetItem *tagItem = new QTreeWidgetItem(tagsItem); + tagItem->setText(0, tag); } - ui->listWidget_Tags->addItem(zoneTag); - // qDebug() << "Tag: " << zoneTag; - zoneTag.clear(); } -} -/* - ParseZoneIndex() + QTreeWidgetItem *indexRecordsItem = new QTreeWidgetItem(mRootItem); + QTreeWidgetItem *assetsItem = new QTreeWidgetItem(mRootItem); - Parse the binary zone index data and populate table -*/ -void MainWindow::ParseZoneIndex(QDataStream *aZoneFileStream) { - // Don't parse if no records - if (!mRecordCount) { return; } + mRootItem = indexRecordsItem; - // Track past assets and counts - int consecutiveIndex = 0; - int consecutiveCount = 0; - QString lastAssetType = ""; + int consecutiveCount = 1; + QString lastRecord = ""; + zoneFile.records = ZoneFileParser::ParseZoneIndex(&zoneFileStream, zoneFile.recordCount); + indexRecordsItem->setText(0, QString("Index Records [%1]") + .arg(zoneFile.records.length())); - // Parse index & map found asset types - for (quint32 i = 0; i < mRecordCount; i++) { - // Skip record start - QByteArray rawAssetType(4, Qt::Uninitialized); - aZoneFileStream->readRawData(rawAssetType.data(), 4); - if (!mTypeMap.contains(rawAssetType.toHex())) { - mTypeMap[rawAssetType.toHex()] = 0; - } - mTypeMap[rawAssetType.toHex()]++; - mTypeOrder << rawAssetType.toHex(); - - // Skip separator - aZoneFileStream->skipRawData(4); - - // Get asset description from type - const QString assetType = rawAssetType.toHex(); - - // Set lastAsset as current if first run - if (lastAssetType.isEmpty()) { - lastAssetType = assetType; - } - - // Track counts or populate asset order table - if (lastAssetType == assetType) { - // Count consecutive assets + foreach (const QString record, zoneFile.records) { + if (lastRecord.isEmpty()) { + lastRecord = record; + continue; + } else if (lastRecord == record) { consecutiveCount++; - } else { - // Insert row and populate for the previous asset type - ui->tableWidget_Order->insertRow(consecutiveIndex); - ui->tableWidget_Order->setItem(consecutiveIndex, 0, new QTableWidgetItem(lastAssetType)); - ui->tableWidget_Order->setItem(consecutiveIndex, 1, new QTableWidgetItem(Utils::AssetTypeToString(lastAssetType))); - ui->tableWidget_Order->setItem(consecutiveIndex, 2, new QTableWidgetItem(QString::number(consecutiveCount))); - - // Update counts and asset type - consecutiveCount = 1; - consecutiveIndex++; - lastAssetType = assetType; + continue; } - } -} - -void MainWindow::ParseAsset_LocalString(QDataStream *aZoneFileStream) { - // Skip separator - aZoneFileStream->skipRawData(8); - - // Parse local string asset contents - QString localStr; - char localStrChar; - *aZoneFileStream >> localStrChar; - while (localStrChar != 0) { - localStr += localStrChar; - *aZoneFileStream >> localStrChar; + QTreeWidgetItem *recordItem = new QTreeWidgetItem(mRootItem); + recordItem->setText(0, QString("%1 [%2]") + .arg(Utils::AssetTypeToString(lastRecord)) + .arg(consecutiveCount)); + lastRecord = record; + consecutiveCount = 1; } - // Parse rawfile name - QString aliasName; - char aliasNameChar; - *aZoneFileStream >> aliasNameChar; - while (aliasNameChar != 0) { - aliasName += aliasNameChar; - *aZoneFileStream >> aliasNameChar; - } - // qDebug() << QString("%1 = %2").arg(aliasName).arg(localStr); - ui->listWidget_LocalString->addItem(QString("%1 = %2").arg(aliasName).arg(localStr)); -} + mRootItem = assetsItem; -void MainWindow::ParseAsset_RawFile(QDataStream *aZoneFileStream) { - // Skip start separator FF FF FF FF (pointer?) - aZoneFileStream->skipRawData(4); + // Parse current and consecutive assets + AssetMap assetMap = ZoneFileParser::ParseAssets(&zoneFileStream, zoneFile.records); + assetsItem->setText(0, QString("Assets")); - quint32 gscLength; - *aZoneFileStream >> gscLength; - - // Skip unknown 4 byte data - aZoneFileStream->skipRawData(4); - - // Parse rawfile path - QString rawFilePath; - char scriptPathChar; - *aZoneFileStream >> scriptPathChar; - while (scriptPathChar != 0) { - rawFilePath += scriptPathChar; - *aZoneFileStream >> scriptPathChar; - } - rawFilePath.replace(",", ""); - const QStringList pathParts = rawFilePath.split('/'); - if (pathParts.size() == 0) { - qDebug() << "Failed to parse ff path! " << rawFilePath; - exit(-1); - } else if (pathParts.size() == 1) { - const QString path = pathParts[0]; - QTreeWidgetItem *newRootItem = new QTreeWidgetItem(ui->treeWidget_Scripts); - newRootItem->setText(0, path); - } else { - const QString path = pathParts[0]; - QTreeWidgetItem *newRootItem; - if (mTreeMap.contains(path)) { - newRootItem = mTreeMap[path]; - } else { - newRootItem = new QTreeWidgetItem(ui->treeWidget_Scripts); - newRootItem->setText(0, path); - mTreeMap[path] = newRootItem; + if (!assetMap.rawFiles.isEmpty()) { + QTreeWidgetItem *rawFilesItem = new QTreeWidgetItem(mRootItem); + foreach (const RawFile rawFile, assetMap.rawFiles) { + QTreeWidgetItem *rawFileItem = new QTreeWidgetItem(rawFilesItem); + rawFileItem->setText(0, rawFile.path); } + rawFilesItem->setText(0, QString("Raw Files [%1]") + .arg(assetMap.rawFiles.length())); + } - QTreeWidgetItem *parentItem = newRootItem; - for (int i = 1; i < pathParts.size(); i++) { - const QString path = pathParts[i]; - QTreeWidgetItem *newChildItem; - if (mTreeMap.contains(path)) { - newChildItem = mTreeMap[path]; - } else { - newChildItem = new QTreeWidgetItem(); - newChildItem->setText(0, path); - mTreeMap[path] = newChildItem; - } - parentItem->addChild(newChildItem); - parentItem = newChildItem; + if (!assetMap.localStrings.isEmpty()) { + QTreeWidgetItem *localStrsItem = new QTreeWidgetItem(mRootItem); + foreach (const LocalString localString, assetMap.localStrings) { + QTreeWidgetItem *localStrItem = new QTreeWidgetItem(localStrsItem); + localStrItem->setText(0, localString.string); } + localStrsItem->setText(0, QString("Local Strings [%1]") + .arg(assetMap.localStrings.length())); } - // Parse gsc contents - QString rawFileContents; - char rawFileContentsChar; - *aZoneFileStream >> rawFileContentsChar; - while (rawFileContentsChar != 0 && rawFileContentsChar != -1) { - rawFileContents += rawFileContentsChar; - *aZoneFileStream >> rawFileContentsChar; - } - mRawFileMap[rawFilePath] = (rawFileContents.isEmpty()) ? ("EMPTY") : (rawFileContents); - // qDebug() << QString("%1: %2").arg(rawFilePath).arg(rawFileContents); -} - -void MainWindow::ParseAsset_PhysPreset(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_XModel(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_Material(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_PixelShader(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_TechSet(QDataStream *aZoneFileStream) { - aZoneFileStream->skipRawData(4); - // Parse techset name - QString techSetName; - char techSetNameChar; - *aZoneFileStream >> techSetNameChar; - while (techSetNameChar == 0) { - *aZoneFileStream >> techSetNameChar; - } - while (techSetNameChar != 0) { - techSetName += techSetNameChar; - *aZoneFileStream >> techSetNameChar; - } - techSetName.replace(",", ""); - ui->listWidget_TechSets->addItem(techSetName); - //qDebug() << "Tech Set: " << techSetName; -} - -void MainWindow::ParseAsset_Image(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_LoadedSound(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_ColMapMP(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_GameMapSP(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_GameMapMP(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_LightDef(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_UIMap(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_SNDDriverGlobals(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_AIType(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_FX(QDataStream *aZoneFileStream) { - -} - -void MainWindow::ParseAsset_XAnim(QDataStream *aZoneFileStream) { - // Read in pointer to x_anim name - QByteArray namePtr(4, Qt::Uninitialized); - aZoneFileStream->readRawData(namePtr.data(), 4); - - // Read in counts - quint16 dataByteCount, dataShortCount, - dataIntCount, randomDataByteCount, - randomDataIntCount, numframes; - *aZoneFileStream >> dataByteCount >> dataShortCount >> - dataIntCount >> randomDataByteCount >> - randomDataIntCount >> numframes; - - // Read bool flags - bool isLooped, isDelta; - *aZoneFileStream >> isLooped >> isDelta; - - // Read in more counts - quint8 noneRotatedBoneCount, - twoDRotatedBoneCount, normalRotatedBoneCount, - twoDStaticRotatedBoneCount, normalStaticRotatedBoneCount, - normalTranslatedBoneCount, preciseTranslatedBoneCount, - staticTranslatedBoneCount, noneTranslatedBoneCount, - totalBoneCount, otherBoneCount1, otherBoneCount2; - *aZoneFileStream >> noneRotatedBoneCount >> - twoDRotatedBoneCount >> normalRotatedBoneCount >> - twoDStaticRotatedBoneCount >> normalStaticRotatedBoneCount >> - normalTranslatedBoneCount >> preciseTranslatedBoneCount >> - staticTranslatedBoneCount >> noneTranslatedBoneCount >> - totalBoneCount >> otherBoneCount1 >> otherBoneCount2; - - // Yet more counts - quint8 notifyCount, assetType; - *aZoneFileStream >> notifyCount >> assetType; - - // Read more bool flags - bool pad; - *aZoneFileStream >> pad; - - // Yet more more counts - unsigned int randomDataShortCount, indexCount; - *aZoneFileStream >> randomDataShortCount >> indexCount; - - // Read in floats - float frameRate, frequency; - *aZoneFileStream >> frameRate >> frequency; - - // Read in pointers - quint32 boneIDsPtr, dataBytePtr, dataShortPtr, dataIntPtr, - randomDataShortPtr, randomDataBytePtr, randomDataIntPtr, - longIndiciesPtr, notificationsPtr, deltaPartsPtr; - *aZoneFileStream >> boneIDsPtr >> dataBytePtr >> dataShortPtr - >> dataIntPtr >> randomDataShortPtr >> randomDataBytePtr - >> randomDataIntPtr >> longIndiciesPtr >> notificationsPtr - >> deltaPartsPtr; - - // Read in x_anim file name - QString xAnimName; - char xAnimNameChar; - *aZoneFileStream >> xAnimNameChar; - while (xAnimNameChar != 0) { - xAnimName += xAnimNameChar; - *aZoneFileStream >> xAnimNameChar; - } - - // Parse x_anim index header - QVector sectionLengths; - for (int i = 0; i < numframes; i++) { - quint8 sectionlength; - *aZoneFileStream >> sectionlength; - sectionLengths.push_back(sectionlength); - // Skip padding - aZoneFileStream->skipRawData(1); - } - // Skip unknown section - aZoneFileStream->skipRawData(2 * 8); -} - -void MainWindow::ParseAsset_MenuFile(QDataStream *aZoneFileStream) { - //MENU_FILE -} - -void MainWindow::ParseAsset_Weapon(QDataStream *aZoneFileStream) { - //WEAPON_FILE -} - -void MainWindow::ParseAsset_D3DBSP(QDataStream *aZoneFileStream) { - //D3DBSP_DUMP -} - -void MainWindow::ParseAsset_StringTable(QDataStream *aZoneFileStream) { - aZoneFileStream->skipRawData(4); - - quint32 columnCount, rowCount; - *aZoneFileStream >> columnCount >> rowCount; - columnCount = 0; - rowCount = 0; - - aZoneFileStream->skipRawData(4); - - QString stringTableName; - char stringTableNameChar; - *aZoneFileStream >> stringTableNameChar; - while (stringTableNameChar != 0) { - stringTableName += stringTableNameChar; - *aZoneFileStream >> stringTableNameChar; - } - ui->comboBox_StringTable->addItem(stringTableName); - - QVector tablePointers = QVector(); - for (quint32 i = 0; i < rowCount; i++) { - QByteArray pointerData(4, Qt::Uninitialized); - aZoneFileStream->readRawData(pointerData.data(), 4); - tablePointers.push_back(pointerData.toHex()); - - aZoneFileStream->skipRawData(4); - } - - for (const QString &pointerAddr : tablePointers) { - QString leadingContent = ""; - if (pointerAddr == "FFFFFFFF") { - char leadingContentChar; - *aZoneFileStream >> leadingContentChar; - while (leadingContentChar != 0) { - leadingContent += leadingContentChar; - *aZoneFileStream >> leadingContentChar; - } - } else { - leadingContent = pointerAddr; + if (!assetMap.stringTables.isEmpty()) { + QTreeWidgetItem *strTablesItem = new QTreeWidgetItem(mRootItem); + foreach (const StringTable stringTable, assetMap.stringTables) { + QTreeWidgetItem *strTableItem = new QTreeWidgetItem(strTablesItem); + strTableItem->setText(0, stringTable.name); } - - QString content; - char contentChar; - *aZoneFileStream >> contentChar; - while (contentChar != 0) { - content += contentChar; - *aZoneFileStream >> contentChar; - } - QPair tableEntry = QPair(); - tableEntry.first = leadingContent; - tableEntry.second = content; - if (!mStrTableMap.contains(stringTableName)) { - mStrTableMap[stringTableName] = QVector>(); - } - mStrTableMap[stringTableName].push_back(tableEntry); + strTablesItem->setText(0, QString("String Tables [%1]") + .arg(assetMap.stringTables.length())); } + + if (!assetMap.techSets.isEmpty()) { + QTreeWidgetItem *techSetsItem = new QTreeWidgetItem(mRootItem); + foreach (const TechSet techSet, assetMap.techSets) { + QTreeWidgetItem *techSetItem = new QTreeWidgetItem(techSetsItem); + techSetItem->setText(0, techSet.name); + } + techSetsItem->setText(0, QString("Tech Sets [%1]") + .arg(assetMap.techSets.length())); + } + + if (!assetMap.animations.isEmpty()) { + QTreeWidgetItem *animationsItem = new QTreeWidgetItem(mRootItem); + animationsItem->setText(0, "Animations"); + foreach (const Animation animation, assetMap.animations) { + QTreeWidgetItem *animationItem = new QTreeWidgetItem(animationsItem); + animationItem->setText(0, animation.name); + } + animationsItem->setText(0, QString("Animations [%1]") + .arg(assetMap.animations.length())); + } + + // Clean up zone file + zoneFileObj->close(); + delete zoneFileObj; + + return true; } -void MainWindow::on_pushButton_FastFile_clicked() { - // Try to prompt user to open fastfile - QFile *fastFile; - if (!(fastFile = OpenFastFile())) { - QMessageBox::warning(this, "Warning!", QString("Failed to open FastFile!.")); - return; +bool MainWindow::OpenZoneFile() { + const QString zoneFileName = Utils::GetOpenZoneFileName(); + if (!OpenZoneFile(zoneFileName)) { + qDebug() << "Failed to open Zone file!"; + return false; } - - // Parse data from fast file header - ParseFFHeader(fastFile); - - // Decompress fastfile and close - const QByteArray fastFileData = fastFile->readAll(); - const QByteArray decompressedData = DecompressZLIB(fastFileData); - // ui->plainTextEdit_ZoneDump->setPlainText(decompressedData.toHex()); - - const QString zoneFilePath = fastFile->fileName().replace(".ff", ".zone"); - fastFile->close(); - - // Check zone file is writeable - QFile *zoneFile = new QFile(zoneFilePath); - if (!zoneFile->open(QIODevice::ReadWrite)) { - qDebug() << QString("Zone file could not be written to: '%1'").arg(zoneFilePath); - return; - } - // Write zone data - zoneFile->write(decompressedData); - zoneFile->close(); - - // Open zone file as little endian stream - QDataStream zoneFileStream(decompressedData); - zoneFileStream.setByteOrder(QDataStream::LittleEndian); - - // Parse data from zone file header - ParseZoneHeader(&zoneFileStream); - ParseZoneIndex(&zoneFileStream); - - // Track current and consecutive assets - int assetIndex = 0; - - // Iterate asset types found in index - for (auto [assetType, assetCount] : mTypeMap.asKeyValueRange()) { - // Get asset description from type - QString assetStr = Utils::AssetTypeToString(assetType); - - // Insert row and populate - ui->tableWidget_Index->insertRow(assetIndex); - ui->tableWidget_Index->setItem(assetIndex, 0, new QTableWidgetItem(assetType)); - ui->tableWidget_Index->setItem(assetIndex, 1, new QTableWidgetItem(assetStr)); - ui->tableWidget_Index->setItem(assetIndex, 2, new QTableWidgetItem(QString::number(assetCount))); - - // Update count - assetIndex++; - } - - for (int i = 0; i < mTypeOrder.size(); i++) { - const QString typeHex = mTypeOrder[i]; - const QString typeStr = Utils::AssetTypeToString(typeHex); - - // qDebug() << "Parsing Asset of Type: " << typeHex; - if (typeStr == "LOCAL STRING") { // localized string asset - ParseAsset_LocalString(&zoneFileStream); - } else if (typeStr == "RAW FILE") { // gsc - ParseAsset_RawFile(&zoneFileStream); - } else if (typeStr == "PHYS PRESET") { // physpreset - ParseAsset_PhysPreset(&zoneFileStream); - } else if (typeStr == "MODEL") { // xmodel - ParseAsset_XModel(&zoneFileStream); - } else if (typeStr == "MATERIAL") { // material - ParseAsset_Material(&zoneFileStream); - } else if (typeStr == "SHADER") { // pixelshader - ParseAsset_PixelShader(&zoneFileStream); - } else if (typeStr == "TECH SET") { // techset include - ParseAsset_TechSet(&zoneFileStream); - } else if (typeStr == "IMAGE") { // image - ParseAsset_Image(&zoneFileStream); - } else if (typeStr == "SOUND") { // loaded_sound - ParseAsset_LoadedSound(&zoneFileStream); - } else if (typeStr == "COLLISION MAP") { // col_map_mp - ParseAsset_ColMapMP(&zoneFileStream); - } else if (typeStr == "MP MAP") { // game_map_sp - ParseAsset_GameMapSP(&zoneFileStream); - } else if (typeStr == "SP MAP") { // game_map_mp - ParseAsset_GameMapMP(&zoneFileStream); - } else if (typeStr == "LIGHT DEF") { // lightdef - ParseAsset_LightDef(&zoneFileStream); - } else if (typeStr == "UI MAP") { // ui_map - ParseAsset_UIMap(&zoneFileStream); - } else if (typeStr == "SND DRIVER GLOBALS") { // snddriverglobals - ParseAsset_SNDDriverGlobals(&zoneFileStream); - } else if (typeStr == "AI TYPE") { // aitype - ParseAsset_AIType(&zoneFileStream); - } else if (typeStr == "EFFECT") { // aitype - ParseAsset_FX(&zoneFileStream); - } else if (typeStr == "ANIMATION") { // aitype - ParseAsset_XAnim(&zoneFileStream); - } else if (typeStr == "STRING TABLE") { // string_table - ParseAsset_StringTable(&zoneFileStream); - } else if (typeStr == "MENU") { // string_table - ParseAsset_MenuFile(&zoneFileStream); - } else if (typeStr == "WEAPON") { // string_table - ParseAsset_Weapon(&zoneFileStream); - } else if (typeStr == "D3DBSP DUMP") { // string_table - ParseAsset_D3DBSP(&zoneFileStream); - } else if (typeStr != "UNKNOWN") { - qDebug() << "Found bad asset type!" << typeStr; - } - } - - // Close zone file - zoneFile->close(); - - // Clean up - delete zoneFile; - delete fastFile; -} - -void MainWindow::on_pushButton_FastFile_2_clicked() { - // Check zone file is writeable - QFile *zoneFile; - if (!(zoneFile = OpenZoneFile())) { - QMessageBox::warning(this, "Warning!", QString("Failed to open FastFile!.")); - return; - } - const QByteArray decompressedData = zoneFile->readAll(); - - // Open zone file as little endian stream - QDataStream zoneFileStream(decompressedData); - zoneFileStream.setByteOrder(QDataStream::LittleEndian); - - // Parse data from zone file header - ParseZoneHeader(&zoneFileStream); - ParseZoneIndex(&zoneFileStream); - - // Track current and consecutive assets - int assetIndex = 0; - - // Iterate asset types found in index - for (auto [assetType, assetCount] : mTypeMap.asKeyValueRange()) { - // Get asset description from type - QString assetStr = Utils::AssetTypeToString(assetType); - - // Insert row and populate - ui->tableWidget_Index->insertRow(assetIndex); - ui->tableWidget_Index->setItem(assetIndex, 0, new QTableWidgetItem(assetType)); - ui->tableWidget_Index->setItem(assetIndex, 1, new QTableWidgetItem(assetStr)); - ui->tableWidget_Index->setItem(assetIndex, 2, new QTableWidgetItem(QString::number(assetCount))); - - // Update count - assetIndex++; - } - - for (int i = 0; i < mTypeOrder.size(); i++) { - const QString typeHex = mTypeOrder[i]; - const QString typeStr = Utils::AssetTypeToString(typeHex); - - // qDebug() << "Parsing Asset of Type: " << typeHex; - if (typeStr == "LOCAL STRING") { // localized string asset - ParseAsset_LocalString(&zoneFileStream); - } else if (typeStr == "RAW FILE") { // gsc - ParseAsset_RawFile(&zoneFileStream); - } else if (typeStr == "PHYS PRESET") { // physpreset - ParseAsset_PhysPreset(&zoneFileStream); - } else if (typeStr == "MODEL") { // xmodel - ParseAsset_XModel(&zoneFileStream); - } else if (typeStr == "MATERIAL") { // material - ParseAsset_Material(&zoneFileStream); - } else if (typeStr == "SHADER") { // pixelshader - ParseAsset_PixelShader(&zoneFileStream); - } else if (typeStr == "TECH SET") { // techset include - ParseAsset_TechSet(&zoneFileStream); - } else if (typeStr == "IMAGE") { // image - ParseAsset_Image(&zoneFileStream); - } else if (typeStr == "SOUND") { // loaded_sound - ParseAsset_LoadedSound(&zoneFileStream); - } else if (typeStr == "COLLISION MAP") { // col_map_mp - ParseAsset_ColMapMP(&zoneFileStream); - } else if (typeStr == "MP MAP") { // game_map_sp - ParseAsset_GameMapSP(&zoneFileStream); - } else if (typeStr == "SP MAP") { // game_map_mp - ParseAsset_GameMapMP(&zoneFileStream); - } else if (typeStr == "LIGHT DEF") { // lightdef - ParseAsset_LightDef(&zoneFileStream); - } else if (typeStr == "UI MAP") { // ui_map - ParseAsset_UIMap(&zoneFileStream); - } else if (typeStr == "SND DRIVER GLOBALS") { // snddriverglobals - ParseAsset_SNDDriverGlobals(&zoneFileStream); - } else if (typeStr == "AI TYPE") { // aitype - ParseAsset_AIType(&zoneFileStream); - } else if (typeStr == "EFFECT") { // aitype - ParseAsset_FX(&zoneFileStream); - } else if (typeStr == "ANIMATION") { // aitype - ParseAsset_XAnim(&zoneFileStream); - } else if (typeStr == "STRING TABLE") { // string_table - ParseAsset_StringTable(&zoneFileStream); - } else if (typeStr == "MENU") { // string_table - ParseAsset_MenuFile(&zoneFileStream); - } else if (typeStr == "WEAPON") { // string_table - ParseAsset_Weapon(&zoneFileStream); - } else if (typeStr == "D3DBSP DUMP") { // string_table - ParseAsset_D3DBSP(&zoneFileStream); - } else if (typeStr != "UNKNOWN") { - qDebug() << "Found bad asset type!" << typeStr; - } - } - - // Close zone file - zoneFile->close(); - - // Clean up - delete zoneFile; + mRootItem = mTreeWidget->invisibleRootItem(); + return true; } int MainWindow::LoadFile_D3DBSP(const QString aFilePath) { @@ -1195,9 +327,6 @@ int MainWindow::LoadFile_D3DBSP(const QString aFilePath) { // Assign diskLumpOrderSize mDiskLumpOrder.resize(mDiskLumpCount); - qDebug() << "BSP Version:" << mBSPVersion; - qDebug() << "Lump Count:" << mDiskLumpCount; - // Read Lump Index Entries quint32 lumpOffset = sizeof(quint32) * 3 + sizeof(LumpIndexEntry) * mDiskLumpCount; diff --git a/mainwindow.h b/mainwindow.h index cfee96c..a6b3963 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -4,6 +4,11 @@ #include "enums.h" #include "structs.h" #include "utils.h" +#include "aboutdialog.h" +#include "compression.h" +#include "ffparser.h" +#include "zfparser.h" +#include "modelviewer.h" #include #include @@ -12,52 +17,8 @@ #include #include #include - -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -#include -#include - -#include - +#include +#include #include QT_BEGIN_NAMESPACE @@ -75,60 +36,15 @@ public: ~MainWindow(); void Reset(); - QByteArray DecompressZLIB(QByteArray compressedData); - private slots: - void on_pushButton_FastFile_clicked(); - void on_pushButton_FastFile_2_clicked(); + bool OpenFastFile(const QString aFastFilePath); + bool OpenFastFile(); - QFile *OpenFastFile(); - QFile *OpenZoneFile(); - void ParseFFHeader(QFile *aFastFilePtr); - void ParseFFCompany(QDataStream *aFastFileStream); - void ParseFFFileType(QDataStream *afastFileStream); - void ParseFFSignage(QDataStream *afastFileStream); - void ParseFFMagic(QDataStream *afastFileStream); - void ParseFFVersion(QDataStream *afastFileStream); - - void ParseZoneHeader(QDataStream *aZoneFileStream); - void ParseZoneSize(QDataStream *aZoneFileStream); - - void ParseZoneUnknownsA(QDataStream *aZoneFileStream); - void ParseZoneUnknownsB(QDataStream *aZoneFileStream); - void ParseZoneUnknownsC(QDataStream *aZoneFileStream); - void ParseZoneTagCount(QDataStream *aZoneFileStream); - void ParseZoneRecordCount(QDataStream *aZoneFileStream); - void ParseZoneTags(QDataStream *aZoneFileStream); - void ParseZoneIndex(QDataStream *aZoneFileStream); - - void ParseAsset_LocalString(QDataStream *aZoneFileStream); - void ParseAsset_RawFile(QDataStream *aZoneFileStream); - void ParseAsset_PhysPreset(QDataStream *aZoneFileStream); - void ParseAsset_XModel(QDataStream *aZoneFileStream); - void ParseAsset_Material(QDataStream *aZoneFileStream); - void ParseAsset_PixelShader(QDataStream *aZoneFileStream); - void ParseAsset_TechSet(QDataStream *aZoneFileStream); - void ParseAsset_Image(QDataStream *aZoneFileStream); - void ParseAsset_LoadedSound(QDataStream *aZoneFileStream); - void ParseAsset_ColMapMP(QDataStream *aZoneFileStream); - void ParseAsset_GameMapSP(QDataStream *aZoneFileStream); - void ParseAsset_GameMapMP(QDataStream *aZoneFileStream); - void ParseAsset_LightDef(QDataStream *aZoneFileStream); - void ParseAsset_UIMap(QDataStream *aZoneFileStream); - void ParseAsset_SNDDriverGlobals(QDataStream *aZoneFileStream); - void ParseAsset_AIType(QDataStream *aZoneFileStream); - void ParseAsset_FX(QDataStream *aZoneFileStream); - void ParseAsset_XAnim(QDataStream *aZoneFileStream); - void ParseAsset_StringTable(QDataStream *aZoneFileStream); - void ParseAsset_MenuFile(QDataStream *aZoneFileStream); - void ParseAsset_Weapon(QDataStream *aZoneFileStream); - void ParseAsset_D3DBSP(QDataStream *aZoneFileStream); + bool OpenZoneFile(const QString aZoneFilePath); + bool OpenZoneFile(); int LoadFile_D3DBSP(const QString aFilePath); - void ScriptSelected(); - void StrTableSelected(QString aStrTableName); - private: Ui::MainWindow *ui; QMap mTypeMap; @@ -139,6 +55,11 @@ private: QMap mTreeMap; QMap>> mStrTableMap; + QTreeWidget *mTreeWidget; + QTreeWidgetItem *mRootItem; + QPlainTextEdit *mScriptEditor; + ModelViewer *mModelViewer; + quint32 mBSPVersion; quint32 mDiskLumpCount; QVector mDiskLumpOrder; diff --git a/mainwindow.ui b/mainwindow.ui index 0476b70..a6a9903 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -6,12 +6,18 @@ 0 0 - 850 - 628 + 986 + 692 + + + 550 + 300 + + - FastFile Wizard + XPlor QMainWindow { @@ -19,1060 +25,337 @@ } - - - - - true - - - - 14 - true - - - - WAW - PC - FastFile Viewer - - - Qt::AlignmentFlag::AlignCenter - - - - - - - true - - - Qt::Orientation::Horizontal - - - - - - - true - - - 0 - - - - General - - - - - - - - true - - - FastFile path... - - - - - - - true - - - Open .ff - - - - - - - - - - - true - - - ZoneFile path... - - - - - - - true - - - Open .zone - - - - - - - - - - - true - - - Company: - - - - - - - true - - - - Select Company - - - - - INFINITY_WARD - - - - - TREYARCH - - - - - SLEDGEHAMMER - - - - - NEVERSOFT - - - - - - - - - - - - true - - - File Type: - - - - - - - true - - - - Select File Type - - - - - FAST_FILE - - - - - - - - true - - - Signed - - - - - - - - - - - true - - - Magic: - - - - - - - true - - - - 0 - 0 - - - - true - - - - - - - true - - - 999999999 - - - - - - - - - - - true - - - Tag Count - - - - - - - true - - - 999999999 - - - - - - - - - - - true - - - File Size: - - - - - - - true - - - B - - - 999999999 - - - - - - - - - - - true - - - Version: - - - - - - - true - - - 999999999 - - - - - - - - - - - true - - - Record Count - - - - - - - true - - - 999999999 - - - - - - - - - Qt::Orientation::Vertical - - - - 17 - 242 - - - - - - - - - Unknowns - - - - - - - - Unknown 1: - - - - - - - - 0 - 0 - - - - true - - - - - - - true - - - 999999999 - - - - - - - - - - - Unknown 2: - - - - - - - - 0 - 0 - - - - true - - - - - - - true - - - 999999999 - - - - - - - - - - - Unknown 4: - - - - - - - - 0 - 0 - - - - - - - - true - - - 999999999 - - - - - - - - - - - Unknown 3: - - - - - - - - 0 - 0 - - - - - - - - true - - - 999999999 - - - - - - - - - - - Unknown 6: - - - - - - - - 0 - 0 - - - - true - - - - - - - true - - - 999999999 - - - - - - - - - - - Unknown 5: - - - - - - - - 0 - 0 - - - - true - - - - - - - true - - - 999999999 - - - - - - - - - - - Unknown 8: - - - - - - - - 0 - 0 - - - - true - - - - - - - true - - - 999999999 - - - - - - - - - - - Unknown 7: - - - - - - - - 0 - 0 - - - - true - - - - - - - true - - - 999999999 - - - - - - - - - - - Unknown 9: - - - - - - - - 0 - 0 - - - - true - - - - - - - true - - - 999999999 - - - - - - - - - - - Unknown 10: - - - - - - - - 0 - 0 - - - - true - - - - - - - true - - - 999999999 - - - - - - - - - - - Unknown 11: - - - - - - - - 0 - 0 - - - - true - - - - - - - true - - - 999999999 - - - - - - - - - Qt::Orientation::Vertical - - - - 20 - 40 - - - - - - - - - Tags - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - true - - - - - - - - Localized Strings - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - Asset Index/Order - - - - - - Asset Index Table - - - - - - 3 - - - 50 - - - true - - - true - - - false - - - - - - - - - - This table shows the total asset counts per type. - - - - - - - - - - Qt::Orientation::Vertical - - - - - - - Asset Order Table - - - - - - 3 - - - 50 - - - true - - - true - - - false - - - - - - - - - - This table shows the consecutive asset counts in order. - - - - - - - - - - - Raw Files - - - - - - - 0 - 0 - - - - - 200 - 16777215 - - - - - 1 - - - - - - - - - 0 - 0 - - - - - - - - - Tech Sets - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - Zone Dump - - - - - - - - Select Asset: - - - - - - - - - - - - - - - - - - - String Tables - - - - - - - - String Table: - - - - - - - - - - - - - - - - 3D Scene - - - - - - - 200 - 16777215 - - - - - 1 - - - - - - - - QFrame::Shape::StyledPanel - - - QFrame::Shadow::Raised - - - - - - - - - - - - - true - - - < Back - - - - - - - true - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - true - - - Next > - - - - - - + + + + + 0 + 0 + 986 + 21 + + + + + File + + + + Recent... + + + + + + + + Import... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Edit + + + + Undo History... + + + + + Redo History... + + + + + + + + + + + + + + + + + + + + + + Help + + + + + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + New + + + + + + + + New Fast File + + + + + + + + New Zone File + + + + + + + + Open Fast File + + + + + + + + Open Zone File + + + + + + + + Open Folder + + + + + + + + Save + + + + + + + + Save As + + + + + e + + + + + File + + + + + Folder + + + + + New File + + + + + New Fast File + + + + + New Zone File + + + + + From Clipboard + + + + + Material + + + + + Sound + + + + + + + + Undo + + + + + + + + Redo + + + + + + + + Cut + + + + + + + + Copy + + + + + + + + Paste + + + + + + + + Rename + + + + + Edit Value + + + + + Edit as Hex + + + + + + + + Delete + + + + + d + + + + + d + + + + + Clear Undo History + + + + + Find + + + + + + + + About + + + + + Change Icons + + + + + + + + Check for Updates + + + + + + + + Find + + diff --git a/modelviewer.cpp b/modelviewer.cpp new file mode 100644 index 0000000..27058b0 --- /dev/null +++ b/modelviewer.cpp @@ -0,0 +1,73 @@ +#include "modelviewer.h" + +ModelViewer::ModelViewer(QWidget *parent) + : QWidget{parent} { + Qt3DExtras::Qt3DWindow *view = new Qt3DExtras::Qt3DWindow(); + view->defaultFrameGraph()->setClearColor(QColor(QRgb(0x4d4d4f))); + + QWidget *container = QWidget::createWindowContainer(view); + QSize screenSize = view->screen()->size(); + container->setMinimumSize(QSize(200, 100)); + container->setMaximumSize(screenSize); + + QHBoxLayout *hLayout = new QHBoxLayout(this); + QVBoxLayout *vLayout = new QVBoxLayout(); + vLayout->setAlignment(Qt::AlignTop); + hLayout->addWidget(container, 1); + hLayout->addLayout(vLayout); + + // Root entity + Qt3DCore::QEntity *rootEntity = new Qt3DCore::QEntity(); + + // Camera + Qt3DRender::QCamera *cameraEntity = view->camera(); + + cameraEntity->lens()->setPerspectiveProjection(45.0f, 16.0f/9.0f, 0.1f, 1000.0f); + cameraEntity->setPosition(QVector3D(0, 0, 50.0f)); // Move farther along Z-axis + cameraEntity->setUpVector(QVector3D(0, 1, 0)); + cameraEntity->setViewCenter(QVector3D(0, 0, 0)); + + Qt3DCore::QEntity *lightEntity = new Qt3DCore::QEntity(rootEntity); + Qt3DRender::QPointLight *light = new Qt3DRender::QPointLight(lightEntity); + light->setColor("white"); + light->setIntensity(1); + lightEntity->addComponent(light); + + Qt3DCore::QTransform *lightTransform = new Qt3DCore::QTransform(lightEntity); + lightTransform->setTranslation(cameraEntity->position()); + lightEntity->addComponent(lightTransform); + + // For camera controls + Qt3DExtras::QFirstPersonCameraController *camController = new Qt3DExtras::QFirstPersonCameraController(rootEntity); + camController->setCamera(cameraEntity); + + // Set root object of the scene + view->setRootEntity(rootEntity); + + // Load custom 3D model + Qt3DRender::QMesh *customMesh = new Qt3DRender::QMesh(); + customMesh->setSource(QUrl::fromLocalFile(":/obj/data/obj/defaultactor_LOD0.obj")); + + // Adjust the model transformation + Qt3DCore::QTransform *customTransform = new Qt3DCore::QTransform(); + customTransform->setRotationX(-90); + customTransform->setRotationY(-90); + + // Keep translation if necessary + customTransform->setTranslation(QVector3D(0.0f, -100.0f, -200.0f)); + + Qt3DExtras::QNormalDiffuseMapMaterial *customMaterial = new Qt3DExtras::QNormalDiffuseMapMaterial(); + + Qt3DRender::QTextureLoader *normalMap = new Qt3DRender::QTextureLoader(); + normalMap->setSource(QUrl::fromLocalFile(":/obj/data/obj/normalmap.png")); + customMaterial->setNormal(normalMap); + + Qt3DRender::QTextureLoader *diffuseMap = new Qt3DRender::QTextureLoader(); + diffuseMap->setSource(QUrl::fromLocalFile(":/obj/data/obj/diffusemap.png")); + customMaterial->setDiffuse(diffuseMap); + + Qt3DCore::QEntity *m_torusEntity = new Qt3DCore::QEntity(rootEntity); + m_torusEntity->addComponent(customMesh); + m_torusEntity->addComponent(customMaterial); + m_torusEntity->addComponent(customTransform); +} diff --git a/modelviewer.h b/modelviewer.h new file mode 100644 index 0000000..c56d376 --- /dev/null +++ b/modelviewer.h @@ -0,0 +1,53 @@ +#ifndef MODELVIEWER_H +#define MODELVIEWER_H + +#include +#include +#include +#include +#include +#include +#include +#include + +// Qt3DCore includes +#include +#include +#include +#include +#include +#include +// Qt3DInput includes +#include +// Qt3DRender includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// Qt3DExtras includes +#include +#include +#include +#include +#include +#include +#include + +class ModelViewer : public QWidget +{ + Q_OBJECT +public: + explicit ModelViewer(QWidget *parent = nullptr); + +signals: +}; + +#endif // MODELVIEWER_H diff --git a/structs.h b/structs.h index 443470e..6f7efbc 100644 --- a/structs.h +++ b/structs.h @@ -1,7 +1,12 @@ #ifndef STRUCTS_H #define STRUCTS_H +#include "enums.h" + #include +#include +#include +#include // Define Lump Structure struct Lump { @@ -16,4 +21,109 @@ struct LumpIndexEntry { quint32 length; }; +struct LocalString { + QString string; + QString alias; +}; + +struct RawFile { + quint32 length; + QString path; + QString contents; +}; + +struct TechSet { + QString name; +}; + +struct Animation { + quint16 dataByteCount; + quint16 dataShortCount; + quint16 dataIntCount; + quint16 randomDataByteCount; + quint16 randomDataIntCount; + quint16 numframes; + bool isLooped; + bool isDelta; + quint8 noneRotatedBoneCount; + quint8 twoDRotatedBoneCount; + quint8 normalRotatedBoneCount; + quint8 twoDStaticRotatedBoneCount; + quint8 normalStaticRotatedBoneCount; + quint8 normalTranslatedBoneCount; + quint8 preciseTranslatedBoneCount; + quint8 staticTranslatedBoneCount; + quint8 noneTranslatedBoneCount; + quint8 totalBoneCount; + quint8 otherBoneCount1; + quint8 otherBoneCount2; + quint8 notifyCount; + quint8 assetType; + bool pad; + unsigned int randomDataShortCount; + unsigned int indexCount; + float frameRate; + float frequency; + quint32 boneIDsPtr; + quint32 dataBytePtr; + quint32 dataShortPtr; + quint32 dataIntPtr; + quint32 randomDataShortPtr; + quint32 randomDataBytePtr; + quint32 randomDataIntPtr; + quint32 longIndiciesPtr; + quint32 notificationsPtr; + quint32 deltaPartsPtr; + QString name; +}; + +struct StringTable { + quint32 columnCount; + quint32 rowCount; + QString name; +}; + +struct AssetMap { + QVector localStrings; + QVector rawFiles; + //QVector phyPresets; + //QVector models; + //QVector rawFiles; + //QVector shaders; + QVector techSets; + //QVector images; + //QVector sounds; + //QVector collMaps; + //QVector lightDefs; + //QVector uiMaps; + //QVector driverGlobals; + //QVector aiType; + //QVector effects; + QVector animations; + QVector stringTables; + //QVector

menus; + //QVector weapons; + //QVector d3dbspDumps; + //QVector spMaps; +}; + +struct ZoneFile { + quint32 size; + quint32 tagCount; + QStringList tags; + quint32 recordCount; + QStringList records; + AssetMap assetMap; +}; + +struct FastFile { + FF_COMPANY company; + FF_FILETYPE fileType; + FF_SIGNAGE signage; + QString magic; + quint32 version; + + ZoneFile zoneFile; +}; + #endif // STRUCTS_H diff --git a/utils.h b/utils.h index 4a0bfc0..2e387c9 100644 --- a/utils.h +++ b/utils.h @@ -5,6 +5,8 @@ #include #include +#include +#include class Utils { public: @@ -164,6 +166,69 @@ public: static quint32 PaddingSize(quint32 size) { return PadInt4(size) - size; } + + static QString GetOpenFastFileName(QWidget *parent = nullptr) { + // Open file dialog to steam apps + const QString steamPath = "C:/Program Files (x86)/Steam/steamapps/common/Call of Duty World at War/zone/english/"; + const QString fastFilePath = QFileDialog::getOpenFileName(parent, "Open Fast File", steamPath, "Fast File (*.ff);;All Files (*.*)"); + if (fastFilePath.isNull()) { + // User pressed cancel + return ""; + } else if (!QFile::exists(fastFilePath)) { + QMessageBox::warning(parent, "Warning!", QString("%1 does not exist!.").arg(fastFilePath)); + return ""; + } + return fastFilePath; + } + + static QString GetOpenZoneFileName(QWidget *parent = nullptr) { + // Open file dialog to steam apps + const QString steamPath = "C:/Program Files (x86)/Steam/steamapps/common/Call of Duty World at War/zone/english/"; + const QString zoneFilePath = QFileDialog::getOpenFileName(parent, "Open Zone File", steamPath, "Zone File (*.zone);;All Files (*.*)"); + if (zoneFilePath.isNull()) { + // User pressed cancel + return ""; + } else if (!QFile::exists(zoneFilePath)) { + QMessageBox::warning(parent, "Warning!", QString("%1 does not exist!.").arg(zoneFilePath)); + return nullptr; + } + return zoneFilePath; + } + + static QString CompanyEnumToStr(FF_COMPANY aCompany) { + switch (aCompany) { + case COMPANY_NONE: + return "None"; + case COMPANY_INFINITY_WARD: + return "Infinity Ward"; + case COMPANY_TREYARCH: + return "Treyarch"; + case COMPANY_SLEDGEHAMMER: + return "Sledgehammer"; + case COMPANY_NEVERSOFT: + return "Neversoft"; + } + } + + static QString FileTypeEnumToStr(FF_FILETYPE aFileType) { + switch (aFileType) { + case FILETYPE_NONE: + return "None"; + case FILETYPE_FAST_FILE: + return "Fast File"; + } + } + + static QString SignageEnumToStr(FF_SIGNAGE aSignage) { + switch (aSignage) { + case SIGNAGE_NONE: + return "None"; + case SIGNAGE_SIGNED: + return "Signed"; + case SIGNAGE_UNSIGNED: + return "Unsigned"; + } + } }; #endif // UTILS_H diff --git a/zfparser.h b/zfparser.h new file mode 100644 index 0000000..a47cc04 --- /dev/null +++ b/zfparser.h @@ -0,0 +1,523 @@ +#ifndef ZFPARSER_H +#define ZFPARSER_H + +#include "qtypes.h" +#include "utils.h" +#include "structs.h" + +#include +#include +#include + +class ZoneFileParser { +public: + static ZoneFile ParseZoneHeader(QDataStream *aZoneFileStream) { + ZoneFile result; + + result.size = ParseZoneSize(aZoneFileStream); + ParseZoneUnknownsA(aZoneFileStream); + + result.tagCount = ParseZoneTagCount(aZoneFileStream); + ParseZoneUnknownsB(aZoneFileStream); + + result.recordCount = ParseZoneRecordCount(aZoneFileStream); + + if (result.tagCount) { + ParseZoneUnknownsC(aZoneFileStream); + result.tags = ParseZoneTags(aZoneFileStream, result.tagCount); + } else { + aZoneFileStream->skipRawData(4); + } + + return result; + } + + static quint32 ParseZoneSize(QDataStream *aZoneFileStream) { + quint32 zoneFileSize; + *aZoneFileStream >> zoneFileSize; + if (zoneFileSize <= 0) { + qDebug() << "Tried to open empty zone file!"; + exit(-1); + } + zoneFileSize += 36; + return zoneFileSize; + } + + /* + ParseZoneUnknownsA() + + Parses the 1st section of unknowns as hex vals and uint32s +*/ + static void ParseZoneUnknownsA(QDataStream *aZoneFileStream) { + // Byte 4-7, 8-11, 12-15: unknown + QByteArray unknown1(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown1.data(), 4); + + QByteArray unknown2(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown2.data(), 4); + + QByteArray unknown3(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown3.data(), 4); + + // Byte 16-19, 20-23: empty/unknown + QByteArray unknown4(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown4.data(), 4); + + QByteArray unknown5(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown5.data(), 4); + + // Byte 24-27: somehow related to the filesize, but smaller value + QByteArray unknown6(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown6.data(), 4); + + // Byte 28-31, 32-35: unknown + QByteArray unknown7(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown7.data(), 4); + + QByteArray unknown8(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown8.data(), 4); + } + + /* + ParseZoneTagCount() + + Parses the number of string tags in the zone index +*/ + static quint32 ParseZoneTagCount(QDataStream *aZoneFileStream) { + quint32 tagCount; + *aZoneFileStream >> tagCount; + return tagCount; + } + + /* + ParseZoneRecordCount() + + Parses the number of records in the zone index +*/ + static quint32 ParseZoneRecordCount(QDataStream *aZoneFileStream) { + quint32 recordCount; + *aZoneFileStream >> recordCount; + return recordCount; + } + + /* + ParseZoneUnknownsB() + + Parses the 2nd section of unknowns as hex vals and uint32s +*/ + static void ParseZoneUnknownsB(QDataStream *aZoneFileStream) { + // Byte 44-47: Unknown/empty? + QByteArray unknown9(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown9.data(), 4); + } + + /* + ParseZoneUnknownsC() + + Parses the 3rd section of unknowns as hex vals and uint32s +*/ + static void ParseZoneUnknownsC(QDataStream *aZoneFileStream) { + // Byte 40-43: Unknown/empty? + QByteArray unknown10(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown10.data(), 4); + + // Byte 44-47: Unknown/empty? + QByteArray unknown11(4, Qt::Uninitialized); + aZoneFileStream->readRawData(unknown11.data(), 4); + } + + /* + ParseZoneTags() + + Parses the string tags ate the start of zone file +*/ + static QStringList ParseZoneTags(QDataStream *aZoneFileStream, quint32 tagCount) { + QStringList tags; + + // Byte 48-51: Repeated separators? ÿÿÿÿ x i + aZoneFileStream->skipRawData(4 * (tagCount - 1)); + + // Parse tags/strings before index + QString zoneTag; + char zoneTagChar; + for (quint32 i = 0; i < tagCount - 1; i++) { + *aZoneFileStream >> zoneTagChar; + while (zoneTagChar != 0) { + zoneTag += zoneTagChar; + *aZoneFileStream >> zoneTagChar; + } + tags << zoneTag; + zoneTag.clear(); + } + return tags; + } + + /* + ParseZoneIndex() + + Parse the binary zone index data and populate table +*/ + static QStringList ParseZoneIndex(QDataStream *aZoneFileStream, quint32 recordCount) { + QStringList result; + + // Don't parse if no records + if (!recordCount) { return result; } + + // Track past assets and counts + QString lastAssetType = ""; + + // Parse index & map found asset types + for (quint32 i = 0; i < recordCount; i++) { + // Skip record start + QByteArray rawAssetType(4, Qt::Uninitialized); + aZoneFileStream->readRawData(rawAssetType.data(), 4); + result << rawAssetType.toHex(); + + // Skip separator + aZoneFileStream->skipRawData(4); + } + return result; + } + + static AssetMap ParseAssets(QDataStream *aZoneFileStream, QStringList assetOrder) { + AssetMap result; + + for (int i = 0; i < assetOrder.size(); i++) { + const QString typeHex = assetOrder[i]; + const QString typeStr = Utils::AssetTypeToString(typeHex); + + if (typeStr == "LOCAL STRING") { // localized string asset + result.localStrings << ParseAsset_LocalString(aZoneFileStream); + } else if (typeStr == "RAW FILE") { // gsc + result.rawFiles << ParseAsset_RawFile(aZoneFileStream); + } else if (typeStr == "PHYS PRESET") { // physpreset + ParseAsset_PhysPreset(aZoneFileStream); + } else if (typeStr == "MODEL") { // xmodel + ParseAsset_Model(aZoneFileStream); + } else if (typeStr == "MATERIAL") { // material + ParseAsset_Material(aZoneFileStream); + } else if (typeStr == "SHADER") { // pixelshader + ParseAsset_PixelShader(aZoneFileStream); + } else if (typeStr == "TECH SET") { // techset include + result.techSets << ParseAsset_TechSet(aZoneFileStream); + } else if (typeStr == "IMAGE") { // image + ParseAsset_Image(aZoneFileStream); + } else if (typeStr == "SOUND") { // loaded_sound + ParseAsset_LoadedSound(aZoneFileStream); + } else if (typeStr == "COLLISION MAP") { // col_map_mp + ParseAsset_ColMapMP(aZoneFileStream); + } else if (typeStr == "MP MAP") { // game_map_sp + ParseAsset_GameMapSP(aZoneFileStream); + } else if (typeStr == "SP MAP") { // game_map_mp + ParseAsset_GameMapMP(aZoneFileStream); + } else if (typeStr == "LIGHT DEF") { // lightdef + ParseAsset_LightDef(aZoneFileStream); + } else if (typeStr == "UI MAP") { // ui_map + ParseAsset_UIMap(aZoneFileStream); + } else if (typeStr == "SND DRIVER GLOBALS") { // snddriverglobals + ParseAsset_SNDDriverGlobals(aZoneFileStream); + } else if (typeStr == "AI TYPE") { // aitype + ParseAsset_AIType(aZoneFileStream); + } else if (typeStr == "EFFECT") { // aitype + ParseAsset_FX(aZoneFileStream); + } else if (typeStr == "ANIMATION") { // aitype + result.animations << ParseAsset_Animation(aZoneFileStream); + } else if (typeStr == "STRING TABLE") { // string_table + result.stringTables << ParseAsset_StringTable(aZoneFileStream); + } else if (typeStr == "MENU") { // string_table + ParseAsset_MenuFile(aZoneFileStream); + } else if (typeStr == "WEAPON") { // string_table + ParseAsset_Weapon(aZoneFileStream); + } else if (typeStr == "D3DBSP DUMP") { // string_table + ParseAsset_D3DBSP(aZoneFileStream); + } else if (typeStr != "UNKNOWN") { + qDebug() << "Found bad asset type!" << typeStr; + } + } + return result; + } + + static LocalString ParseAsset_LocalString(QDataStream *aZoneFileStream) { + LocalString result; + + // Skip separator + aZoneFileStream->skipRawData(8); + + // Parse local string asset contents + QString localStr; + char localStrChar; + *aZoneFileStream >> localStrChar; + while (localStrChar != 0) { + result.string += localStrChar; + *aZoneFileStream >> localStrChar; + } + + // Parse rawfile name + QString aliasName; + char aliasNameChar; + *aZoneFileStream >> aliasNameChar; + while (aliasNameChar != 0) { + result.alias += aliasNameChar; + *aZoneFileStream >> aliasNameChar; + } + return result; + } + + static RawFile ParseAsset_RawFile(QDataStream *aZoneFileStream) { + RawFile result; + + // Skip start separator FF FF FF FF (pointer?) + aZoneFileStream->skipRawData(4); + + *aZoneFileStream >> result.length; + + // Skip unknown 4 byte data + aZoneFileStream->skipRawData(4); + + // Parse rawfile path + char scriptPathChar; + *aZoneFileStream >> scriptPathChar; + while (scriptPathChar != 0) { + result.path += scriptPathChar; + *aZoneFileStream >> scriptPathChar; + } + result.path.replace(",", ""); + const QStringList pathParts = result.path.split('/'); + if (pathParts.size() == 0) { + qDebug() << "Failed to parse ff path! " << result.path; + exit(-1); + } + + // Parse gsc contents + char rawFileContentsChar; + *aZoneFileStream >> rawFileContentsChar; + while (rawFileContentsChar != 0 && rawFileContentsChar != -1) { + result.contents += rawFileContentsChar; + *aZoneFileStream >> rawFileContentsChar; + } + return result; + } + + static void ParseAsset_PhysPreset(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_Model(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_Material(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_PixelShader(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static TechSet ParseAsset_TechSet(QDataStream *aZoneFileStream) { + TechSet result; + + aZoneFileStream->skipRawData(4); + // Parse techset name + char techSetNameChar; + *aZoneFileStream >> techSetNameChar; + while (techSetNameChar == 0) { + *aZoneFileStream >> techSetNameChar; + } + while (techSetNameChar != 0) { + result.name += techSetNameChar; + *aZoneFileStream >> techSetNameChar; + } + result.name.replace(",", ""); + + return result; + } + + static void ParseAsset_Image(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_LoadedSound(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_ColMapMP(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_GameMapSP(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_GameMapMP(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_LightDef(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_UIMap(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_SNDDriverGlobals(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_AIType(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_FX(QDataStream *aZoneFileStream) { + Q_UNUSED(aZoneFileStream); + } + + static Animation ParseAsset_Animation(QDataStream *aZoneFileStream) { + Animation result; + + aZoneFileStream->skipRawData(4); + + *aZoneFileStream + >> result.dataByteCount + >> result.dataShortCount + >> result.dataIntCount + >> result.randomDataByteCount + >> result.randomDataIntCount + >> result.numframes + >> result.isLooped + >> result.isDelta + >> result.noneRotatedBoneCount + >> result.twoDRotatedBoneCount + >> result.normalRotatedBoneCount + >> result.twoDStaticRotatedBoneCount + >> result.normalStaticRotatedBoneCount + >> result.normalTranslatedBoneCount + >> result.preciseTranslatedBoneCount + >> result.staticTranslatedBoneCount + >> result.noneTranslatedBoneCount + >> result.totalBoneCount + >> result.otherBoneCount1 + >> result.otherBoneCount2 + >> result.notifyCount + >> result.assetType + >> result.pad + >> result.randomDataShortCount + >> result.indexCount + >> result.frameRate + >> result.frequency + >> result.boneIDsPtr + >> result.dataBytePtr + >> result.dataShortPtr + >> result.dataIntPtr + >> result.randomDataShortPtr + >> result.randomDataBytePtr + >> result.randomDataIntPtr + >> result.longIndiciesPtr + >> result.notificationsPtr + >> result.deltaPartsPtr; + + // Read in x_anim file name + QString xAnimName; + char xAnimNameChar; + *aZoneFileStream >> xAnimNameChar; + while (xAnimNameChar != 0) { + result.name += xAnimNameChar; + *aZoneFileStream >> xAnimNameChar; + } + + // Parse x_anim index header + QVector sectionLengths; + for (int i = 0; i < result.numframes; i++) { + quint8 sectionlength; + *aZoneFileStream >> sectionlength; + sectionLengths.push_back(sectionlength); + // Skip padding + aZoneFileStream->skipRawData(1); + } + // Skip unknown section + aZoneFileStream->skipRawData(2 * 8); + + return result; + } + + static void ParseAsset_MenuFile(QDataStream *aZoneFileStream) { + //MENU_FILE + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_Weapon(QDataStream *aZoneFileStream) { + //WEAPON_FILE + Q_UNUSED(aZoneFileStream); + } + + static void ParseAsset_D3DBSP(QDataStream *aZoneFileStream) { + //D3DBSP_DUMP + Q_UNUSED(aZoneFileStream); + } + + static StringTable ParseAsset_StringTable(QDataStream *aZoneFileStream) { + StringTable result; + + aZoneFileStream->skipRawData(4); + + *aZoneFileStream + >> result.columnCount + >> result.rowCount; + + // Todo fix this + result.columnCount = 0; + result.rowCount = 0; + + aZoneFileStream->skipRawData(4); + + QString stringTableName; + char stringTableNameChar; + *aZoneFileStream >> stringTableNameChar; + while (stringTableNameChar != 0) { + result.name += stringTableNameChar; + *aZoneFileStream >> stringTableNameChar; + } + + QVector tablePointers = QVector(); + for (quint32 i = 0; i < result.rowCount; i++) { + QByteArray pointerData(4, Qt::Uninitialized); + aZoneFileStream->readRawData(pointerData.data(), 4); + tablePointers.push_back(pointerData.toHex()); + + aZoneFileStream->skipRawData(4); + } + + for (const QString &pointerAddr : tablePointers) { + QString leadingContent = ""; + if (pointerAddr == "FFFFFFFF") { + char leadingContentChar; + *aZoneFileStream >> leadingContentChar; + while (leadingContentChar != 0) { + leadingContent += leadingContentChar; + *aZoneFileStream >> leadingContentChar; + } + } else { + leadingContent = pointerAddr; + } + + QString content; + char contentChar; + *aZoneFileStream >> contentChar; + while (contentChar != 0) { + content += contentChar; + *aZoneFileStream >> contentChar; + } + QPair tableEntry = QPair(); + tableEntry.first = leadingContent; + tableEntry.second = content; + //if (!mStrTableMap.contains(stringTableName)) { + // mStrTableMap[stringTableName] = QVector>(); + //} + //mStrTableMap[stringTableName].push_back(tableEntry); + } + return result; + } +}; + +#endif // ZFPARSER_H