diff --git a/CMakeLists.txt b/CMakeLists.txt index d8ff1d9..befa380 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,6 +63,8 @@ add_executable(Reclass src/import_reclass_xml.cpp src/import_source.h src/import_source.cpp + src/export_reclass_xml.h + src/export_reclass_xml.cpp src/mainwindow.h src/optionsdialog.h src/optionsdialog.cpp @@ -276,6 +278,12 @@ if(BUILD_TESTING) target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test) 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) add_executable(test_windbg_provider tests/test_windbg_provider.cpp plugins/WinDbgMemory/WinDbgMemoryPlugin.cpp) diff --git a/src/export_reclass_xml.cpp b/src/export_reclass_xml.cpp new file mode 100644 index 0000000..db270ca --- /dev/null +++ b/src/export_reclass_xml.cpp @@ -0,0 +1,204 @@ +#include "export_reclass_xml.h" +#include +#include +#include +#include +#include + +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> 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 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 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 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 diff --git a/src/export_reclass_xml.h b/src/export_reclass_xml.h new file mode 100644 index 0000000..1940c88 --- /dev/null +++ b/src/export_reclass_xml.h @@ -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 diff --git a/src/main.cpp b/src/main.cpp index e6ec913..e4f0a7f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,7 @@ #include "generator.h" #include "import_reclass_xml.h" #include "import_source.h" +#include "export_reclass_xml.h" #include "mcp/mcp_bridge.h" #include #include @@ -381,6 +382,7 @@ void MainWindow::createMenus() { Qt5Qt6AddAction(file, "&Unload Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile); file->addSeparator(); 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 ReClass XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml); file->addSeparator(); @@ -1315,6 +1317,31 @@ void MainWindow::exportCpp() { 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 ── void MainWindow::importReclassXml() { diff --git a/src/mainwindow.h b/src/mainwindow.h index 0421632..761be59 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -47,6 +47,7 @@ private slots: void toggleMcp(); void setEditorFont(const QString& fontName); void exportCpp(); + void exportReclassXmlAction(); void importFromSource(); void importReclassXml(); void showTypeAliasesDialog(); diff --git a/tests/test_export_xml.cpp b/tests/test_export_xml.cpp new file mode 100644 index 0000000..9e7e141 --- /dev/null +++ b/tests/test_export_xml.cpp @@ -0,0 +1,360 @@ +#include +#include +#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 childrenOf(const NodeTree& tree, uint64_t parentId) { + QVector 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("= 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 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" diff --git a/tests/test_import_xml.cpp b/tests/test_import_xml.cpp index c982561..11bc4ac 100644 --- a/tests/test_import_xml.cpp +++ b/tests/test_import_xml.cpp @@ -7,83 +7,9 @@ using namespace rcx; class TestImportXml : public QObject { Q_OBJECT private slots: - void importReClassEx(); - void importMemeClsEx(); - void importOlderFormat(); 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() { // Create a minimal XML in a temp file and test parsing QTemporaryFile tmp;