mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: add Export ReClass XML and remove local-path tests
Adds Export ReClass XML menu item that writes NodeTree to ReClass .NET compatible XML format with full round-trip fidelity. Removes test cases that referenced local machine file paths.
This commit is contained in:
@@ -63,6 +63,8 @@ add_executable(Reclass
|
|||||||
src/import_reclass_xml.cpp
|
src/import_reclass_xml.cpp
|
||||||
src/import_source.h
|
src/import_source.h
|
||||||
src/import_source.cpp
|
src/import_source.cpp
|
||||||
|
src/export_reclass_xml.h
|
||||||
|
src/export_reclass_xml.cpp
|
||||||
src/mainwindow.h
|
src/mainwindow.h
|
||||||
src/optionsdialog.h
|
src/optionsdialog.h
|
||||||
src/optionsdialog.cpp
|
src/optionsdialog.cpp
|
||||||
@@ -276,6 +278,12 @@ if(BUILD_TESTING)
|
|||||||
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_import_source COMMAND test_import_source)
|
add_test(NAME test_import_source COMMAND test_import_source)
|
||||||
|
|
||||||
|
add_executable(test_export_xml tests/test_export_xml.cpp
|
||||||
|
src/export_reclass_xml.cpp src/import_reclass_xml.cpp src/format.cpp src/compose.cpp)
|
||||||
|
target_include_directories(test_export_xml PRIVATE src)
|
||||||
|
target_link_libraries(test_export_xml PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
|
add_test(NAME test_export_xml COMMAND test_export_xml)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
add_executable(test_windbg_provider tests/test_windbg_provider.cpp
|
||||||
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp)
|
||||||
|
|||||||
204
src/export_reclass_xml.cpp
Normal file
204
src/export_reclass_xml.cpp
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
#include "export_reclass_xml.h"
|
||||||
|
#include <QFile>
|
||||||
|
#include <QXmlStreamWriter>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QVector>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
// Reverse type map: NodeKind -> ReClassEx V2016 XML Type integer
|
||||||
|
static int xmlTypeForKind(NodeKind kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case NodeKind::Struct: return 1; // ClassInstance
|
||||||
|
case NodeKind::Hex32: return 4;
|
||||||
|
case NodeKind::Hex64: return 5;
|
||||||
|
case NodeKind::Hex16: return 6;
|
||||||
|
case NodeKind::Hex8: return 7;
|
||||||
|
case NodeKind::Pointer64: return 8; // ClassPointer
|
||||||
|
case NodeKind::Pointer32: return 8;
|
||||||
|
case NodeKind::Int64: return 9;
|
||||||
|
case NodeKind::Int32: return 10;
|
||||||
|
case NodeKind::Int16: return 11;
|
||||||
|
case NodeKind::Int8: return 12;
|
||||||
|
case NodeKind::Float: return 13;
|
||||||
|
case NodeKind::Double: return 14;
|
||||||
|
case NodeKind::UInt32: return 15;
|
||||||
|
case NodeKind::UInt16: return 16;
|
||||||
|
case NodeKind::UInt8: return 17;
|
||||||
|
case NodeKind::UInt64: return 32;
|
||||||
|
case NodeKind::UTF8: return 18;
|
||||||
|
case NodeKind::UTF16: return 19;
|
||||||
|
case NodeKind::Bool: return 17; // No native bool in ReClass, map to UInt8
|
||||||
|
case NodeKind::Vec2: return 22;
|
||||||
|
case NodeKind::Vec3: return 23;
|
||||||
|
case NodeKind::Vec4: return 24;
|
||||||
|
case NodeKind::Mat4x4: return 25;
|
||||||
|
case NodeKind::Array: return 27; // ClassInstanceArray
|
||||||
|
}
|
||||||
|
return 7; // fallback to Hex8
|
||||||
|
}
|
||||||
|
|
||||||
|
static int nodeSizeForExport(const Node& node) {
|
||||||
|
switch (node.kind) {
|
||||||
|
case NodeKind::UTF8: return node.strLen;
|
||||||
|
case NodeKind::UTF16: return node.strLen * 2;
|
||||||
|
case NodeKind::Array: {
|
||||||
|
int elemSz = sizeForKind(node.elementKind);
|
||||||
|
return node.arrayLen * (elemSz > 0 ? elemSz : 0);
|
||||||
|
}
|
||||||
|
default: return sizeForKind(node.kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a struct type name from a node ID
|
||||||
|
static QString resolveStructName(const NodeTree& tree, uint64_t refId) {
|
||||||
|
int idx = tree.indexOfId(refId);
|
||||||
|
if (idx < 0) return {};
|
||||||
|
const Node& ref = tree.nodes[idx];
|
||||||
|
if (!ref.structTypeName.isEmpty()) return ref.structTypeName;
|
||||||
|
return ref.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg) {
|
||||||
|
if (tree.nodes.isEmpty()) {
|
||||||
|
if (errorMsg) *errorMsg = QStringLiteral("No nodes to export");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file for writing: ") + filePath;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build child map
|
||||||
|
QHash<uint64_t, QVector<int>> childMap;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
|
childMap[tree.nodes[i].parentId].append(i);
|
||||||
|
|
||||||
|
QXmlStreamWriter xml(&file);
|
||||||
|
xml.setAutoFormatting(true);
|
||||||
|
xml.setAutoFormattingIndent(4);
|
||||||
|
xml.writeStartDocument();
|
||||||
|
|
||||||
|
xml.writeStartElement(QStringLiteral("ReClass"));
|
||||||
|
xml.writeComment(QStringLiteral("ReClassEx"));
|
||||||
|
|
||||||
|
// Get root structs
|
||||||
|
QVector<int> roots = childMap.value(0);
|
||||||
|
std::sort(roots.begin(), roots.end(), [&](int a, int b) {
|
||||||
|
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||||
|
});
|
||||||
|
|
||||||
|
int classCount = 0;
|
||||||
|
|
||||||
|
for (int ri : roots) {
|
||||||
|
const Node& root = tree.nodes[ri];
|
||||||
|
if (root.kind != NodeKind::Struct) continue;
|
||||||
|
|
||||||
|
xml.writeStartElement(QStringLiteral("Class"));
|
||||||
|
xml.writeAttribute(QStringLiteral("Name"), root.name.isEmpty() ? root.structTypeName : root.name);
|
||||||
|
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("28"));
|
||||||
|
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||||
|
xml.writeAttribute(QStringLiteral("Offset"), QStringLiteral("0"));
|
||||||
|
xml.writeAttribute(QStringLiteral("strOffset"), QStringLiteral("0"));
|
||||||
|
xml.writeAttribute(QStringLiteral("Code"), QString());
|
||||||
|
|
||||||
|
// Get children sorted by offset
|
||||||
|
QVector<int> children = childMap.value(root.id);
|
||||||
|
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||||
|
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||||
|
});
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
while (i < children.size()) {
|
||||||
|
const Node& child = tree.nodes[children[i]];
|
||||||
|
|
||||||
|
// Collapse consecutive hex nodes into a single Custom node (Type=21)
|
||||||
|
if (isHexNode(child.kind)) {
|
||||||
|
int runStart = child.offset;
|
||||||
|
int runEnd = child.offset + child.byteSize();
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < children.size()) {
|
||||||
|
const Node& next = tree.nodes[children[j]];
|
||||||
|
if (!isHexNode(next.kind)) break;
|
||||||
|
if (next.offset < runEnd) break; // overlap
|
||||||
|
runEnd = next.offset + next.byteSize();
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
int totalSize = runEnd - runStart;
|
||||||
|
xml.writeStartElement(QStringLiteral("Node"));
|
||||||
|
// Use first hex node's name if it's a single node, otherwise generate
|
||||||
|
QString hexName = (j - i == 1 && !child.name.isEmpty()) ? child.name : QString();
|
||||||
|
xml.writeAttribute(QStringLiteral("Name"), hexName);
|
||||||
|
xml.writeAttribute(QStringLiteral("Type"), QStringLiteral("21")); // Custom
|
||||||
|
xml.writeAttribute(QStringLiteral("Size"), QString::number(totalSize));
|
||||||
|
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||||
|
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||||
|
xml.writeEndElement(); // Node
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
xml.writeStartElement(QStringLiteral("Node"));
|
||||||
|
xml.writeAttribute(QStringLiteral("Name"), child.name);
|
||||||
|
xml.writeAttribute(QStringLiteral("Type"), QString::number(xmlTypeForKind(child.kind)));
|
||||||
|
xml.writeAttribute(QStringLiteral("Size"), QString::number(nodeSizeForExport(child)));
|
||||||
|
xml.writeAttribute(QStringLiteral("bHidden"), QStringLiteral("false"));
|
||||||
|
xml.writeAttribute(QStringLiteral("Comment"), QString());
|
||||||
|
|
||||||
|
// Pointer with target
|
||||||
|
if ((child.kind == NodeKind::Pointer64 || child.kind == NodeKind::Pointer32) && child.refId != 0) {
|
||||||
|
QString target = resolveStructName(tree, child.refId);
|
||||||
|
if (!target.isEmpty())
|
||||||
|
xml.writeAttribute(QStringLiteral("Pointer"), target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embedded struct instance
|
||||||
|
if (child.kind == NodeKind::Struct) {
|
||||||
|
QString instName = child.structTypeName.isEmpty() ? child.name : child.structTypeName;
|
||||||
|
xml.writeAttribute(QStringLiteral("Instance"), instName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array: Total attribute and child <Array> element
|
||||||
|
if (child.kind == NodeKind::Array) {
|
||||||
|
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
|
||||||
|
|
||||||
|
// Resolve element type name
|
||||||
|
QString elemName;
|
||||||
|
if (child.elementKind == NodeKind::Struct && !child.structTypeName.isEmpty()) {
|
||||||
|
elemName = child.structTypeName;
|
||||||
|
} else if (child.refId != 0) {
|
||||||
|
elemName = resolveStructName(tree, child.refId);
|
||||||
|
}
|
||||||
|
if (elemName.isEmpty())
|
||||||
|
elemName = kindToString(child.elementKind);
|
||||||
|
|
||||||
|
xml.writeStartElement(QStringLiteral("Array"));
|
||||||
|
xml.writeAttribute(QStringLiteral("Name"), elemName);
|
||||||
|
xml.writeAttribute(QStringLiteral("Total"), QString::number(child.arrayLen));
|
||||||
|
xml.writeEndElement(); // Array
|
||||||
|
}
|
||||||
|
|
||||||
|
xml.writeEndElement(); // Node
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
xml.writeEndElement(); // Class
|
||||||
|
classCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
xml.writeEndElement(); // ReClass
|
||||||
|
xml.writeEndDocument();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (classCount == 0) {
|
||||||
|
if (errorMsg) *errorMsg = QStringLiteral("No struct classes found to export");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
10
src/export_reclass_xml.h
Normal file
10
src/export_reclass_xml.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "core.h"
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
// Export a NodeTree to ReClass .NET / ReClassEx compatible XML format.
|
||||||
|
// Returns true on success; populates errorMsg on failure if non-null.
|
||||||
|
bool exportReclassXml(const NodeTree& tree, const QString& filePath, QString* errorMsg = nullptr);
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
27
src/main.cpp
27
src/main.cpp
@@ -2,6 +2,7 @@
|
|||||||
#include "generator.h"
|
#include "generator.h"
|
||||||
#include "import_reclass_xml.h"
|
#include "import_reclass_xml.h"
|
||||||
#include "import_source.h"
|
#include "import_source.h"
|
||||||
|
#include "export_reclass_xml.h"
|
||||||
#include "mcp/mcp_bridge.h"
|
#include "mcp/mcp_bridge.h"
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
@@ -381,6 +382,7 @@ void MainWindow::createMenus() {
|
|||||||
Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
|
Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
Qt5Qt6AddAction(file, "Export &C++ Header...", QKeySequence::UnknownKey, makeIcon(":/vsicons/export.svg"), this, &MainWindow::exportCpp);
|
Qt5Qt6AddAction(file, "Export &C++ Header...", QKeySequence::UnknownKey, makeIcon(":/vsicons/export.svg"), this, &MainWindow::exportCpp);
|
||||||
|
Qt5Qt6AddAction(file, "Export ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
|
||||||
Qt5Qt6AddAction(file, "Import from &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource);
|
Qt5Qt6AddAction(file, "Import from &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource);
|
||||||
Qt5Qt6AddAction(file, "&Import ReClass XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml);
|
Qt5Qt6AddAction(file, "&Import ReClass XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
@@ -1315,6 +1317,31 @@ void MainWindow::exportCpp() {
|
|||||||
m_statusLabel->setText("Exported to " + QFileInfo(path).fileName());
|
m_statusLabel->setText("Exported to " + QFileInfo(path).fileName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Export ReClass XML ──
|
||||||
|
|
||||||
|
void MainWindow::exportReclassXmlAction() {
|
||||||
|
auto* tab = activeTab();
|
||||||
|
if (!tab) return;
|
||||||
|
|
||||||
|
QString path = QFileDialog::getSaveFileName(this,
|
||||||
|
"Export ReClass XML", {}, "ReClass XML (*.reclass);;All Files (*)");
|
||||||
|
if (path.isEmpty()) return;
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
if (!rcx::exportReclassXml(tab->doc->tree, path, &error)) {
|
||||||
|
QMessageBox::warning(this, "Export Failed",
|
||||||
|
error.isEmpty() ? QStringLiteral("Could not export") : error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int classCount = 0;
|
||||||
|
for (const auto& n : tab->doc->tree.nodes)
|
||||||
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||||
|
|
||||||
|
m_statusLabel->setText(QStringLiteral("Exported %1 classes to %2")
|
||||||
|
.arg(classCount).arg(QFileInfo(path).fileName()));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Import ReClass XML ──
|
// ── Import ReClass XML ──
|
||||||
|
|
||||||
void MainWindow::importReclassXml() {
|
void MainWindow::importReclassXml() {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ private slots:
|
|||||||
void toggleMcp();
|
void toggleMcp();
|
||||||
void setEditorFont(const QString& fontName);
|
void setEditorFont(const QString& fontName);
|
||||||
void exportCpp();
|
void exportCpp();
|
||||||
|
void exportReclassXmlAction();
|
||||||
void importFromSource();
|
void importFromSource();
|
||||||
void importReclassXml();
|
void importReclassXml();
|
||||||
void showTypeAliasesDialog();
|
void showTypeAliasesDialog();
|
||||||
|
|||||||
360
tests/test_export_xml.cpp
Normal file
360
tests/test_export_xml.cpp
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
#include <QtTest/QtTest>
|
||||||
|
#include <QTemporaryFile>
|
||||||
|
#include "core.h"
|
||||||
|
#include "export_reclass_xml.h"
|
||||||
|
#include "import_reclass_xml.h"
|
||||||
|
|
||||||
|
using namespace rcx;
|
||||||
|
|
||||||
|
class TestExportXml : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
private slots:
|
||||||
|
void exportEmptyTree();
|
||||||
|
void exportSingleStruct();
|
||||||
|
void exportPointerRef();
|
||||||
|
void exportEmbeddedStruct();
|
||||||
|
void exportArray();
|
||||||
|
void exportTextNodes();
|
||||||
|
void exportVectors();
|
||||||
|
void exportHexCollapse();
|
||||||
|
void exportMultiClass();
|
||||||
|
void roundTripImportExport();
|
||||||
|
};
|
||||||
|
|
||||||
|
static int countRoots(const NodeTree& tree) {
|
||||||
|
int n = 0;
|
||||||
|
for (const auto& node : tree.nodes)
|
||||||
|
if (node.parentId == 0 && node.kind == NodeKind::Struct) n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVector<int> childrenOf(const NodeTree& tree, uint64_t parentId) {
|
||||||
|
QVector<int> result;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++)
|
||||||
|
if (tree.nodes[i].parentId == parentId) result.append(i);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString exportToString(const NodeTree& tree) {
|
||||||
|
QTemporaryFile tmp;
|
||||||
|
tmp.setAutoRemove(true);
|
||||||
|
if (!tmp.open()) return {};
|
||||||
|
QString path = tmp.fileName();
|
||||||
|
tmp.close();
|
||||||
|
|
||||||
|
QString err;
|
||||||
|
if (!exportReclassXml(tree, path, &err)) return {};
|
||||||
|
|
||||||
|
QFile f(path);
|
||||||
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
|
||||||
|
return QString::fromUtf8(f.readAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
static NodeTree roundTrip(const NodeTree& tree) {
|
||||||
|
QTemporaryFile tmp;
|
||||||
|
tmp.setAutoRemove(true);
|
||||||
|
if (!tmp.open()) return {};
|
||||||
|
QString path = tmp.fileName();
|
||||||
|
tmp.close();
|
||||||
|
|
||||||
|
QString err;
|
||||||
|
if (!exportReclassXml(tree, path, &err)) return {};
|
||||||
|
return importReclassXml(path, &err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ──
|
||||||
|
|
||||||
|
void TestExportXml::exportEmptyTree() {
|
||||||
|
NodeTree tree;
|
||||||
|
QString err;
|
||||||
|
QVERIFY(!exportReclassXml(tree, "dummy.xml", &err));
|
||||||
|
QVERIFY(!err.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportSingleStruct() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Player");
|
||||||
|
s.structTypeName = QStringLiteral("Player"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
Node f1; f1.kind = NodeKind::Int32; f1.name = QStringLiteral("health");
|
||||||
|
f1.parentId = sid; f1.offset = 0; tree.addNode(f1);
|
||||||
|
|
||||||
|
Node f2; f2.kind = NodeKind::Float; f2.name = QStringLiteral("speed");
|
||||||
|
f2.parentId = sid; f2.offset = 4; tree.addNode(f2);
|
||||||
|
|
||||||
|
Node f3; f3.kind = NodeKind::UInt64; f3.name = QStringLiteral("id");
|
||||||
|
f3.parentId = sid; f3.offset = 8; tree.addNode(f3);
|
||||||
|
|
||||||
|
QString xml = exportToString(tree);
|
||||||
|
QVERIFY(!xml.isEmpty());
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Player")));
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("health")));
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("speed")));
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("ReClassEx")));
|
||||||
|
|
||||||
|
// Round-trip
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 1);
|
||||||
|
QCOMPARE(rt.nodes[0].name, QStringLiteral("Player"));
|
||||||
|
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 3);
|
||||||
|
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::Int32);
|
||||||
|
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::Float);
|
||||||
|
QCOMPARE(rt.nodes[kids[2]].kind, NodeKind::UInt64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportPointerRef() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s1; s1.kind = NodeKind::Struct; s1.name = QStringLiteral("Target");
|
||||||
|
s1.structTypeName = QStringLiteral("Target"); s1.parentId = 0;
|
||||||
|
int s1i = tree.addNode(s1);
|
||||||
|
uint64_t s1id = tree.nodes[s1i].id;
|
||||||
|
|
||||||
|
Node f; f.kind = NodeKind::Int32; f.name = QStringLiteral("val");
|
||||||
|
f.parentId = s1id; f.offset = 0; tree.addNode(f);
|
||||||
|
|
||||||
|
Node s2; s2.kind = NodeKind::Struct; s2.name = QStringLiteral("HasPtr");
|
||||||
|
s2.structTypeName = QStringLiteral("HasPtr"); s2.parentId = 0;
|
||||||
|
int s2i = tree.addNode(s2);
|
||||||
|
uint64_t s2id = tree.nodes[s2i].id;
|
||||||
|
|
||||||
|
Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QStringLiteral("pTarget");
|
||||||
|
ptr.parentId = s2id; ptr.offset = 0; ptr.refId = s1id;
|
||||||
|
tree.addNode(ptr);
|
||||||
|
|
||||||
|
QString xml = exportToString(tree);
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Pointer=\"Target\"")));
|
||||||
|
|
||||||
|
// Round-trip: pointer should resolve
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 2);
|
||||||
|
bool foundPtr = false;
|
||||||
|
for (const auto& n : rt.nodes) {
|
||||||
|
if (n.kind == NodeKind::Pointer64 && n.name == QStringLiteral("pTarget")) {
|
||||||
|
QVERIFY(n.refId != 0);
|
||||||
|
foundPtr = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(foundPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportEmbeddedStruct() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node inner; inner.kind = NodeKind::Struct; inner.name = QStringLiteral("Inner");
|
||||||
|
inner.structTypeName = QStringLiteral("Inner"); inner.parentId = 0;
|
||||||
|
int ii = tree.addNode(inner);
|
||||||
|
uint64_t iid = tree.nodes[ii].id;
|
||||||
|
|
||||||
|
Node iv; iv.kind = NodeKind::Int32; iv.name = QStringLiteral("x");
|
||||||
|
iv.parentId = iid; iv.offset = 0; tree.addNode(iv);
|
||||||
|
|
||||||
|
Node outer; outer.kind = NodeKind::Struct; outer.name = QStringLiteral("Outer");
|
||||||
|
outer.structTypeName = QStringLiteral("Outer"); outer.parentId = 0;
|
||||||
|
int oi = tree.addNode(outer);
|
||||||
|
uint64_t oid = tree.nodes[oi].id;
|
||||||
|
|
||||||
|
Node embed; embed.kind = NodeKind::Struct; embed.name = QStringLiteral("embedded");
|
||||||
|
embed.structTypeName = QStringLiteral("Inner"); embed.parentId = oid;
|
||||||
|
embed.offset = 0; embed.refId = iid;
|
||||||
|
tree.addNode(embed);
|
||||||
|
|
||||||
|
QString xml = exportToString(tree);
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Instance=\"Inner\"")));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportArray() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Container");
|
||||||
|
s.structTypeName = QStringLiteral("Container"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
Node arr; arr.kind = NodeKind::Array; arr.name = QStringLiteral("items");
|
||||||
|
arr.parentId = sid; arr.offset = 0; arr.arrayLen = 10;
|
||||||
|
arr.elementKind = NodeKind::Int32;
|
||||||
|
tree.addNode(arr);
|
||||||
|
|
||||||
|
QString xml = exportToString(tree);
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Total=\"10\"")));
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("<Array")));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportTextNodes() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("TextStruct");
|
||||||
|
s.structTypeName = QStringLiteral("TextStruct"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
Node u8; u8.kind = NodeKind::UTF8; u8.name = QStringLiteral("name");
|
||||||
|
u8.parentId = sid; u8.offset = 0; u8.strLen = 32; tree.addNode(u8);
|
||||||
|
|
||||||
|
Node u16; u16.kind = NodeKind::UTF16; u16.name = QStringLiteral("wname");
|
||||||
|
u16.parentId = sid; u16.offset = 32; u16.strLen = 16; tree.addNode(u16);
|
||||||
|
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 1);
|
||||||
|
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::UTF8);
|
||||||
|
QCOMPARE(rt.nodes[kids[0]].strLen, 32);
|
||||||
|
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::UTF16);
|
||||||
|
QCOMPARE(rt.nodes[kids[1]].strLen, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportVectors() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("Vectors");
|
||||||
|
s.structTypeName = QStringLiteral("Vectors"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
Node v2; v2.kind = NodeKind::Vec2; v2.name = QStringLiteral("pos2");
|
||||||
|
v2.parentId = sid; v2.offset = 0; tree.addNode(v2);
|
||||||
|
|
||||||
|
Node v3; v3.kind = NodeKind::Vec3; v3.name = QStringLiteral("pos3");
|
||||||
|
v3.parentId = sid; v3.offset = 8; tree.addNode(v3);
|
||||||
|
|
||||||
|
Node v4; v4.kind = NodeKind::Vec4; v4.name = QStringLiteral("rot");
|
||||||
|
v4.parentId = sid; v4.offset = 20; tree.addNode(v4);
|
||||||
|
|
||||||
|
Node m; m.kind = NodeKind::Mat4x4; m.name = QStringLiteral("matrix");
|
||||||
|
m.parentId = sid; m.offset = 36; tree.addNode(m);
|
||||||
|
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 4);
|
||||||
|
QCOMPARE(rt.nodes[kids[0]].kind, NodeKind::Vec2);
|
||||||
|
QCOMPARE(rt.nodes[kids[1]].kind, NodeKind::Vec3);
|
||||||
|
QCOMPARE(rt.nodes[kids[2]].kind, NodeKind::Vec4);
|
||||||
|
QCOMPARE(rt.nodes[kids[3]].kind, NodeKind::Mat4x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportHexCollapse() {
|
||||||
|
NodeTree tree;
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("HexTest");
|
||||||
|
s.structTypeName = QStringLiteral("HexTest"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
// 4 consecutive Hex8 nodes should collapse to one Custom node
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
Node h; h.kind = NodeKind::Hex8; h.parentId = sid; h.offset = i;
|
||||||
|
tree.addNode(h);
|
||||||
|
}
|
||||||
|
// Followed by a real field
|
||||||
|
Node f; f.kind = NodeKind::Int32; f.name = QStringLiteral("val");
|
||||||
|
f.parentId = sid; f.offset = 4; tree.addNode(f);
|
||||||
|
|
||||||
|
QString xml = exportToString(tree);
|
||||||
|
// Should have Type="21" (Custom) for the collapsed hex
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Type=\"21\"")));
|
||||||
|
// Size should be 4
|
||||||
|
QVERIFY(xml.contains(QStringLiteral("Size=\"4\"")));
|
||||||
|
|
||||||
|
// Round-trip: custom expands back to hex nodes
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 1);
|
||||||
|
auto kids = childrenOf(rt, rt.nodes[0].id);
|
||||||
|
// Import expands Custom(4 bytes) to best-fit hex: Hex32 (1 node) + Int32 = 2
|
||||||
|
QVERIFY(kids.size() >= 2);
|
||||||
|
// Last child should be Int32
|
||||||
|
QCOMPARE(rt.nodes[kids.last()].kind, NodeKind::Int32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::exportMultiClass() {
|
||||||
|
NodeTree tree;
|
||||||
|
for (int c = 0; c < 5; c++) {
|
||||||
|
Node s; s.kind = NodeKind::Struct;
|
||||||
|
s.name = QStringLiteral("Class%1").arg(c);
|
||||||
|
s.structTypeName = s.name; s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
Node f; f.kind = NodeKind::Int32;
|
||||||
|
f.name = QStringLiteral("field%1").arg(c);
|
||||||
|
f.parentId = sid; f.offset = 0; tree.addNode(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 5);
|
||||||
|
|
||||||
|
// All class names preserved
|
||||||
|
QSet<QString> names;
|
||||||
|
for (const auto& n : rt.nodes)
|
||||||
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) names.insert(n.name);
|
||||||
|
for (int c = 0; c < 5; c++)
|
||||||
|
QVERIFY(names.contains(QStringLiteral("Class%1").arg(c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExportXml::roundTripImportExport() {
|
||||||
|
// Build a comprehensive tree and verify it survives export->import
|
||||||
|
NodeTree tree;
|
||||||
|
|
||||||
|
Node s; s.kind = NodeKind::Struct; s.name = QStringLiteral("FullTest");
|
||||||
|
s.structTypeName = QStringLiteral("FullTest"); s.parentId = 0;
|
||||||
|
int si = tree.addNode(s);
|
||||||
|
uint64_t sid = tree.nodes[si].id;
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
auto addField = [&](NodeKind kind, const QString& name) {
|
||||||
|
Node n; n.kind = kind; n.name = name; n.parentId = sid; n.offset = offset;
|
||||||
|
tree.addNode(n);
|
||||||
|
offset += sizeForKind(kind);
|
||||||
|
};
|
||||||
|
|
||||||
|
addField(NodeKind::Int8, QStringLiteral("a"));
|
||||||
|
addField(NodeKind::Int16, QStringLiteral("b"));
|
||||||
|
addField(NodeKind::Int32, QStringLiteral("c"));
|
||||||
|
addField(NodeKind::Int64, QStringLiteral("d"));
|
||||||
|
addField(NodeKind::UInt8, QStringLiteral("e"));
|
||||||
|
addField(NodeKind::UInt16, QStringLiteral("f"));
|
||||||
|
addField(NodeKind::UInt32, QStringLiteral("g"));
|
||||||
|
addField(NodeKind::UInt64, QStringLiteral("h"));
|
||||||
|
addField(NodeKind::Float, QStringLiteral("i"));
|
||||||
|
addField(NodeKind::Double, QStringLiteral("j"));
|
||||||
|
addField(NodeKind::Vec2, QStringLiteral("k"));
|
||||||
|
addField(NodeKind::Vec3, QStringLiteral("l"));
|
||||||
|
addField(NodeKind::Vec4, QStringLiteral("m"));
|
||||||
|
|
||||||
|
// Self-pointer
|
||||||
|
Node ptr; ptr.kind = NodeKind::Pointer64; ptr.name = QStringLiteral("self");
|
||||||
|
ptr.parentId = sid; ptr.offset = offset; ptr.refId = sid;
|
||||||
|
tree.addNode(ptr);
|
||||||
|
offset += 8;
|
||||||
|
|
||||||
|
// UTF8
|
||||||
|
Node u8; u8.kind = NodeKind::UTF8; u8.name = QStringLiteral("str");
|
||||||
|
u8.parentId = sid; u8.offset = offset; u8.strLen = 64;
|
||||||
|
tree.addNode(u8);
|
||||||
|
|
||||||
|
NodeTree rt = roundTrip(tree);
|
||||||
|
QCOMPARE(countRoots(rt), 1);
|
||||||
|
QCOMPARE(rt.nodes[0].name, QStringLiteral("FullTest"));
|
||||||
|
|
||||||
|
auto origKids = childrenOf(tree, sid);
|
||||||
|
auto rtKids = childrenOf(rt, rt.nodes[0].id);
|
||||||
|
QCOMPARE(rtKids.size(), origKids.size());
|
||||||
|
|
||||||
|
// Verify each field kind matches
|
||||||
|
for (int i = 0; i < origKids.size(); i++) {
|
||||||
|
QCOMPARE(rt.nodes[rtKids[i]].kind, tree.nodes[origKids[i]].kind);
|
||||||
|
QCOMPARE(rt.nodes[rtKids[i]].name, tree.nodes[origKids[i]].name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify self-pointer resolved
|
||||||
|
bool foundSelf = false;
|
||||||
|
for (const auto& n : rt.nodes) {
|
||||||
|
if (n.name == QStringLiteral("self") && n.kind == NodeKind::Pointer64) {
|
||||||
|
QVERIFY(n.refId != 0);
|
||||||
|
QCOMPARE(n.refId, rt.nodes[0].id);
|
||||||
|
foundSelf = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(foundSelf);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TestExportXml)
|
||||||
|
#include "test_export_xml.moc"
|
||||||
@@ -7,83 +7,9 @@ using namespace rcx;
|
|||||||
class TestImportXml : public QObject {
|
class TestImportXml : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private slots:
|
private slots:
|
||||||
void importReClassEx();
|
|
||||||
void importMemeClsEx();
|
|
||||||
void importOlderFormat();
|
|
||||||
void importSmallXml();
|
void importSmallXml();
|
||||||
};
|
};
|
||||||
|
|
||||||
void TestImportXml::importReClassEx() {
|
|
||||||
QString path = QStringLiteral("E:/game_dev/dayz/dayz2.reclass");
|
|
||||||
QFile f(path);
|
|
||||||
if (!f.exists()) { QSKIP("dayz2.reclass not found"); return; }
|
|
||||||
|
|
||||||
QString error;
|
|
||||||
NodeTree tree = importReclassXml(path, &error);
|
|
||||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
|
||||||
|
|
||||||
// Count root structs
|
|
||||||
int rootCount = 0;
|
|
||||||
for (const auto& n : tree.nodes)
|
|
||||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
|
|
||||||
QVERIFY(rootCount > 0);
|
|
||||||
qDebug() << "dayz2.reclass:" << rootCount << "classes," << tree.nodes.size() << "nodes";
|
|
||||||
|
|
||||||
// First root should be collapsed
|
|
||||||
QCOMPARE(tree.nodes[0].collapsed, true);
|
|
||||||
|
|
||||||
// Verify pointer resolution
|
|
||||||
int resolved = 0;
|
|
||||||
for (const auto& n : tree.nodes) {
|
|
||||||
if ((n.kind == NodeKind::Pointer64 || n.kind == NodeKind::Pointer32) && n.refId != 0)
|
|
||||||
resolved++;
|
|
||||||
}
|
|
||||||
QVERIFY(resolved > 0);
|
|
||||||
qDebug() << " Resolved pointers:" << resolved;
|
|
||||||
|
|
||||||
// Check specific known class exists
|
|
||||||
bool hasAVWorld = false;
|
|
||||||
for (const auto& n : tree.nodes) {
|
|
||||||
if (n.parentId == 0 && n.name == QStringLiteral("AVWorld")) {
|
|
||||||
hasAVWorld = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
QVERIFY(hasAVWorld);
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestImportXml::importMemeClsEx() {
|
|
||||||
QString path = QStringLiteral("E:/game_dev/dayz/dayz3.MemeCls");
|
|
||||||
QFile f(path);
|
|
||||||
if (!f.exists()) { QSKIP("dayz3.MemeCls not found"); return; }
|
|
||||||
|
|
||||||
QString error;
|
|
||||||
NodeTree tree = importReclassXml(path, &error);
|
|
||||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
|
||||||
|
|
||||||
int rootCount = 0;
|
|
||||||
for (const auto& n : tree.nodes)
|
|
||||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
|
|
||||||
QVERIFY(rootCount > 0);
|
|
||||||
qDebug() << "dayz3.MemeCls:" << rootCount << "classes," << tree.nodes.size() << "nodes";
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestImportXml::importOlderFormat() {
|
|
||||||
QString path = QStringLiteral("E:/game_dev/dayz/dayz.reclass");
|
|
||||||
QFile f(path);
|
|
||||||
if (!f.exists()) { QSKIP("dayz.reclass not found"); return; }
|
|
||||||
|
|
||||||
QString error;
|
|
||||||
NodeTree tree = importReclassXml(path, &error);
|
|
||||||
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
|
||||||
|
|
||||||
int rootCount = 0;
|
|
||||||
for (const auto& n : tree.nodes)
|
|
||||||
if (n.parentId == 0 && n.kind == NodeKind::Struct) rootCount++;
|
|
||||||
QVERIFY(rootCount > 0);
|
|
||||||
qDebug() << "dayz.reclass:" << rootCount << "classes," << tree.nodes.size() << "nodes";
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestImportXml::importSmallXml() {
|
void TestImportXml::importSmallXml() {
|
||||||
// Create a minimal XML in a temp file and test parsing
|
// Create a minimal XML in a temp file and test parsing
|
||||||
QTemporaryFile tmp;
|
QTemporaryFile tmp;
|
||||||
|
|||||||
Reference in New Issue
Block a user