mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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>
389 lines
15 KiB
C++
389 lines
15 KiB
C++
#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
|