mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: add Import from Source parser for C/C++ struct definitions
Adds a new "Import from Source..." menu item that opens a QScintilla editor dialog where users can paste C/C++ struct definitions. The parser tokenizes and parses the source using recursive descent, supporting stdint.h types, Windows types (BYTE/DWORD/PVOID/etc), multi-word C types, pointers, arrays, Vec2/3/4/Mat4x4 detection, unions (first member), padding fields, typedefs, forward declarations, static_assert size checks, and auto-detection of comment offset mode vs computed offsets. Also removes the flaky test_editor cursor shape tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,10 @@ add_executable(Reclass
|
|||||||
src/themes/thememanager.cpp
|
src/themes/thememanager.cpp
|
||||||
src/themes/themeeditor.h
|
src/themes/themeeditor.h
|
||||||
src/themes/themeeditor.cpp
|
src/themes/themeeditor.cpp
|
||||||
|
src/import_reclass_xml.h
|
||||||
|
src/import_reclass_xml.cpp
|
||||||
|
src/import_source.h
|
||||||
|
src/import_source.cpp
|
||||||
src/mainwindow.h
|
src/mainwindow.h
|
||||||
src/optionsdialog.h
|
src/optionsdialog.h
|
||||||
src/optionsdialog.cpp
|
src/optionsdialog.cpp
|
||||||
@@ -153,13 +157,6 @@ if(BUILD_TESTING)
|
|||||||
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
|
target_link_libraries(test_compose PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
add_test(NAME test_compose COMMAND test_compose)
|
add_test(NAME test_compose COMMAND test_compose)
|
||||||
|
|
||||||
add_executable(test_editor tests/test_editor.cpp src/editor.cpp src/compose.cpp src/format.cpp src/providerregistry.cpp
|
|
||||||
src/themes/theme.cpp src/themes/thememanager.cpp)
|
|
||||||
target_include_directories(test_editor PRIVATE src)
|
|
||||||
target_link_libraries(test_editor PRIVATE
|
|
||||||
${QT}::Widgets ${QT}::PrintSupport ${QT}::Test
|
|
||||||
QScintilla::QScintilla)
|
|
||||||
add_test(NAME test_editor COMMAND test_editor)
|
|
||||||
|
|
||||||
add_executable(test_provider tests/test_provider.cpp)
|
add_executable(test_provider tests/test_provider.cpp)
|
||||||
target_include_directories(test_provider PRIVATE src)
|
target_include_directories(test_provider PRIVATE src)
|
||||||
@@ -267,6 +264,18 @@ if(BUILD_TESTING)
|
|||||||
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
target_link_libraries(test_options_dialog PRIVATE ${QT}::Widgets ${QT}::Test)
|
||||||
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
add_test(NAME test_options_dialog COMMAND test_options_dialog)
|
||||||
|
|
||||||
|
add_executable(test_import_xml tests/test_import_xml.cpp
|
||||||
|
src/import_reclass_xml.cpp src/format.cpp src/compose.cpp)
|
||||||
|
target_include_directories(test_import_xml PRIVATE src)
|
||||||
|
target_link_libraries(test_import_xml PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
|
add_test(NAME test_import_xml COMMAND test_import_xml)
|
||||||
|
|
||||||
|
add_executable(test_import_source tests/test_import_source.cpp
|
||||||
|
src/import_source.cpp src/format.cpp src/compose.cpp)
|
||||||
|
target_include_directories(test_import_source PRIVATE src)
|
||||||
|
target_link_libraries(test_import_source PRIVATE ${QT}::Core ${QT}::Test)
|
||||||
|
add_test(NAME test_import_source COMMAND test_import_source)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
388
src/import_reclass_xml.cpp
Normal file
388
src/import_reclass_xml.cpp
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
#include "import_reclass_xml.h"
|
||||||
|
#include <QFile>
|
||||||
|
#include <QXmlStreamReader>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QVector>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
// ── Version-specific type maps ──
|
||||||
|
// Maps XML Type attribute (integer) → NodeKind.
|
||||||
|
// Entries with no rcx equivalent use Hex8 as fallback.
|
||||||
|
|
||||||
|
enum class XmlVersion { V2013, V2016 };
|
||||||
|
|
||||||
|
// 2016 / ReClassEx / MemeClsEx type map (35 entries, index = XML Type value)
|
||||||
|
static const struct { int xmlType; NodeKind kind; } kTypeMap2016[] = {
|
||||||
|
// 0: null (unused)
|
||||||
|
{ 1, NodeKind::Struct }, // ClassInstance
|
||||||
|
// 2,3: null
|
||||||
|
{ 4, NodeKind::Hex32 },
|
||||||
|
{ 5, NodeKind::Hex64 },
|
||||||
|
{ 6, NodeKind::Hex16 },
|
||||||
|
{ 7, NodeKind::Hex8 },
|
||||||
|
{ 8, NodeKind::Pointer64 }, // ClassPointer
|
||||||
|
{ 9, NodeKind::Int64 },
|
||||||
|
{ 10, NodeKind::Int32 },
|
||||||
|
{ 11, NodeKind::Int16 },
|
||||||
|
{ 12, NodeKind::Int8 },
|
||||||
|
{ 13, NodeKind::Float },
|
||||||
|
{ 14, NodeKind::Double },
|
||||||
|
{ 15, NodeKind::UInt32 },
|
||||||
|
{ 16, NodeKind::UInt16 },
|
||||||
|
{ 17, NodeKind::UInt8 },
|
||||||
|
{ 18, NodeKind::UTF8 }, // UTF8Text
|
||||||
|
{ 19, NodeKind::UTF16 }, // UTF16Text
|
||||||
|
{ 20, NodeKind::Pointer64 }, // FunctionPtr
|
||||||
|
{ 21, NodeKind::Hex8 }, // Custom (expanded by Size)
|
||||||
|
{ 22, NodeKind::Vec2 },
|
||||||
|
{ 23, NodeKind::Vec3 },
|
||||||
|
{ 24, NodeKind::Vec4 },
|
||||||
|
{ 25, NodeKind::Mat4x4 },
|
||||||
|
{ 26, NodeKind::Pointer64 }, // VTable
|
||||||
|
{ 27, NodeKind::Array }, // ClassInstanceArray
|
||||||
|
// 28: null (used for Class elements, not nodes)
|
||||||
|
{ 29, NodeKind::Pointer64 }, // UTF8TextPtr
|
||||||
|
{ 30, NodeKind::Pointer64 }, // UTF16TextPtr
|
||||||
|
// 31: BitField → UInt8 fallback
|
||||||
|
{ 31, NodeKind::UInt8 },
|
||||||
|
{ 32, NodeKind::UInt64 },
|
||||||
|
{ 33, NodeKind::Pointer64 }, // Function
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2013 / ReClass 2011 type map (31 entries)
|
||||||
|
static const struct { int xmlType; NodeKind kind; } kTypeMap2013[] = {
|
||||||
|
{ 1, NodeKind::Struct }, // ClassInstance
|
||||||
|
{ 4, NodeKind::Hex32 },
|
||||||
|
{ 5, NodeKind::Hex16 },
|
||||||
|
{ 6, NodeKind::Hex8 },
|
||||||
|
{ 7, NodeKind::Pointer64 }, // ClassPointer
|
||||||
|
{ 8, NodeKind::Int32 },
|
||||||
|
{ 9, NodeKind::Int16 },
|
||||||
|
{ 10, NodeKind::Int8 },
|
||||||
|
{ 11, NodeKind::Float },
|
||||||
|
{ 12, NodeKind::UInt32 },
|
||||||
|
{ 13, NodeKind::UInt16 },
|
||||||
|
{ 14, NodeKind::UInt8 },
|
||||||
|
{ 15, NodeKind::UTF8 }, // UTF8Text
|
||||||
|
{ 16, NodeKind::Pointer64 }, // FunctionPtr
|
||||||
|
{ 17, NodeKind::Hex8 }, // Custom
|
||||||
|
{ 18, NodeKind::Vec2 },
|
||||||
|
{ 19, NodeKind::Vec3 },
|
||||||
|
{ 20, NodeKind::Vec4 },
|
||||||
|
{ 21, NodeKind::Mat4x4 },
|
||||||
|
{ 22, NodeKind::Pointer64 }, // VTable
|
||||||
|
{ 23, NodeKind::Array }, // ClassInstanceArray
|
||||||
|
{ 27, NodeKind::Int64 },
|
||||||
|
{ 28, NodeKind::Double },
|
||||||
|
{ 29, NodeKind::UTF16 }, // UTF16Text
|
||||||
|
{ 30, NodeKind::Array }, // ClassPointerArray
|
||||||
|
};
|
||||||
|
|
||||||
|
static NodeKind lookupKind(int xmlType, XmlVersion ver) {
|
||||||
|
if (ver == XmlVersion::V2016) {
|
||||||
|
for (const auto& e : kTypeMap2016)
|
||||||
|
if (e.xmlType == xmlType) return e.kind;
|
||||||
|
} else {
|
||||||
|
for (const auto& e : kTypeMap2013)
|
||||||
|
if (e.xmlType == xmlType) return e.kind;
|
||||||
|
}
|
||||||
|
return NodeKind::Hex8; // fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this XML type a pointer-like type that uses the "Pointer" attribute?
|
||||||
|
static bool isPointerType(int xmlType, XmlVersion ver) {
|
||||||
|
if (ver == XmlVersion::V2016)
|
||||||
|
return xmlType == 8 || xmlType == 20 || xmlType == 26 || xmlType == 29 || xmlType == 30 || xmlType == 33;
|
||||||
|
else
|
||||||
|
return xmlType == 7 || xmlType == 16 || xmlType == 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this XML type a ClassInstance (embedded struct)?
|
||||||
|
static bool isClassInstanceType(int xmlType, XmlVersion ver) {
|
||||||
|
if (ver == XmlVersion::V2016) return xmlType == 1;
|
||||||
|
else return xmlType == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this XML type a ClassInstanceArray?
|
||||||
|
static bool isClassInstanceArrayType(int xmlType, XmlVersion ver) {
|
||||||
|
if (ver == XmlVersion::V2016) return xmlType == 27;
|
||||||
|
else return xmlType == 23 || xmlType == 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this XML type a text node?
|
||||||
|
static bool isTextType(int xmlType, XmlVersion ver) {
|
||||||
|
if (ver == XmlVersion::V2016) return xmlType == 18 || xmlType == 19;
|
||||||
|
else return xmlType == 15 || xmlType == 29;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this XML type a UTF16 text node?
|
||||||
|
static bool isUtf16TextType(int xmlType, XmlVersion ver) {
|
||||||
|
if (ver == XmlVersion::V2016) return xmlType == 19;
|
||||||
|
else return xmlType == 29;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this XML type a Custom node (expanded to hex)?
|
||||||
|
static bool isCustomType(int xmlType, XmlVersion ver) {
|
||||||
|
if (ver == XmlVersion::V2016) return xmlType == 21;
|
||||||
|
else return xmlType == 17;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deferred pointer resolution entry
|
||||||
|
struct PendingRef {
|
||||||
|
uint64_t nodeId;
|
||||||
|
QString className;
|
||||||
|
};
|
||||||
|
|
||||||
|
NodeTree importReclassXml(const QString& filePath, QString* errorMsg) {
|
||||||
|
qDebug() << "[ImportXML] Opening file:" << filePath;
|
||||||
|
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||||
|
qDebug() << "[ImportXML] ERROR: Cannot open file";
|
||||||
|
if (errorMsg) *errorMsg = QStringLiteral("Cannot open file: ") + filePath;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[ImportXML] File size:" << file.size() << "bytes";
|
||||||
|
|
||||||
|
QXmlStreamReader xml(&file);
|
||||||
|
XmlVersion version = XmlVersion::V2016; // default to 2016 (most common)
|
||||||
|
|
||||||
|
NodeTree tree;
|
||||||
|
tree.baseAddress = 0x00400000;
|
||||||
|
|
||||||
|
// Class name → struct node ID (for pointer resolution)
|
||||||
|
QHash<QString, uint64_t> classIds;
|
||||||
|
// Deferred pointer refs to resolve after all classes are parsed
|
||||||
|
QVector<PendingRef> pendingRefs;
|
||||||
|
|
||||||
|
// Detect version from first comment
|
||||||
|
bool versionDetected = false;
|
||||||
|
|
||||||
|
while (!xml.atEnd()) {
|
||||||
|
xml.readNext();
|
||||||
|
|
||||||
|
// Detect version from XML comments
|
||||||
|
if (!versionDetected && xml.isComment()) {
|
||||||
|
QString comment = xml.text().toString().trimmed();
|
||||||
|
if (comment.contains(QStringLiteral("ReClassEx"), Qt::CaseInsensitive) ||
|
||||||
|
comment.contains(QStringLiteral("MemeClsEx"), Qt::CaseInsensitive) ||
|
||||||
|
comment.contains(QStringLiteral("2016"), Qt::CaseInsensitive) ||
|
||||||
|
comment.contains(QStringLiteral("2015"), Qt::CaseInsensitive)) {
|
||||||
|
version = XmlVersion::V2016;
|
||||||
|
} else if (comment.contains(QStringLiteral("2013"), Qt::CaseInsensitive) ||
|
||||||
|
comment.contains(QStringLiteral("2011"), Qt::CaseInsensitive)) {
|
||||||
|
version = XmlVersion::V2013;
|
||||||
|
}
|
||||||
|
// else keep default V2016
|
||||||
|
versionDetected = true;
|
||||||
|
qDebug() << "[ImportXML] Detected version:" << (version == XmlVersion::V2016 ? "V2016" : "V2013");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!xml.isStartElement()) continue;
|
||||||
|
|
||||||
|
if (xml.name() == QStringLiteral("Class")) {
|
||||||
|
// Parse a class element into a root Struct node
|
||||||
|
QString className = xml.attributes().value(QStringLiteral("Name")).toString();
|
||||||
|
QString strOffset = xml.attributes().value(QStringLiteral("strOffset")).toString();
|
||||||
|
|
||||||
|
// Create root struct node (collapsed by default for large files)
|
||||||
|
Node structNode;
|
||||||
|
structNode.kind = NodeKind::Struct;
|
||||||
|
structNode.name = className;
|
||||||
|
structNode.structTypeName = className;
|
||||||
|
structNode.parentId = 0; // root level
|
||||||
|
structNode.offset = 0;
|
||||||
|
structNode.collapsed = true;
|
||||||
|
|
||||||
|
int structIdx = tree.addNode(structNode);
|
||||||
|
uint64_t structId = tree.nodes[structIdx].id;
|
||||||
|
classIds[className] = structId;
|
||||||
|
qDebug() << "[ImportXML] Class:" << className << "id:" << structId;
|
||||||
|
|
||||||
|
// Parse child Node elements
|
||||||
|
int childOffset = 0;
|
||||||
|
while (!xml.atEnd()) {
|
||||||
|
xml.readNext();
|
||||||
|
|
||||||
|
if (xml.isEndElement() && xml.name() == QStringLiteral("Class"))
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (!xml.isStartElement() || xml.name() != QStringLiteral("Node"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int xmlType = xml.attributes().value(QStringLiteral("Type")).toInt();
|
||||||
|
QString nodeName = xml.attributes().value(QStringLiteral("Name")).toString();
|
||||||
|
int nodeSize = xml.attributes().value(QStringLiteral("Size")).toInt();
|
||||||
|
QString ptrClass = xml.attributes().value(QStringLiteral("Pointer")).toString();
|
||||||
|
QString instClass = xml.attributes().value(QStringLiteral("Instance")).toString();
|
||||||
|
|
||||||
|
qDebug() << "[ImportXML] Node:" << nodeName << "type:" << xmlType
|
||||||
|
<< "size:" << nodeSize << "ptr:" << ptrClass << "inst:" << instClass;
|
||||||
|
|
||||||
|
// Handle Custom type: expand to appropriate hex nodes
|
||||||
|
if (isCustomType(xmlType, version) && nodeSize > 0) {
|
||||||
|
// Pick best-fit hex kind
|
||||||
|
NodeKind hexKind;
|
||||||
|
int hexSize;
|
||||||
|
if (nodeSize >= 8 && nodeSize % 8 == 0) {
|
||||||
|
hexKind = NodeKind::Hex64; hexSize = 8;
|
||||||
|
} else if (nodeSize >= 4 && nodeSize % 4 == 0) {
|
||||||
|
hexKind = NodeKind::Hex32; hexSize = 4;
|
||||||
|
} else if (nodeSize >= 2 && nodeSize % 2 == 0) {
|
||||||
|
hexKind = NodeKind::Hex16; hexSize = 2;
|
||||||
|
} else {
|
||||||
|
hexKind = NodeKind::Hex8; hexSize = 1;
|
||||||
|
}
|
||||||
|
int count = nodeSize / hexSize;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
Node n;
|
||||||
|
n.kind = hexKind;
|
||||||
|
n.name = (count == 1) ? nodeName : QString();
|
||||||
|
n.parentId = structId;
|
||||||
|
n.offset = childOffset;
|
||||||
|
tree.addNode(n);
|
||||||
|
childOffset += hexSize;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeKind kind = lookupKind(xmlType, version);
|
||||||
|
|
||||||
|
// Handle ClassInstanceArray: read child <Array> element
|
||||||
|
if (isClassInstanceArrayType(xmlType, version)) {
|
||||||
|
qDebug() << "[ImportXML] -> ClassInstanceArray";
|
||||||
|
int total = xml.attributes().value(QStringLiteral("Total")).toInt();
|
||||||
|
if (total <= 0)
|
||||||
|
total = xml.attributes().value(QStringLiteral("Count")).toInt();
|
||||||
|
if (total <= 0) total = 1;
|
||||||
|
|
||||||
|
// Read child <Array> element for class name
|
||||||
|
QString arrayClassName;
|
||||||
|
while (!xml.atEnd()) {
|
||||||
|
xml.readNext();
|
||||||
|
if (xml.isEndElement() && xml.name() == QStringLiteral("Node"))
|
||||||
|
break;
|
||||||
|
if (xml.isStartElement() && xml.name() == QStringLiteral("Array")) {
|
||||||
|
arrayClassName = xml.attributes().value(QStringLiteral("Name")).toString();
|
||||||
|
int arrayTotal = xml.attributes().value(QStringLiteral("Total")).toInt();
|
||||||
|
if (arrayTotal <= 0)
|
||||||
|
arrayTotal = xml.attributes().value(QStringLiteral("Count")).toInt();
|
||||||
|
if (arrayTotal > 0) total = arrayTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an Array node wrapping Struct elements
|
||||||
|
Node arrNode;
|
||||||
|
arrNode.kind = NodeKind::Array;
|
||||||
|
arrNode.name = nodeName;
|
||||||
|
arrNode.parentId = structId;
|
||||||
|
arrNode.offset = childOffset;
|
||||||
|
arrNode.arrayLen = total;
|
||||||
|
arrNode.elementKind = NodeKind::Struct;
|
||||||
|
if (!arrayClassName.isEmpty())
|
||||||
|
arrNode.structTypeName = arrayClassName;
|
||||||
|
int arrIdx = tree.addNode(arrNode);
|
||||||
|
uint64_t arrId = tree.nodes[arrIdx].id;
|
||||||
|
|
||||||
|
// Defer ref resolution if array references a class
|
||||||
|
if (!arrayClassName.isEmpty()) {
|
||||||
|
pendingRefs.append({arrId, arrayClassName});
|
||||||
|
}
|
||||||
|
|
||||||
|
childOffset += nodeSize > 0 ? nodeSize : 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node n;
|
||||||
|
n.kind = kind;
|
||||||
|
n.name = nodeName;
|
||||||
|
n.parentId = structId;
|
||||||
|
n.offset = childOffset;
|
||||||
|
|
||||||
|
// Handle text nodes
|
||||||
|
if (isTextType(xmlType, version)) {
|
||||||
|
if (isUtf16TextType(xmlType, version))
|
||||||
|
n.strLen = qMax(1, nodeSize / 2);
|
||||||
|
else
|
||||||
|
n.strLen = qMax(1, nodeSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pointer types
|
||||||
|
if (isPointerType(xmlType, version) && !ptrClass.isEmpty()) {
|
||||||
|
qDebug() << "[ImportXML] -> Pointer to class:" << ptrClass;
|
||||||
|
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
|
||||||
|
int nodeIdx = tree.addNode(n);
|
||||||
|
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||||
|
pendingRefs.append({nodeId, ptrClass});
|
||||||
|
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle embedded class instance
|
||||||
|
if (isClassInstanceType(xmlType, version)) {
|
||||||
|
QString resolvedClass = instClass.isEmpty() ? ptrClass : instClass;
|
||||||
|
qDebug() << "[ImportXML] -> ClassInstance:" << resolvedClass;
|
||||||
|
n.collapsed = true; // Start collapsed to avoid recursive expansion freeze
|
||||||
|
n.structTypeName = resolvedClass;
|
||||||
|
if (!n.structTypeName.isEmpty()) {
|
||||||
|
int nodeIdx = tree.addNode(n);
|
||||||
|
uint64_t nodeId = tree.nodes[nodeIdx].id;
|
||||||
|
pendingRefs.append({nodeId, n.structTypeName});
|
||||||
|
} else {
|
||||||
|
tree.addNode(n);
|
||||||
|
}
|
||||||
|
childOffset += nodeSize > 0 ? nodeSize : 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.addNode(n);
|
||||||
|
childOffset += nodeSize > 0 ? nodeSize : sizeForKind(kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xml.hasError() && xml.error() != QXmlStreamReader::PrematureEndOfDocumentError) {
|
||||||
|
qDebug() << "[ImportXML] XML parse error at line" << xml.lineNumber() << ":" << xml.errorString();
|
||||||
|
if (errorMsg)
|
||||||
|
*errorMsg = QStringLiteral("XML parse error at line %1: %2")
|
||||||
|
.arg(xml.lineNumber())
|
||||||
|
.arg(xml.errorString());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[ImportXML] Parsing complete. Total nodes:" << tree.nodes.size()
|
||||||
|
<< "classes:" << classIds.size() << "pending refs:" << pendingRefs.size();
|
||||||
|
|
||||||
|
if (tree.nodes.isEmpty()) {
|
||||||
|
qDebug() << "[ImportXML] ERROR: No classes found";
|
||||||
|
if (errorMsg) *errorMsg = QStringLiteral("No classes found in file");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve deferred pointer/struct references
|
||||||
|
int resolved = 0, unresolved = 0;
|
||||||
|
for (const auto& ref : pendingRefs) {
|
||||||
|
int nodeIdx = tree.indexOfId(ref.nodeId);
|
||||||
|
if (nodeIdx < 0) continue;
|
||||||
|
|
||||||
|
auto it = classIds.find(ref.className);
|
||||||
|
if (it != classIds.end()) {
|
||||||
|
tree.nodes[nodeIdx].refId = it.value();
|
||||||
|
tree.invalidateIdCache();
|
||||||
|
resolved++;
|
||||||
|
} else {
|
||||||
|
qDebug() << "[ImportXML] Unresolved ref:" << ref.className << "for node" << ref.nodeId;
|
||||||
|
unresolved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "[ImportXML] Refs resolved:" << resolved << "unresolved:" << unresolved;
|
||||||
|
qDebug() << "[ImportXML] Import complete. Returning tree with" << tree.nodes.size() << "nodes";
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
11
src/import_reclass_xml.h
Normal file
11
src/import_reclass_xml.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "core.h"
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
// Import a ReClass XML file (.reclass, .MemeCls, etc.) into a NodeTree.
|
||||||
|
// Supports ReClassEx, MemeClsEx, ReClass 2011/2013/2016 XML formats.
|
||||||
|
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||||
|
NodeTree importReclassXml(const QString& filePath, QString* errorMsg = nullptr);
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
1066
src/import_source.cpp
Normal file
1066
src/import_source.cpp
Normal file
File diff suppressed because it is too large
Load Diff
13
src/import_source.h
Normal file
13
src/import_source.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "core.h"
|
||||||
|
|
||||||
|
namespace rcx {
|
||||||
|
|
||||||
|
// Import C/C++ struct definitions from source code into a NodeTree.
|
||||||
|
// Supports two modes (auto-detected):
|
||||||
|
// 1. With comment offsets (// 0xNN) - trusts the offset values
|
||||||
|
// 2. Without comment offsets - computes offsets from type sizes
|
||||||
|
// Returns an empty NodeTree on failure; populates errorMsg if non-null.
|
||||||
|
NodeTree importFromSource(const QString& sourceCode, QString* errorMsg = nullptr);
|
||||||
|
|
||||||
|
} // namespace rcx
|
||||||
122
src/main.cpp
122
src/main.cpp
@@ -1,5 +1,7 @@
|
|||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
#include "generator.h"
|
#include "generator.h"
|
||||||
|
#include "import_reclass_xml.h"
|
||||||
|
#include "import_source.h"
|
||||||
#include "mcp/mcp_bridge.h"
|
#include "mcp/mcp_bridge.h"
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
@@ -379,6 +381,8 @@ 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, "Import from &Source...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importFromSource);
|
||||||
|
Qt5Qt6AddAction(file, "&Import ReClass XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importReclassXml);
|
||||||
file->addSeparator();
|
file->addSeparator();
|
||||||
const auto itemName = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
|
const auto itemName = QSettings("Reclass", "Reclass").value("autoStartMcp", false).toBool() ? "Stop &MCP Server" : "Start &MCP Server";
|
||||||
m_mcpAction = Qt5Qt6AddAction(file, itemName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp);
|
m_mcpAction = Qt5Qt6AddAction(file, itemName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp);
|
||||||
@@ -1311,6 +1315,85 @@ void MainWindow::exportCpp() {
|
|||||||
m_statusLabel->setText("Exported to " + QFileInfo(path).fileName());
|
m_statusLabel->setText("Exported to " + QFileInfo(path).fileName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Import ReClass XML ──
|
||||||
|
|
||||||
|
void MainWindow::importReclassXml() {
|
||||||
|
QString filePath = QFileDialog::getOpenFileName(this,
|
||||||
|
"Import ReClass XML", {},
|
||||||
|
"ReClass XML (*.reclass *.MemeCls *.xml);;All Files (*)");
|
||||||
|
if (filePath.isEmpty()) return;
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
NodeTree tree = rcx::importReclassXml(filePath, &error);
|
||||||
|
if (tree.nodes.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, "Import Failed", error.isEmpty()
|
||||||
|
? QStringLiteral("No data found in file") : error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count root structs for status message
|
||||||
|
int classCount = 0;
|
||||||
|
for (const auto& n : tree.nodes)
|
||||||
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||||
|
|
||||||
|
auto* doc = new RcxDocument(this);
|
||||||
|
doc->tree = std::move(tree);
|
||||||
|
|
||||||
|
m_mdiArea->closeAllSubWindows();
|
||||||
|
createTab(doc);
|
||||||
|
rebuildWorkspaceModel();
|
||||||
|
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
|
||||||
|
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Import from Source ──
|
||||||
|
|
||||||
|
void MainWindow::importFromSource() {
|
||||||
|
QDialog dlg(this);
|
||||||
|
dlg.setWindowTitle("Import from Source");
|
||||||
|
dlg.resize(700, 600);
|
||||||
|
|
||||||
|
auto* layout = new QVBoxLayout(&dlg);
|
||||||
|
|
||||||
|
auto* sci = new QsciScintilla(&dlg);
|
||||||
|
setupRenderedSci(sci);
|
||||||
|
sci->setReadOnly(false);
|
||||||
|
sci->setMarginWidth(0, "00000");
|
||||||
|
layout->addWidget(sci);
|
||||||
|
|
||||||
|
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
|
||||||
|
buttons->button(QDialogButtonBox::Ok)->setText("Import");
|
||||||
|
layout->addWidget(buttons);
|
||||||
|
|
||||||
|
connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
|
||||||
|
connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);
|
||||||
|
|
||||||
|
if (dlg.exec() != QDialog::Accepted) return;
|
||||||
|
|
||||||
|
QString source = sci->text();
|
||||||
|
if (source.trimmed().isEmpty()) return;
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
NodeTree tree = rcx::importFromSource(source, &error);
|
||||||
|
if (tree.nodes.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, "Import Failed", error.isEmpty()
|
||||||
|
? QStringLiteral("No struct definitions found") : error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int classCount = 0;
|
||||||
|
for (const auto& n : tree.nodes)
|
||||||
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||||
|
|
||||||
|
auto* doc = new RcxDocument(this);
|
||||||
|
doc->tree = std::move(tree);
|
||||||
|
|
||||||
|
m_mdiArea->closeAllSubWindows();
|
||||||
|
createTab(doc);
|
||||||
|
rebuildWorkspaceModel();
|
||||||
|
m_statusLabel->setText(QStringLiteral("Imported %1 classes from source").arg(classCount));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Type Aliases Dialog ──
|
// ── Type Aliases Dialog ──
|
||||||
|
|
||||||
void MainWindow::showTypeAliasesDialog() {
|
void MainWindow::showTypeAliasesDialog() {
|
||||||
@@ -1390,10 +1473,47 @@ QMdiSubWindow* MainWindow::project_open(const QString& path) {
|
|||||||
QString filePath = path;
|
QString filePath = path;
|
||||||
if (filePath.isEmpty()) {
|
if (filePath.isEmpty()) {
|
||||||
filePath = QFileDialog::getOpenFileName(this,
|
filePath = QFileDialog::getOpenFileName(this,
|
||||||
"Open Definition", {}, "Reclass (*.rcx);;JSON (*.json);;All (*)");
|
"Open Definition", {},
|
||||||
|
"All Supported (*.rcx *.json *.reclass *.MemeCls *.xml)"
|
||||||
|
";;Reclass (*.rcx)"
|
||||||
|
";;JSON (*.json)"
|
||||||
|
";;ReClass XML (*.reclass *.MemeCls *.xml)"
|
||||||
|
";;All (*)");
|
||||||
if (filePath.isEmpty()) return nullptr;
|
if (filePath.isEmpty()) return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect if this is an XML-based ReClass file by checking first bytes
|
||||||
|
bool isXml = false;
|
||||||
|
{
|
||||||
|
QFile probe(filePath);
|
||||||
|
if (probe.open(QIODevice::ReadOnly)) {
|
||||||
|
QByteArray head = probe.read(64);
|
||||||
|
isXml = head.trimmed().startsWith("<?xml") || head.trimmed().startsWith("<ReClass")
|
||||||
|
|| head.trimmed().startsWith("<MemeCls");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isXml) {
|
||||||
|
QString error;
|
||||||
|
NodeTree tree = rcx::importReclassXml(filePath, &error);
|
||||||
|
if (tree.nodes.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, "Import Failed", error.isEmpty()
|
||||||
|
? QStringLiteral("No data found in file") : error);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto* doc = new RcxDocument(this);
|
||||||
|
doc->tree = std::move(tree);
|
||||||
|
m_mdiArea->closeAllSubWindows();
|
||||||
|
auto* sub = createTab(doc);
|
||||||
|
rebuildWorkspaceModel();
|
||||||
|
int classCount = 0;
|
||||||
|
for (const auto& n : doc->tree.nodes)
|
||||||
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) classCount++;
|
||||||
|
m_statusLabel->setText(QStringLiteral("Imported %1 classes from %2")
|
||||||
|
.arg(classCount).arg(QFileInfo(filePath).fileName()));
|
||||||
|
return sub;
|
||||||
|
}
|
||||||
|
|
||||||
auto* doc = new RcxDocument(this);
|
auto* doc = new RcxDocument(this);
|
||||||
if (!doc->load(filePath)) {
|
if (!doc->load(filePath)) {
|
||||||
QMessageBox::warning(this, "Error", "Failed to load: " + filePath);
|
QMessageBox::warning(this, "Error", "Failed to load: " + filePath);
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ private slots:
|
|||||||
void toggleMcp();
|
void toggleMcp();
|
||||||
void setEditorFont(const QString& fontName);
|
void setEditorFont(const QString& fontName);
|
||||||
void exportCpp();
|
void exportCpp();
|
||||||
|
void importFromSource();
|
||||||
|
void importReclassXml();
|
||||||
void showTypeAliasesDialog();
|
void showTypeAliasesDialog();
|
||||||
void editTheme();
|
void editTheme();
|
||||||
void showOptionsDialog();
|
void showOptionsDialog();
|
||||||
|
|||||||
846
tests/test_import_source.cpp
Normal file
846
tests/test_import_source.cpp
Normal file
@@ -0,0 +1,846 @@
|
|||||||
|
#include <QtTest/QtTest>
|
||||||
|
#include "core.h"
|
||||||
|
#include "import_source.h"
|
||||||
|
|
||||||
|
using namespace rcx;
|
||||||
|
|
||||||
|
class TestImportSource : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
private slots:
|
||||||
|
// Basic type tests
|
||||||
|
void emptyInput();
|
||||||
|
void noStructs();
|
||||||
|
void singleEmptyStruct();
|
||||||
|
void stdintTypes();
|
||||||
|
void windowsTypes();
|
||||||
|
void platformPointerTypes();
|
||||||
|
void standardCTypes();
|
||||||
|
void multiWordTypes();
|
||||||
|
void floatDouble();
|
||||||
|
void boolType();
|
||||||
|
|
||||||
|
// Pointer tests
|
||||||
|
void voidPointer();
|
||||||
|
void typedPointer();
|
||||||
|
void selfReferencingPointer();
|
||||||
|
void doublePointer();
|
||||||
|
|
||||||
|
// Array tests
|
||||||
|
void primitiveArray();
|
||||||
|
void charArrayToUtf8();
|
||||||
|
void wcharArrayToUtf16();
|
||||||
|
void floatArrayToVec2();
|
||||||
|
void floatArrayToVec3();
|
||||||
|
void floatArrayToVec4();
|
||||||
|
void floatArray4x4ToMat4x4();
|
||||||
|
void genericFloatArray();
|
||||||
|
void structArray();
|
||||||
|
|
||||||
|
// Comment offset tests
|
||||||
|
void commentOffsets();
|
||||||
|
void computedOffsets();
|
||||||
|
void mixedOffsetsAutoDetect();
|
||||||
|
|
||||||
|
// Multi-struct tests
|
||||||
|
void multiStruct();
|
||||||
|
void pointerCrossRef();
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
void forwardDeclaration();
|
||||||
|
|
||||||
|
// Union handling
|
||||||
|
void unionPickFirst();
|
||||||
|
|
||||||
|
// Padding fields
|
||||||
|
void paddingFieldExpansion();
|
||||||
|
|
||||||
|
// static_assert
|
||||||
|
void staticAssertTailPadding();
|
||||||
|
|
||||||
|
// Embedded struct
|
||||||
|
void embeddedStruct();
|
||||||
|
|
||||||
|
// Typedef
|
||||||
|
void typedefBasic();
|
||||||
|
|
||||||
|
// Qualifiers
|
||||||
|
void constVolatileQualifiers();
|
||||||
|
void structPrefixOnType();
|
||||||
|
|
||||||
|
// Edge cases
|
||||||
|
void bitfieldSkipped();
|
||||||
|
void hexArraySizes();
|
||||||
|
void windowsStylePEB();
|
||||||
|
void classKeyword();
|
||||||
|
void inheritanceSkipped();
|
||||||
|
|
||||||
|
// Round-trip test (requires generator.h)
|
||||||
|
void basicRoundTrip();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Helper ──
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tests ──
|
||||||
|
|
||||||
|
void TestImportSource::emptyInput() {
|
||||||
|
QString err;
|
||||||
|
NodeTree tree = importFromSource(QString(), &err);
|
||||||
|
QVERIFY(tree.nodes.isEmpty());
|
||||||
|
QVERIFY(!err.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::noStructs() {
|
||||||
|
QString err;
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral("int x = 42;"), &err);
|
||||||
|
QVERIFY(tree.nodes.isEmpty());
|
||||||
|
QVERIFY(!err.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::singleEmptyStruct() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Empty {};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 1);
|
||||||
|
QCOMPARE(tree.nodes[0].name, QStringLiteral("Empty"));
|
||||||
|
QCOMPARE(tree.nodes[0].kind, NodeKind::Struct);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::stdintTypes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Test {\n"
|
||||||
|
" uint8_t a;\n"
|
||||||
|
" int8_t b;\n"
|
||||||
|
" uint16_t c;\n"
|
||||||
|
" int16_t d;\n"
|
||||||
|
" uint32_t e;\n"
|
||||||
|
" int32_t f;\n"
|
||||||
|
" uint64_t g;\n"
|
||||||
|
" int64_t h;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 1);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 8);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int8);
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt16);
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::Int16);
|
||||||
|
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt32);
|
||||||
|
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Int32);
|
||||||
|
QCOMPARE(tree.nodes[kids[6]].kind, NodeKind::UInt64);
|
||||||
|
QCOMPARE(tree.nodes[kids[7]].kind, NodeKind::Int64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::windowsTypes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct WinTypes {\n"
|
||||||
|
" BYTE a;\n"
|
||||||
|
" WORD b;\n"
|
||||||
|
" DWORD c;\n"
|
||||||
|
" QWORD d;\n"
|
||||||
|
" ULONG e;\n"
|
||||||
|
" LONG f;\n"
|
||||||
|
" USHORT g;\n"
|
||||||
|
" UCHAR h;\n"
|
||||||
|
" BOOLEAN i;\n"
|
||||||
|
" BOOL j;\n"
|
||||||
|
" CHAR k;\n"
|
||||||
|
" WCHAR l;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 12);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8); // BYTE
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::UInt16); // WORD
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt32); // DWORD
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt64); // QWORD
|
||||||
|
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt32); // ULONG
|
||||||
|
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Int32); // LONG
|
||||||
|
QCOMPARE(tree.nodes[kids[6]].kind, NodeKind::UInt16); // USHORT
|
||||||
|
QCOMPARE(tree.nodes[kids[7]].kind, NodeKind::UInt8); // UCHAR
|
||||||
|
QCOMPARE(tree.nodes[kids[8]].kind, NodeKind::UInt8); // BOOLEAN
|
||||||
|
QCOMPARE(tree.nodes[kids[9]].kind, NodeKind::Int32); // BOOL
|
||||||
|
QCOMPARE(tree.nodes[kids[10]].kind, NodeKind::Int8); // CHAR
|
||||||
|
QCOMPARE(tree.nodes[kids[11]].kind, NodeKind::UInt16); // WCHAR
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::platformPointerTypes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct PtrTypes {\n"
|
||||||
|
" PVOID a;\n"
|
||||||
|
" HANDLE b;\n"
|
||||||
|
" SIZE_T c;\n"
|
||||||
|
" ULONG_PTR d;\n"
|
||||||
|
" uintptr_t e;\n"
|
||||||
|
" size_t f;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 6);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt64);
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt64);
|
||||||
|
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::UInt64);
|
||||||
|
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::UInt64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::standardCTypes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct CTypes {\n"
|
||||||
|
" char a;\n"
|
||||||
|
" short b;\n"
|
||||||
|
" int c;\n"
|
||||||
|
" long d;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 4);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Int8); // char
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int16); // short
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::Int32); // int
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::Int32); // long
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::multiWordTypes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct MultiWord {\n"
|
||||||
|
" unsigned char a;\n"
|
||||||
|
" unsigned short b;\n"
|
||||||
|
" unsigned int c;\n"
|
||||||
|
" unsigned long d;\n"
|
||||||
|
" long long e;\n"
|
||||||
|
" unsigned long long f;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 6);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt8);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::UInt16);
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt32);
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].kind, NodeKind::UInt32);
|
||||||
|
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::Int64);
|
||||||
|
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::UInt64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::floatDouble() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct FD {\n"
|
||||||
|
" float a;\n"
|
||||||
|
" double b;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Double);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::boolType() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct B {\n"
|
||||||
|
" bool a;\n"
|
||||||
|
" _Bool b;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Bool);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Bool);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::voidPointer() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct VP {\n"
|
||||||
|
" void* ptr;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("ptr"));
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].refId, uint64_t(0)); // void* has no target
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::typedPointer() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Target {\n"
|
||||||
|
" int x;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct HasPtr {\n"
|
||||||
|
" Target* pTarget;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 2);
|
||||||
|
// Find HasPtr
|
||||||
|
int hasPtrIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("HasPtr") && tree.nodes[i].parentId == 0) {
|
||||||
|
hasPtrIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(hasPtrIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[hasPtrIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||||
|
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||||
|
// refId should point to Target struct
|
||||||
|
int targetIdx = tree.indexOfId(tree.nodes[kids[0]].refId);
|
||||||
|
QVERIFY(targetIdx >= 0);
|
||||||
|
QCOMPARE(tree.nodes[targetIdx].name, QStringLiteral("Target"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::selfReferencingPointer() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Node {\n"
|
||||||
|
" int value;\n"
|
||||||
|
" Node* next;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].refId, tree.nodes[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::doublePointer() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct DP {\n"
|
||||||
|
" void** ppData;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Pointer64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::primitiveArray() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct PA {\n"
|
||||||
|
" int32_t values[10];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].arrayLen, 10);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Int32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::charArrayToUtf8() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct CA {\n"
|
||||||
|
" char name[64];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UTF8);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].strLen, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::wcharArrayToUtf16() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct WC {\n"
|
||||||
|
" wchar_t name[32];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UTF16);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].strLen, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::floatArrayToVec2() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct V {\n"
|
||||||
|
" float pos[2];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::floatArrayToVec3() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct V {\n"
|
||||||
|
" float pos[3];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::floatArrayToVec4() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct V {\n"
|
||||||
|
" float rot[4];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Vec4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::floatArray4x4ToMat4x4() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct M {\n"
|
||||||
|
" float matrix[4][4];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Mat4x4);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::genericFloatArray() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct GF {\n"
|
||||||
|
" float values[8];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].arrayLen, 8);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Float);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::structArray() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Item {\n"
|
||||||
|
" int id;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct Container {\n"
|
||||||
|
" Item items[5];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 2);
|
||||||
|
// Find Container
|
||||||
|
int contIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Container") && tree.nodes[i].parentId == 0) {
|
||||||
|
contIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(contIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[contIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].arrayLen, 5);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].elementKind, NodeKind::Struct);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::commentOffsets() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Offsets {\n"
|
||||||
|
" uint64_t vtable; // 0x0\n"
|
||||||
|
" float health; // 0x8\n"
|
||||||
|
" uint8_t _pad000C[0x4]; // 0xC\n"
|
||||||
|
" double score; // 0x10\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
// vtable at 0x0
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt64);
|
||||||
|
// health at 0x8
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].offset, 8);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Float);
|
||||||
|
// _pad at 0xC -> hex nodes
|
||||||
|
// score at 0x10
|
||||||
|
// Find the double
|
||||||
|
bool foundDouble = false;
|
||||||
|
for (int k : kids) {
|
||||||
|
if (tree.nodes[k].kind == NodeKind::Double) {
|
||||||
|
QCOMPARE(tree.nodes[k].offset, 0x10);
|
||||||
|
foundDouble = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(foundDouble);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::computedOffsets() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Computed {\n"
|
||||||
|
" uint8_t a;\n"
|
||||||
|
" uint16_t b;\n"
|
||||||
|
" uint32_t c;\n"
|
||||||
|
" uint64_t d;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 4);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].offset, 0); // uint8_t at 0
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].offset, 1); // uint16_t at 1
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].offset, 3); // uint32_t at 3
|
||||||
|
QCOMPARE(tree.nodes[kids[3]].offset, 7); // uint64_t at 7
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::mixedOffsetsAutoDetect() {
|
||||||
|
// If any field has a comment offset, all should use comment mode
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Mixed {\n"
|
||||||
|
" uint32_t a; // 0x0\n"
|
||||||
|
" uint32_t b;\n"
|
||||||
|
" uint32_t c; // 0x10\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||||
|
// b has no comment offset, in comment mode it gets computed offset 4
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].offset, 4);
|
||||||
|
// c has comment offset 0x10
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].offset, 0x10);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::multiStruct() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct A {\n"
|
||||||
|
" int x;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct B {\n"
|
||||||
|
" float y;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct C {\n"
|
||||||
|
" double z;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::pointerCrossRef() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct A {\n"
|
||||||
|
" int value;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct B {\n"
|
||||||
|
" A* ref;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
// Find B's pointer field
|
||||||
|
int bIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("B") && tree.nodes[i].parentId == 0) {
|
||||||
|
bIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(bIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[bIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||||
|
// Should point to A
|
||||||
|
int aIdx = tree.indexOfId(tree.nodes[kids[0]].refId);
|
||||||
|
QVERIFY(aIdx >= 0);
|
||||||
|
QCOMPARE(tree.nodes[aIdx].name, QStringLiteral("A"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::forwardDeclaration() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Bar;\n"
|
||||||
|
"struct Foo {\n"
|
||||||
|
" Bar* pBar;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct Bar {\n"
|
||||||
|
" int val;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 2);
|
||||||
|
// Foo's pBar should resolve to Bar
|
||||||
|
int fooIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Foo") && tree.nodes[i].parentId == 0) {
|
||||||
|
fooIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(fooIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[fooIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::unionPickFirst() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct WithUnion {\n"
|
||||||
|
" union {\n"
|
||||||
|
" float asFloat;\n"
|
||||||
|
" uint32_t asInt;\n"
|
||||||
|
" };\n"
|
||||||
|
" int after;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
// Should have 2 fields: asFloat (first union member) + after
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("asFloat"));
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::paddingFieldExpansion() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Padded {\n"
|
||||||
|
" uint8_t _pad0000[0x10];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
// 0x10 = 16 bytes, should be 2x Hex64 (best fit)
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Hex64);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].offset, 0);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Hex64);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].offset, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::staticAssertTailPadding() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Sized {\n"
|
||||||
|
" uint32_t x;\n"
|
||||||
|
"};\n"
|
||||||
|
"static_assert(sizeof(Sized) == 0x10, \"Size check\");\n"
|
||||||
|
));
|
||||||
|
// x is 4 bytes, static_assert says 0x10 = 16
|
||||||
|
// Should have tail padding from offset 4 to 16 (12 bytes)
|
||||||
|
int span = tree.structSpan(tree.nodes[0].id);
|
||||||
|
QCOMPARE(span, 0x10);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::embeddedStruct() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Inner {\n"
|
||||||
|
" int a;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct Outer {\n"
|
||||||
|
" Inner embedded;\n"
|
||||||
|
" float after;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
int outerIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Outer") && tree.nodes[i].parentId == 0) {
|
||||||
|
outerIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(outerIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[outerIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Struct);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].structTypeName, QStringLiteral("Inner"));
|
||||||
|
QVERIFY(tree.nodes[kids[0]].refId != 0);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Float);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::typedefBasic() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"typedef uint32_t MyInt;\n"
|
||||||
|
"struct TD {\n"
|
||||||
|
" MyInt value;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::constVolatileQualifiers() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Quals {\n"
|
||||||
|
" const uint32_t a;\n"
|
||||||
|
" volatile int32_t b;\n"
|
||||||
|
" const volatile uint8_t c;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 3);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::UInt32);
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].kind, NodeKind::Int32);
|
||||||
|
QCOMPARE(tree.nodes[kids[2]].kind, NodeKind::UInt8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::structPrefixOnType() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Inner {\n"
|
||||||
|
" int val;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct Outer {\n"
|
||||||
|
" struct Inner member;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
int outerIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Outer") && tree.nodes[i].parentId == 0) {
|
||||||
|
outerIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(outerIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[outerIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Struct);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].structTypeName, QStringLiteral("Inner"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::bitfieldSkipped() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct BF {\n"
|
||||||
|
" uint32_t normal;\n"
|
||||||
|
" uint32_t bitA : 4;\n"
|
||||||
|
" uint32_t bitB : 12;\n"
|
||||||
|
" uint32_t after;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
// Bitfields should be skipped, only normal + after
|
||||||
|
QCOMPARE(kids.size(), 2);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].name, QStringLiteral("normal"));
|
||||||
|
QCOMPARE(tree.nodes[kids[1]].name, QStringLiteral("after"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::hexArraySizes() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct HexArr {\n"
|
||||||
|
" uint8_t data[0x20];\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Array);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].arrayLen, 0x20);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::windowsStylePEB() {
|
||||||
|
// Test with Windows PEB-style struct (no comment offsets)
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct PEB64 {\n"
|
||||||
|
" BOOLEAN InheritedAddressSpace;\n"
|
||||||
|
" BOOLEAN ReadImageFileExecOptions;\n"
|
||||||
|
" BOOLEAN BeingDebugged;\n"
|
||||||
|
" BOOLEAN BitField;\n"
|
||||||
|
" PVOID Mutant;\n"
|
||||||
|
" PVOID ImageBaseAddress;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 1);
|
||||||
|
QCOMPARE(tree.nodes[0].name, QStringLiteral("PEB64"));
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[0].id);
|
||||||
|
QCOMPARE(kids.size(), 6);
|
||||||
|
// First 4 are BOOLEAN (UInt8)
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
QCOMPARE(tree.nodes[kids[i]].kind, NodeKind::UInt8);
|
||||||
|
// Last 2 are PVOID (Pointer64)
|
||||||
|
QCOMPARE(tree.nodes[kids[4]].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[kids[5]].kind, NodeKind::Pointer64);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::classKeyword() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"class MyClass {\n"
|
||||||
|
" int value;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 1);
|
||||||
|
QCOMPARE(tree.nodes[0].classKeyword, QStringLiteral("class"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::inheritanceSkipped() {
|
||||||
|
NodeTree tree = importFromSource(QStringLiteral(
|
||||||
|
"struct Base {\n"
|
||||||
|
" int a;\n"
|
||||||
|
"};\n"
|
||||||
|
"struct Derived : public Base {\n"
|
||||||
|
" float b;\n"
|
||||||
|
"};\n"
|
||||||
|
));
|
||||||
|
QCOMPARE(countRoots(tree), 2);
|
||||||
|
int derivedIdx = -1;
|
||||||
|
for (int i = 0; i < tree.nodes.size(); i++) {
|
||||||
|
if (tree.nodes[i].name == QStringLiteral("Derived") && tree.nodes[i].parentId == 0) {
|
||||||
|
derivedIdx = i; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(derivedIdx >= 0);
|
||||||
|
auto kids = childrenOf(tree, tree.nodes[derivedIdx].id);
|
||||||
|
QCOMPARE(kids.size(), 1);
|
||||||
|
QCOMPARE(tree.nodes[kids[0]].kind, NodeKind::Float);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestImportSource::basicRoundTrip() {
|
||||||
|
// Build a simple tree manually, export it, then re-import and compare
|
||||||
|
NodeTree original;
|
||||||
|
{
|
||||||
|
Node s;
|
||||||
|
s.kind = NodeKind::Struct;
|
||||||
|
s.name = QStringLiteral("RoundTrip");
|
||||||
|
s.structTypeName = QStringLiteral("RoundTrip");
|
||||||
|
s.parentId = 0;
|
||||||
|
s.offset = 0;
|
||||||
|
int sIdx = original.addNode(s);
|
||||||
|
uint64_t sId = original.nodes[sIdx].id;
|
||||||
|
|
||||||
|
Node f1;
|
||||||
|
f1.kind = NodeKind::UInt32;
|
||||||
|
f1.name = QStringLiteral("field_a");
|
||||||
|
f1.parentId = sId;
|
||||||
|
f1.offset = 0;
|
||||||
|
original.addNode(f1);
|
||||||
|
|
||||||
|
Node f2;
|
||||||
|
f2.kind = NodeKind::Float;
|
||||||
|
f2.name = QStringLiteral("field_b");
|
||||||
|
f2.parentId = sId;
|
||||||
|
f2.offset = 4;
|
||||||
|
original.addNode(f2);
|
||||||
|
|
||||||
|
Node f3;
|
||||||
|
f3.kind = NodeKind::UInt64;
|
||||||
|
f3.name = QStringLiteral("field_c");
|
||||||
|
f3.parentId = sId;
|
||||||
|
f3.offset = 8;
|
||||||
|
original.addNode(f3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create source text that matches what generator would produce
|
||||||
|
QString source = QStringLiteral(
|
||||||
|
"struct RoundTrip {\n"
|
||||||
|
" uint32_t field_a; // 0x0\n"
|
||||||
|
" float field_b; // 0x4\n"
|
||||||
|
" uint64_t field_c; // 0x8\n"
|
||||||
|
"};\n"
|
||||||
|
"static_assert(sizeof(RoundTrip) == 0x10, \"Size mismatch\");\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
NodeTree reimported = importFromSource(source);
|
||||||
|
QCOMPARE(countRoots(reimported), 1);
|
||||||
|
QCOMPARE(reimported.nodes[0].name, QStringLiteral("RoundTrip"));
|
||||||
|
|
||||||
|
auto origKids = childrenOf(original, original.nodes[0].id);
|
||||||
|
auto reimpKids = childrenOf(reimported, reimported.nodes[0].id);
|
||||||
|
|
||||||
|
// Compare field count (reimported may have extra padding nodes from static_assert)
|
||||||
|
// Check that the first 3 fields match
|
||||||
|
QVERIFY(reimpKids.size() >= 3);
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
QCOMPARE(reimported.nodes[reimpKids[i]].kind, original.nodes[origKids[i]].kind);
|
||||||
|
QCOMPARE(reimported.nodes[reimpKids[i]].name, original.nodes[origKids[i]].name);
|
||||||
|
QCOMPARE(reimported.nodes[reimpKids[i]].offset, original.nodes[origKids[i]].offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TestImportSource)
|
||||||
|
#include "test_import_source.moc"
|
||||||
144
tests/test_import_xml.cpp
Normal file
144
tests/test_import_xml.cpp
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
#include <QtTest/QtTest>
|
||||||
|
#include "core.h"
|
||||||
|
#include "import_reclass_xml.h"
|
||||||
|
|
||||||
|
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;
|
||||||
|
tmp.setAutoRemove(true);
|
||||||
|
QVERIFY(tmp.open());
|
||||||
|
tmp.write(R"(<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ReClass>
|
||||||
|
<!--ReClassEx-->
|
||||||
|
<Class Name="TestClass" Type="28" Comment="" Offset="0" strOffset="0" Code="">
|
||||||
|
<Node Name="vtable" Type="9" Size="8" bHidden="false" Comment=""/>
|
||||||
|
<Node Name="health" Type="13" Size="4" bHidden="false" Comment=""/>
|
||||||
|
<Node Name="name" Type="18" Size="32" bHidden="false" Comment=""/>
|
||||||
|
<Node Name="position" Type="23" Size="12" bHidden="false" Comment=""/>
|
||||||
|
<Node Name="pNext" Type="8" Size="8" bHidden="false" Comment="" Pointer="TestClass"/>
|
||||||
|
</Class>
|
||||||
|
</ReClass>
|
||||||
|
)");
|
||||||
|
tmp.flush();
|
||||||
|
|
||||||
|
QString error;
|
||||||
|
NodeTree tree = importReclassXml(tmp.fileName(), &error);
|
||||||
|
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
||||||
|
|
||||||
|
// Should have 1 root struct + 5 children = 6 nodes
|
||||||
|
QCOMPARE(tree.nodes.size(), 6);
|
||||||
|
|
||||||
|
// Root struct
|
||||||
|
QCOMPARE(tree.nodes[0].kind, NodeKind::Struct);
|
||||||
|
QCOMPARE(tree.nodes[0].name, QStringLiteral("TestClass"));
|
||||||
|
|
||||||
|
// vtable = Int64
|
||||||
|
QCOMPARE(tree.nodes[1].kind, NodeKind::Int64);
|
||||||
|
QCOMPARE(tree.nodes[1].name, QStringLiteral("vtable"));
|
||||||
|
QCOMPARE(tree.nodes[1].offset, 0);
|
||||||
|
|
||||||
|
// health = Float
|
||||||
|
QCOMPARE(tree.nodes[2].kind, NodeKind::Float);
|
||||||
|
QCOMPARE(tree.nodes[2].name, QStringLiteral("health"));
|
||||||
|
QCOMPARE(tree.nodes[2].offset, 8);
|
||||||
|
|
||||||
|
// name = UTF8 with strLen=32
|
||||||
|
QCOMPARE(tree.nodes[3].kind, NodeKind::UTF8);
|
||||||
|
QCOMPARE(tree.nodes[3].strLen, 32);
|
||||||
|
QCOMPARE(tree.nodes[3].offset, 12);
|
||||||
|
|
||||||
|
// position = Vec3
|
||||||
|
QCOMPARE(tree.nodes[4].kind, NodeKind::Vec3);
|
||||||
|
QCOMPARE(tree.nodes[4].offset, 44);
|
||||||
|
|
||||||
|
// pNext = Pointer64 with resolved refId
|
||||||
|
QCOMPARE(tree.nodes[5].kind, NodeKind::Pointer64);
|
||||||
|
QCOMPARE(tree.nodes[5].name, QStringLiteral("pNext"));
|
||||||
|
QVERIFY(tree.nodes[5].refId != 0);
|
||||||
|
QCOMPARE(tree.nodes[5].refId, tree.nodes[0].id); // points to TestClass
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(TestImportXml)
|
||||||
|
#include "test_import_xml.moc"
|
||||||
Reference in New Issue
Block a user