mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: Vergilius-style C++ generator, struct type click fix, item view highlight fix
Rewrite C++ generator for Vergilius-style output: inline anonymous structs/unions, reference opaque types by name with struct keyword prefix, size comments, aligned offset comments, no anon_ stubs. Fix struct type name not clickable in editor headers (headerTypeNameSpan assumed "struct TYPENAME" format but named structs use bare name). Add static_assert toggle in Options > Generator, default off. Fix item view highlight bleed: patch PE_PanelItemViewRow to use theme.hover so row background matches CE_ItemViewItem.
This commit is contained in:
@@ -1468,39 +1468,35 @@ static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Type name span for struct headers (not arrays)
|
// Type name span for struct headers (not arrays)
|
||||||
// Format: "struct TYPENAME NAME {" or collapsed variants
|
// Named structs format as: "_MMPTE OriginalPte {" (type column = just the name)
|
||||||
// For "struct NAME {" (no typename), returns invalid span
|
// Anonymous structs format as: "union {" or "struct {" (no clickable type)
|
||||||
static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText) {
|
static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText) {
|
||||||
if (lm.lineKind != LineKind::Header) return {};
|
if (lm.lineKind != LineKind::Header) return {};
|
||||||
if (lm.isArrayHeader) return {}; // Arrays use arrayHeaderTypeSpan instead
|
if (lm.isArrayHeader) return {};
|
||||||
|
|
||||||
int ind = kFoldCol + lm.depth * 3;
|
int ind = kFoldCol + lm.depth * 3;
|
||||||
int typeW = lm.effectiveTypeW;
|
int typeW = lm.effectiveTypeW;
|
||||||
int typeEnd = ind + typeW;
|
int typeEnd = ind + typeW;
|
||||||
|
|
||||||
// Clamp to actual line content
|
|
||||||
if (typeEnd > lineText.size()) typeEnd = lineText.size();
|
if (typeEnd > lineText.size()) typeEnd = lineText.size();
|
||||||
|
|
||||||
// Extract the type column text and check if it has a typename
|
|
||||||
// Format: "struct" or "struct TYPENAME"
|
|
||||||
QString typeCol = lineText.mid(ind, typeEnd - ind).trimmed();
|
QString typeCol = lineText.mid(ind, typeEnd - ind).trimmed();
|
||||||
|
if (typeCol.isEmpty()) return {};
|
||||||
|
|
||||||
// Find first space (after "struct")
|
// Anonymous structs use bare keywords — not clickable
|
||||||
int firstSpace = typeCol.indexOf(' ');
|
static const QStringList kKeywords = {
|
||||||
if (firstSpace < 0) return {}; // Just "struct", no typename
|
QStringLiteral("struct"), QStringLiteral("union"), QStringLiteral("class")
|
||||||
|
};
|
||||||
|
if (kKeywords.contains(typeCol)) return {};
|
||||||
|
|
||||||
// If there's content after "struct ", that's the typename
|
// Named struct: entire type column is the type name (e.g. "_MMPTE")
|
||||||
QString typename_ = typeCol.mid(firstSpace + 1).trimmed();
|
// Find the actual text bounds within the padded column
|
||||||
if (typename_.isEmpty()) return {};
|
int start = ind;
|
||||||
|
while (start < typeEnd && lineText[start] == ' ') start++;
|
||||||
|
int end = start;
|
||||||
|
while (end < typeEnd && lineText[end] != ' ') end++;
|
||||||
|
if (end <= start) return {};
|
||||||
|
|
||||||
// Return span of the typename within the type column
|
return {start, end, true};
|
||||||
int typenameStart = ind + firstSpace + 1;
|
|
||||||
// Find where the typename actually ends (skip padding)
|
|
||||||
int typenameEnd = typenameStart;
|
|
||||||
while (typenameEnd < typeEnd && lineText[typenameEnd] != ' ')
|
|
||||||
typenameEnd++;
|
|
||||||
|
|
||||||
return {typenameStart, typenameEnd, true};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type span for array headers: "int32_t[10]" in "int32_t[10] positions {"
|
// Type span for array headers: "int32_t[10]" in "int32_t[10] positions {"
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ struct GenContext {
|
|||||||
QString output;
|
QString output;
|
||||||
int padCounter = 0;
|
int padCounter = 0;
|
||||||
const QHash<NodeKind, QString>* typeAliases = nullptr;
|
const QHash<NodeKind, QString>* typeAliases = nullptr;
|
||||||
|
bool emitAsserts = false;
|
||||||
|
|
||||||
QString uniquePadName() {
|
QString uniquePadName() {
|
||||||
return QStringLiteral("_pad%1").arg(padCounter++, 4, 16, QChar('0'));
|
return QStringLiteral("_pad%1").arg(padCounter++, 4, 16, QChar('0'));
|
||||||
@@ -104,64 +105,70 @@ static QString offsetComment(int offset) {
|
|||||||
return QString(kCommentMarker) + QStringLiteral("// 0x%1").arg(QString::number(offset, 16).toUpper());
|
return QString(kCommentMarker) + QStringLiteral("// 0x%1").arg(QString::number(offset, 16).toUpper());
|
||||||
}
|
}
|
||||||
|
|
||||||
static QString emitField(GenContext& ctx, const Node& node) {
|
static QString indent(int depth) {
|
||||||
|
return QString(depth * 4, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString emitField(GenContext& ctx, const Node& node, int depth, int baseOffset) {
|
||||||
const NodeTree& tree = ctx.tree;
|
const NodeTree& tree = ctx.tree;
|
||||||
|
QString ind = indent(depth);
|
||||||
QString name = sanitizeIdent(node.name.isEmpty()
|
QString name = sanitizeIdent(node.name.isEmpty()
|
||||||
? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0'))
|
? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0'))
|
||||||
: node.name);
|
: node.name);
|
||||||
QString oc = offsetComment(node.offset);
|
QString oc = offsetComment(baseOffset + node.offset);
|
||||||
|
|
||||||
switch (node.kind) {
|
switch (node.kind) {
|
||||||
case NodeKind::Vec2:
|
case NodeKind::Vec2:
|
||||||
return QStringLiteral(" %1 %2[2];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
return ind + QStringLiteral("%1 %2[2];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||||
case NodeKind::Vec3:
|
case NodeKind::Vec3:
|
||||||
return QStringLiteral(" %1 %2[3];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
return ind + QStringLiteral("%1 %2[3];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||||
case NodeKind::Vec4:
|
case NodeKind::Vec4:
|
||||||
return QStringLiteral(" %1 %2[4];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
return ind + QStringLiteral("%1 %2[4];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||||
case NodeKind::Mat4x4:
|
case NodeKind::Mat4x4:
|
||||||
return QStringLiteral(" %1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
return ind + QStringLiteral("%1 %2[4][4];").arg(ctx.cType(NodeKind::Float), name) + oc;
|
||||||
case NodeKind::UTF8:
|
case NodeKind::UTF8:
|
||||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
|
return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF8), name).arg(node.strLen) + oc;
|
||||||
case NodeKind::UTF16:
|
case NodeKind::UTF16:
|
||||||
return QStringLiteral(" %1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
|
return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
|
||||||
case NodeKind::Pointer32: {
|
case NodeKind::Pointer32: {
|
||||||
if (node.refId != 0) {
|
if (node.refId != 0) {
|
||||||
int refIdx = tree.indexOfId(node.refId);
|
int refIdx = tree.indexOfId(node.refId);
|
||||||
if (refIdx >= 0) {
|
if (refIdx >= 0) {
|
||||||
QString target = ctx.structName(tree.nodes[refIdx]);
|
QString target = ctx.structName(tree.nodes[refIdx]);
|
||||||
return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) +
|
return ind + QStringLiteral("struct %1* %2;").arg(target, name) + oc;
|
||||||
offsetComment(node.offset).replace(QStringLiteral("//"), QStringLiteral("// -> %1*").arg(target));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) + oc;
|
return ind + QStringLiteral("%1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) + oc;
|
||||||
}
|
}
|
||||||
case NodeKind::Pointer64: {
|
case NodeKind::Pointer64: {
|
||||||
if (node.refId != 0) {
|
if (node.refId != 0) {
|
||||||
int refIdx = tree.indexOfId(node.refId);
|
int refIdx = tree.indexOfId(node.refId);
|
||||||
if (refIdx >= 0) {
|
if (refIdx >= 0) {
|
||||||
QString target = ctx.structName(tree.nodes[refIdx]);
|
QString target = ctx.structName(tree.nodes[refIdx]);
|
||||||
return QStringLiteral(" %1* %2;").arg(target, name) + oc;
|
return ind + QStringLiteral("struct %1* %2;").arg(target, name) + oc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return QStringLiteral(" void* %1;").arg(name) + oc;
|
return ind + QStringLiteral("void* %1;").arg(name) + oc;
|
||||||
}
|
}
|
||||||
case NodeKind::FuncPtr32:
|
case NodeKind::FuncPtr32:
|
||||||
return QStringLiteral(" void (*%1)();").arg(name) + oc;
|
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;
|
||||||
case NodeKind::FuncPtr64:
|
case NodeKind::FuncPtr64:
|
||||||
return QStringLiteral(" void (*%1)();").arg(name) + oc;
|
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;
|
||||||
default:
|
default:
|
||||||
return QStringLiteral(" %1 %2;").arg(ctx.cType(node.kind), name) + oc;
|
return ind + QStringLiteral("%1 %2;").arg(ctx.cType(node.kind), name) + oc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Emit struct body (fields + padding) ──
|
// ── Emit struct body (fields + padding) — Vergilius-style ──
|
||||||
|
|
||||||
static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
static void emitStructBody(GenContext& ctx, uint64_t structId,
|
||||||
|
bool isUnion, int depth, int baseOffset) {
|
||||||
const NodeTree& tree = ctx.tree;
|
const NodeTree& tree = ctx.tree;
|
||||||
int idx = tree.indexOfId(structId);
|
int idx = tree.indexOfId(structId);
|
||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
|
|
||||||
int structSize = tree.structSpan(structId, &ctx.childMap);
|
int structSize = tree.structSpan(structId, &ctx.childMap);
|
||||||
|
QString ind = indent(depth);
|
||||||
|
|
||||||
QVector<int> children = ctx.childMap.value(structId);
|
QVector<int> children = ctx.childMap.value(structId);
|
||||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||||
@@ -169,13 +176,12 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper: emit a padding/hex run as a single collapsed byte array
|
// Helper: emit a padding/hex run as a single collapsed byte array
|
||||||
auto emitPadRun = [&](int offset, int size) {
|
auto emitPadRun = [&](int relOffset, int size) {
|
||||||
if (size <= 0) return;
|
if (size <= 0) return;
|
||||||
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
|
ctx.output += ind + QStringLiteral("uint8_t %1[0x%2];%3\n")
|
||||||
.arg(QStringLiteral("uint8_t"))
|
|
||||||
.arg(ctx.uniquePadName())
|
.arg(ctx.uniquePadName())
|
||||||
.arg(QString::number(size, 16).toUpper())
|
.arg(QString::number(size, 16).toUpper())
|
||||||
.arg(offsetComment(offset));
|
.arg(offsetComment(baseOffset + relOffset));
|
||||||
};
|
};
|
||||||
|
|
||||||
int cursor = 0;
|
int cursor = 0;
|
||||||
@@ -189,13 +195,15 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
|||||||
else
|
else
|
||||||
childSize = child.byteSize();
|
childSize = child.byteSize();
|
||||||
|
|
||||||
// Gap before this field
|
// Gap/overlap handling (skip for unions)
|
||||||
if (child.offset > cursor)
|
if (!isUnion) {
|
||||||
emitPadRun(cursor, child.offset - cursor);
|
if (child.offset > cursor)
|
||||||
else if (child.offset < cursor)
|
emitPadRun(cursor, child.offset - cursor);
|
||||||
ctx.output += QStringLiteral(" // WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n")
|
else if (child.offset < cursor)
|
||||||
.arg(QString::number(child.offset, 16).toUpper())
|
ctx.output += ind + QStringLiteral("// WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n")
|
||||||
.arg(QString::number(cursor, 16).toUpper());
|
.arg(QString::number(baseOffset + child.offset, 16).toUpper())
|
||||||
|
.arg(QString::number(baseOffset + cursor, 16).toUpper());
|
||||||
|
}
|
||||||
|
|
||||||
// Collapse consecutive hex nodes into a single padding array
|
// Collapse consecutive hex nodes into a single padding array
|
||||||
if (isHexNode(child.kind)) {
|
if (isHexNode(child.kind)) {
|
||||||
@@ -206,8 +214,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
|||||||
const Node& next = tree.nodes[children[j]];
|
const Node& next = tree.nodes[children[j]];
|
||||||
if (!isHexNode(next.kind)) break;
|
if (!isHexNode(next.kind)) break;
|
||||||
int nextSize = next.byteSize();
|
int nextSize = next.byteSize();
|
||||||
// Allow gaps within the run (they become part of the pad)
|
if (next.offset < runEnd) break;
|
||||||
if (next.offset < runEnd) break; // overlap — stop merging
|
|
||||||
runEnd = next.offset + nextSize;
|
runEnd = next.offset + nextSize;
|
||||||
j++;
|
j++;
|
||||||
}
|
}
|
||||||
@@ -219,10 +226,31 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
|||||||
|
|
||||||
// Emit the field
|
// Emit the field
|
||||||
if (child.kind == NodeKind::Struct) {
|
if (child.kind == NodeKind::Struct) {
|
||||||
emitStruct(ctx, child.id);
|
bool isAnonymous = child.structTypeName.isEmpty();
|
||||||
QString typeName = ctx.structName(child);
|
|
||||||
QString fieldName = sanitizeIdent(child.name);
|
if (isAnonymous) {
|
||||||
ctx.output += QStringLiteral(" %1 %2;%3\n").arg(typeName, fieldName, offsetComment(child.offset));
|
// Inline anonymous struct/union
|
||||||
|
QString kw = child.resolvedClassKeyword();
|
||||||
|
ctx.output += ind + kw + QStringLiteral("\n");
|
||||||
|
ctx.output += ind + QStringLiteral("{\n");
|
||||||
|
bool childIsUnion = (kw == QStringLiteral("union"));
|
||||||
|
emitStructBody(ctx, child.id, childIsUnion, depth + 1,
|
||||||
|
baseOffset + child.offset);
|
||||||
|
QString fieldName = child.name.isEmpty()
|
||||||
|
? QString() : QStringLiteral(" ") + sanitizeIdent(child.name);
|
||||||
|
ctx.output += ind + QStringLiteral("}") + fieldName + QStringLiteral(";")
|
||||||
|
+ offsetComment(baseOffset + child.offset) + QStringLiteral("\n");
|
||||||
|
} else {
|
||||||
|
// Named struct — reference by name with struct keyword prefix
|
||||||
|
QString kw = child.resolvedClassKeyword();
|
||||||
|
if (kw == QStringLiteral("enum") && child.enumMembers.isEmpty())
|
||||||
|
kw = QStringLiteral("struct");
|
||||||
|
QString typeName = sanitizeIdent(child.structTypeName);
|
||||||
|
QString fieldName = sanitizeIdent(child.name);
|
||||||
|
ctx.output += ind + kw + QStringLiteral(" ") + typeName
|
||||||
|
+ QStringLiteral(" ") + fieldName + QStringLiteral(";")
|
||||||
|
+ offsetComment(baseOffset + child.offset) + QStringLiteral("\n");
|
||||||
|
}
|
||||||
} else if (child.kind == NodeKind::Array) {
|
} else if (child.kind == NodeKind::Array) {
|
||||||
QVector<int> arrayKids = ctx.childMap.value(child.id);
|
QVector<int> arrayKids = ctx.childMap.value(child.id);
|
||||||
bool hasStructChild = false;
|
bool hasStructChild = false;
|
||||||
@@ -231,7 +259,6 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
|||||||
for (int ak : arrayKids) {
|
for (int ak : arrayKids) {
|
||||||
if (tree.nodes[ak].kind == NodeKind::Struct) {
|
if (tree.nodes[ak].kind == NodeKind::Struct) {
|
||||||
hasStructChild = true;
|
hasStructChild = true;
|
||||||
emitStruct(ctx, tree.nodes[ak].id);
|
|
||||||
elemTypeName = ctx.structName(tree.nodes[ak]);
|
elemTypeName = ctx.structName(tree.nodes[ak]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -239,14 +266,16 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
|||||||
|
|
||||||
QString fieldName = sanitizeIdent(child.name);
|
QString fieldName = sanitizeIdent(child.name);
|
||||||
if (hasStructChild && !elemTypeName.isEmpty()) {
|
if (hasStructChild && !elemTypeName.isEmpty()) {
|
||||||
ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
|
ctx.output += ind + QStringLiteral("struct %1 %2[%3];%4\n")
|
||||||
.arg(elemTypeName, fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
|
.arg(elemTypeName, fieldName).arg(child.arrayLen)
|
||||||
|
.arg(offsetComment(baseOffset + child.offset));
|
||||||
} else {
|
} else {
|
||||||
ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
|
ctx.output += ind + QStringLiteral("%1 %2[%3];%4\n")
|
||||||
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
|
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen)
|
||||||
|
.arg(offsetComment(baseOffset + child.offset));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.output += emitField(ctx, child) + QStringLiteral("\n");
|
ctx.output += emitField(ctx, child, depth, baseOffset) + QStringLiteral("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
int childEnd = child.offset + childSize;
|
int childEnd = child.offset + childSize;
|
||||||
@@ -254,12 +283,12 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
|||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tail padding
|
// Tail padding (skip for unions)
|
||||||
if (cursor < structSize)
|
if (!isUnion && cursor < structSize)
|
||||||
emitPadRun(cursor, structSize - cursor);
|
emitPadRun(cursor, structSize - cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Emit a complete struct definition ──
|
// ── Emit a complete top-level struct definition (Vergilius-style) ──
|
||||||
|
|
||||||
static void emitStruct(GenContext& ctx, uint64_t structId) {
|
static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||||
if (ctx.emittedIds.contains(structId)) return;
|
if (ctx.emittedIds.contains(structId)) return;
|
||||||
@@ -275,19 +304,12 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For arrays, we don't emit a top-level struct — the array itself
|
|
||||||
// is a field inside its parent. But we do emit struct element types.
|
|
||||||
if (node.kind == NodeKind::Array) {
|
if (node.kind == NodeKind::Array) {
|
||||||
QVector<int> kids = ctx.childMap.value(structId);
|
|
||||||
for (int ki : kids) {
|
|
||||||
if (ctx.tree.nodes[ki].kind == NodeKind::Struct)
|
|
||||||
emitStruct(ctx, ctx.tree.nodes[ki].id);
|
|
||||||
}
|
|
||||||
ctx.visiting.remove(structId);
|
ctx.visiting.remove(structId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplicate by struct type name (different nodes may share the same type)
|
// Deduplicate by struct type name
|
||||||
QString typeName = ctx.structName(node);
|
QString typeName = ctx.structName(node);
|
||||||
if (ctx.emittedTypeNames.contains(typeName)) {
|
if (ctx.emittedTypeNames.contains(typeName)) {
|
||||||
ctx.emittedIds.insert(structId);
|
ctx.emittedIds.insert(structId);
|
||||||
@@ -295,34 +317,6 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit nested struct types first (dependency order)
|
|
||||||
QVector<int> children = ctx.childMap.value(structId);
|
|
||||||
for (int ci : children) {
|
|
||||||
const Node& child = ctx.tree.nodes[ci];
|
|
||||||
if (child.kind == NodeKind::Struct)
|
|
||||||
emitStruct(ctx, child.id);
|
|
||||||
else if (child.kind == NodeKind::Array) {
|
|
||||||
QVector<int> arrayKids = ctx.childMap.value(child.id);
|
|
||||||
for (int ak : arrayKids) {
|
|
||||||
if (ctx.tree.nodes[ak].kind == NodeKind::Struct)
|
|
||||||
emitStruct(ctx, ctx.tree.nodes[ak].id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Forward-declare pointer target types if they're outside this subtree
|
|
||||||
if (child.kind == NodeKind::Pointer64 && child.refId != 0) {
|
|
||||||
int refIdx = ctx.tree.indexOfId(child.refId);
|
|
||||||
if (refIdx >= 0 && !ctx.emittedIds.contains(child.refId)
|
|
||||||
&& !ctx.forwardDeclared.contains(child.refId)) {
|
|
||||||
QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]);
|
|
||||||
QString fwdKw = ctx.tree.nodes[refIdx].resolvedClassKeyword();
|
|
||||||
if (fwdKw == QStringLiteral("enum") && ctx.tree.nodes[refIdx].enumMembers.isEmpty())
|
|
||||||
fwdKw = QStringLiteral("struct");
|
|
||||||
ctx.output += QStringLiteral("%1 %2;\n").arg(fwdKw, fwdName);
|
|
||||||
ctx.forwardDeclared.insert(child.refId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.emittedIds.insert(structId);
|
ctx.emittedIds.insert(structId);
|
||||||
ctx.emittedTypeNames.insert(typeName);
|
ctx.emittedTypeNames.insert(typeName);
|
||||||
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
||||||
@@ -342,15 +336,21 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum without members: fallback
|
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct");
|
||||||
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
|
|
||||||
|
|
||||||
emitStructBody(ctx, structId);
|
// Size comment (Vergilius-style)
|
||||||
|
ctx.output += QStringLiteral("//0x%1 bytes (sizeof)\n")
|
||||||
|
.arg(QString::number(structSize, 16).toUpper());
|
||||||
|
ctx.output += kw + QStringLiteral(" ") + typeName + QStringLiteral("\n{\n");
|
||||||
|
|
||||||
|
emitStructBody(ctx, structId, kw == QStringLiteral("union"), 1, 0);
|
||||||
|
|
||||||
ctx.output += QStringLiteral("};\n");
|
ctx.output += QStringLiteral("};\n");
|
||||||
ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n\n")
|
if (ctx.emitAsserts)
|
||||||
.arg(typeName)
|
ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n")
|
||||||
.arg(QString::number(structSize, 16).toUpper());
|
.arg(typeName)
|
||||||
|
.arg(QString::number(structSize, 16).toUpper());
|
||||||
|
ctx.output += QStringLiteral("\n");
|
||||||
|
|
||||||
ctx.visiting.remove(structId);
|
ctx.visiting.remove(structId);
|
||||||
}
|
}
|
||||||
@@ -404,14 +404,15 @@ static QString alignComments(const QString& raw) {
|
|||||||
// ── Public API ──
|
// ── Public API ──
|
||||||
|
|
||||||
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
||||||
const QHash<NodeKind, QString>* typeAliases) {
|
const QHash<NodeKind, QString>* typeAliases,
|
||||||
|
bool emitAsserts) {
|
||||||
int idx = tree.indexOfId(rootStructId);
|
int idx = tree.indexOfId(rootStructId);
|
||||||
if (idx < 0) return {};
|
if (idx < 0) return {};
|
||||||
|
|
||||||
const Node& root = tree.nodes[idx];
|
const Node& root = tree.nodes[idx];
|
||||||
if (root.kind != NodeKind::Struct) return {};
|
if (root.kind != NodeKind::Struct) return {};
|
||||||
|
|
||||||
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
|
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts};
|
||||||
|
|
||||||
ctx.output += QStringLiteral("#pragma once\n\n");
|
ctx.output += QStringLiteral("#pragma once\n\n");
|
||||||
|
|
||||||
@@ -421,8 +422,9 @@ QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString renderCppAll(const NodeTree& tree,
|
QString renderCppAll(const NodeTree& tree,
|
||||||
const QHash<NodeKind, QString>* typeAliases) {
|
const QHash<NodeKind, QString>* typeAliases,
|
||||||
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
|
bool emitAsserts) {
|
||||||
|
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts};
|
||||||
|
|
||||||
ctx.output += QStringLiteral("#pragma once\n\n");
|
ctx.output += QStringLiteral("#pragma once\n\n");
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ namespace rcx {
|
|||||||
// Generate C++ struct definitions for a single root struct and all
|
// Generate C++ struct definitions for a single root struct and all
|
||||||
// nested/referenced types reachable from it.
|
// nested/referenced types reachable from it.
|
||||||
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
||||||
const QHash<NodeKind, QString>* typeAliases = nullptr);
|
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||||
|
bool emitAsserts = false);
|
||||||
|
|
||||||
// Generate C++ struct definitions for every root-level struct (full SDK).
|
// Generate C++ struct definitions for every root-level struct (full SDK).
|
||||||
QString renderCppAll(const NodeTree& tree,
|
QString renderCppAll(const NodeTree& tree,
|
||||||
const QHash<NodeKind, QString>* typeAliases = nullptr);
|
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||||
|
bool emitAsserts = false);
|
||||||
|
|
||||||
// Null generator placeholder (returns empty string).
|
// Null generator placeholder (returns empty string).
|
||||||
QString renderNull(const NodeTree& tree, uint64_t rootStructId);
|
QString renderNull(const NodeTree& tree, uint64_t rootStructId);
|
||||||
|
|||||||
22
src/main.cpp
22
src/main.cpp
@@ -267,6 +267,16 @@ public:
|
|||||||
// Transparent menu bar background (no CSS needed)
|
// Transparent menu bar background (no CSS needed)
|
||||||
if (elem == PE_PanelMenuBar)
|
if (elem == PE_PanelMenuBar)
|
||||||
return;
|
return;
|
||||||
|
// Item-view row background — patch Highlight so the row bg matches CE_ItemViewItem
|
||||||
|
if (elem == PE_PanelItemViewRow) {
|
||||||
|
if (auto* vi = qstyleoption_cast<const QStyleOptionViewItem*>(opt)) {
|
||||||
|
QStyleOptionViewItem patched = *vi;
|
||||||
|
patched.palette.setColor(QPalette::Highlight,
|
||||||
|
vi->palette.color(QPalette::Mid));
|
||||||
|
QProxyStyle::drawPrimitive(elem, &patched, p, w);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
QProxyStyle::drawPrimitive(elem, opt, p, w);
|
QProxyStyle::drawPrimitive(elem, opt, p, w);
|
||||||
}
|
}
|
||||||
void drawControl(ControlElement element, const QStyleOption* opt,
|
void drawControl(ControlElement element, const QStyleOption* opt,
|
||||||
@@ -1804,6 +1814,7 @@ void MainWindow::showOptionsDialog() {
|
|||||||
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
|
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
|
||||||
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool();
|
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool();
|
||||||
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
|
||||||
|
current.generatorAsserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
|
||||||
|
|
||||||
OptionsDialog dlg(current, this);
|
OptionsDialog dlg(current, this);
|
||||||
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK
|
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK
|
||||||
@@ -1837,6 +1848,9 @@ void MainWindow::showOptionsDialog() {
|
|||||||
for (auto& tab : m_tabs)
|
for (auto& tab : m_tabs)
|
||||||
tab.ctrl->setRefreshInterval(r.refreshMs);
|
tab.ctrl->setRefreshInterval(r.refreshMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (r.generatorAsserts != current.generatorAsserts)
|
||||||
|
QSettings("Reclass", "Reclass").setValue("generatorAsserts", r.generatorAsserts);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::setEditorFont(const QString& fontName) {
|
void MainWindow::setEditorFont(const QString& fontName) {
|
||||||
@@ -2023,11 +2037,12 @@ void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) {
|
|||||||
// Generate text
|
// Generate text
|
||||||
const QHash<NodeKind, QString>* aliases =
|
const QHash<NodeKind, QString>* aliases =
|
||||||
tab.doc->typeAliases.isEmpty() ? nullptr : &tab.doc->typeAliases;
|
tab.doc->typeAliases.isEmpty() ? nullptr : &tab.doc->typeAliases;
|
||||||
|
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
|
||||||
QString text;
|
QString text;
|
||||||
if (rootId != 0)
|
if (rootId != 0)
|
||||||
text = renderCpp(tab.doc->tree, rootId, aliases);
|
text = renderCpp(tab.doc->tree, rootId, aliases, asserts);
|
||||||
else
|
else
|
||||||
text = renderCppAll(tab.doc->tree, aliases);
|
text = renderCppAll(tab.doc->tree, aliases, asserts);
|
||||||
|
|
||||||
// Scroll restoration: save if same root, reset if different
|
// Scroll restoration: save if same root, reset if different
|
||||||
int restoreLine = 0;
|
int restoreLine = 0;
|
||||||
@@ -2071,7 +2086,8 @@ void MainWindow::exportCpp() {
|
|||||||
|
|
||||||
const QHash<NodeKind, QString>* aliases =
|
const QHash<NodeKind, QString>* aliases =
|
||||||
tab->doc->typeAliases.isEmpty() ? nullptr : &tab->doc->typeAliases;
|
tab->doc->typeAliases.isEmpty() ? nullptr : &tab->doc->typeAliases;
|
||||||
QString text = renderCppAll(tab->doc->tree, aliases);
|
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
|
||||||
|
QString text = renderCppAll(tab->doc->tree, aliases, asserts);
|
||||||
QFile file(path);
|
QFile file(path);
|
||||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
QMessageBox::warning(this, "Export Failed",
|
QMessageBox::warning(this, "Export Failed",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "generator.h"
|
#include "generator.h"
|
||||||
#include "mainwindow.h"
|
#include "mainwindow.h"
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QSettings>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
@@ -1094,15 +1095,16 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
|
|||||||
if (action == "export_cpp") {
|
if (action == "export_cpp") {
|
||||||
if (!doc) return makeTextResult("No active tab", true);
|
if (!doc) return makeTextResult("No active tab", true);
|
||||||
const QHash<NodeKind, QString>* aliases = doc->typeAliases.isEmpty() ? nullptr : &doc->typeAliases;
|
const QHash<NodeKind, QString>* aliases = doc->typeAliases.isEmpty() ? nullptr : &doc->typeAliases;
|
||||||
|
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
|
||||||
QString code;
|
QString code;
|
||||||
if (!nodeIdStr.isEmpty()) {
|
if (!nodeIdStr.isEmpty()) {
|
||||||
// Per-struct export
|
// Per-struct export
|
||||||
uint64_t nid = nodeIdStr.toULongLong();
|
uint64_t nid = nodeIdStr.toULongLong();
|
||||||
code = renderCpp(doc->tree, nid, aliases);
|
code = renderCpp(doc->tree, nid, aliases, asserts);
|
||||||
if (code.isEmpty())
|
if (code.isEmpty())
|
||||||
return makeTextResult("Node not found or not a struct: " + nodeIdStr, true);
|
return makeTextResult("Node not found or not a struct: " + nodeIdStr, true);
|
||||||
} else {
|
} else {
|
||||||
code = renderCppAll(doc->tree, aliases);
|
code = renderCppAll(doc->tree, aliases, asserts);
|
||||||
}
|
}
|
||||||
// Truncate if too large (64 KB limit)
|
// Truncate if too large (64 KB limit)
|
||||||
if (code.size() > 65536) {
|
if (code.size() > 65536) {
|
||||||
|
|||||||
@@ -170,6 +170,14 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
|
|||||||
auto* generatorLayout = new QVBoxLayout(generatorPage);
|
auto* generatorLayout = new QVBoxLayout(generatorPage);
|
||||||
generatorLayout->setContentsMargins(0, 0, 0, 0);
|
generatorLayout->setContentsMargins(0, 0, 0, 0);
|
||||||
generatorLayout->setSpacing(8);
|
generatorLayout->setSpacing(8);
|
||||||
|
|
||||||
|
auto* cppGroup = new QGroupBox("C++ Header");
|
||||||
|
auto* cppLayout = new QVBoxLayout(cppGroup);
|
||||||
|
m_assertCheck = new QCheckBox("Emit static_assert size checks");
|
||||||
|
m_assertCheck->setChecked(current.generatorAsserts);
|
||||||
|
cppLayout->addWidget(m_assertCheck);
|
||||||
|
generatorLayout->addWidget(cppGroup);
|
||||||
|
|
||||||
generatorLayout->addStretch();
|
generatorLayout->addStretch();
|
||||||
|
|
||||||
m_pages->addWidget(generatorPage); // index 2
|
m_pages->addWidget(generatorPage); // index 2
|
||||||
@@ -208,6 +216,7 @@ OptionsResult OptionsDialog::result() const {
|
|||||||
r.safeMode = m_safeModeCheck->isChecked();
|
r.safeMode = m_safeModeCheck->isChecked();
|
||||||
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
r.autoStartMcp = m_autoMcpCheck->isChecked();
|
||||||
r.refreshMs = m_refreshSpin->value();
|
r.refreshMs = m_refreshSpin->value();
|
||||||
|
r.generatorAsserts = m_assertCheck->isChecked();
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ struct OptionsResult {
|
|||||||
bool safeMode = false;
|
bool safeMode = false;
|
||||||
bool autoStartMcp = true;
|
bool autoStartMcp = true;
|
||||||
int refreshMs = 660;
|
int refreshMs = 660;
|
||||||
|
bool generatorAsserts = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class OptionsDialog : public QDialog {
|
class OptionsDialog : public QDialog {
|
||||||
@@ -41,6 +42,7 @@ private:
|
|||||||
QCheckBox* m_safeModeCheck = nullptr;
|
QCheckBox* m_safeModeCheck = nullptr;
|
||||||
QCheckBox* m_autoMcpCheck = nullptr;
|
QCheckBox* m_autoMcpCheck = nullptr;
|
||||||
QSpinBox* m_refreshSpin = nullptr;
|
QSpinBox* m_refreshSpin = nullptr;
|
||||||
|
QCheckBox* m_assertCheck = nullptr;
|
||||||
|
|
||||||
// searchable keywords per leaf tree item
|
// searchable keywords per leaf tree item
|
||||||
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;
|
||||||
|
|||||||
@@ -2514,6 +2514,48 @@ private slots:
|
|||||||
<< QString("gapRight=%1 gapBottom=%2 (font-independent)")
|
<< QString("gapRight=%1 gapBottom=%2 (font-independent)")
|
||||||
.arg(gapR1).arg(gapB1);
|
.arg(gapR1).arg(gapB1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Test: hovering struct type name shows PointingHand cursor ──
|
||||||
|
// Regression: headerTypeNameSpan returned invalid for named structs
|
||||||
|
// because it assumed "struct TYPENAME" format, but named structs are
|
||||||
|
// formatted as just "TYPENAME" (e.g. "_STRING64 CSDVersion").
|
||||||
|
void testStructTypeClickable() {
|
||||||
|
m_editor->applyDocument(m_result);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// Find a named struct header (e.g. _STRING64 CSDVersion from makeTestTree)
|
||||||
|
int headerLine = -1;
|
||||||
|
for (int i = 0; i < m_result.meta.size(); i++) {
|
||||||
|
const auto& lm = m_result.meta[i];
|
||||||
|
if (lm.lineKind == LineKind::Header && lm.foldHead
|
||||||
|
&& lm.nodeKind == NodeKind::Struct && !lm.isArrayHeader) {
|
||||||
|
headerLine = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY2(headerLine >= 0, "Should have a struct header");
|
||||||
|
|
||||||
|
const LineMeta* lm = m_editor->metaForLine(headerLine);
|
||||||
|
QVERIFY(lm);
|
||||||
|
|
||||||
|
// Scroll to ensure line is visible
|
||||||
|
m_editor->scintilla()->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_ENSUREVISIBLE, (unsigned long)headerLine);
|
||||||
|
m_editor->scintilla()->SendScintilla(
|
||||||
|
QsciScintillaBase::SCI_GOTOLINE, (unsigned long)headerLine);
|
||||||
|
QApplication::processEvents();
|
||||||
|
|
||||||
|
// The type column starts at kFoldCol + depth*3
|
||||||
|
int typeStart = 3 + lm->depth * 3; // kFoldCol = 3
|
||||||
|
|
||||||
|
// Hover over type column — should show PointingHandCursor
|
||||||
|
// (Before fix: showed ArrowCursor because headerTypeNameSpan returned invalid)
|
||||||
|
QPoint typePos = colToViewport(m_editor->scintilla(), headerLine, typeStart + 1);
|
||||||
|
QVERIFY2(typePos.y() > 0, "Header line should be visible");
|
||||||
|
sendMouseMove(m_editor->scintilla()->viewport(), typePos);
|
||||||
|
QApplication::processEvents();
|
||||||
|
QCOMPARE(viewportCursor(m_editor), Qt::PointingHandCursor);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_MAIN(TestEditor)
|
QTEST_MAIN(TestEditor)
|
||||||
|
|||||||
@@ -46,27 +46,37 @@ private:
|
|||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
||||||
// ── Basic struct generation ──
|
// ── Basic struct generation (Vergilius-style) ──
|
||||||
|
|
||||||
void testSimpleStruct() {
|
void testSimpleStruct() {
|
||||||
auto tree = makeSimpleStruct();
|
auto tree = makeSimpleStruct();
|
||||||
uint64_t rootId = tree.nodes[0].id;
|
uint64_t rootId = tree.nodes[0].id;
|
||||||
QString result = rcx::renderCpp(tree, rootId);
|
QString result = rcx::renderCpp(tree, rootId, nullptr, true);
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
QVERIFY(result.contains("#pragma once"));
|
QVERIFY(result.contains("#pragma once"));
|
||||||
QVERIFY(!result.contains("#include <cstdint>"));
|
|
||||||
QVERIFY(!result.contains("#pragma pack"));
|
|
||||||
|
|
||||||
// Struct definition
|
// Size comment (Vergilius-style)
|
||||||
QVERIFY(result.contains("struct Player {"));
|
QVERIFY(result.contains("//0x10 bytes (sizeof)"));
|
||||||
|
|
||||||
|
// Struct definition (brace on new line)
|
||||||
|
QVERIFY(result.contains("struct Player\n{"));
|
||||||
QVERIFY(result.contains("int32_t health;"));
|
QVERIFY(result.contains("int32_t health;"));
|
||||||
QVERIFY(result.contains("float speed;"));
|
QVERIFY(result.contains("float speed;"));
|
||||||
QVERIFY(result.contains("uint64_t id;"));
|
QVERIFY(result.contains("uint64_t id;"));
|
||||||
QVERIFY(result.contains("};"));
|
QVERIFY(result.contains("};"));
|
||||||
|
|
||||||
// static_assert - struct is 16 bytes (0+4 + 4+4 + 8+8 = 16)
|
// Offset comments
|
||||||
|
QVERIFY(result.contains("// 0x0"));
|
||||||
|
QVERIFY(result.contains("// 0x4"));
|
||||||
|
QVERIFY(result.contains("// 0x8"));
|
||||||
|
|
||||||
|
// static_assert
|
||||||
QVERIFY(result.contains("static_assert(sizeof(Player) == 0x10"));
|
QVERIFY(result.contains("static_assert(sizeof(Player) == 0x10"));
|
||||||
|
|
||||||
|
// Without emitAsserts, static_assert should not appear
|
||||||
|
QString noAsserts = rcx::renderCpp(tree, rootId);
|
||||||
|
QVERIFY(!noAsserts.contains("static_assert"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Padding gap detection ──
|
// ── Padding gap detection ──
|
||||||
@@ -134,7 +144,7 @@ private slots:
|
|||||||
f2.offset = 16;
|
f2.offset = 16;
|
||||||
tree.addNode(f2);
|
tree.addNode(f2);
|
||||||
|
|
||||||
QString result = rcx::renderCpp(tree, rootId);
|
QString result = rcx::renderCpp(tree, rootId, nullptr, true);
|
||||||
|
|
||||||
// Gap between offset 1 and 16 = 15 bytes padding
|
// Gap between offset 1 and 16 = 15 bytes padding
|
||||||
QVERIFY(result.contains("[0xF]"));
|
QVERIFY(result.contains("[0xF]"));
|
||||||
@@ -175,7 +185,47 @@ private slots:
|
|||||||
QVERIFY(result.contains("WARNING: overlap"));
|
QVERIFY(result.contains("WARNING: overlap"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Nested struct ──
|
// ── Union members should NOT produce overlap warnings ──
|
||||||
|
|
||||||
|
void testUnionNoOverlapWarning() {
|
||||||
|
rcx::NodeTree tree;
|
||||||
|
rcx::Node root;
|
||||||
|
root.kind = rcx::NodeKind::Struct;
|
||||||
|
root.name = "TestUnion";
|
||||||
|
root.structTypeName = "TestUnion";
|
||||||
|
root.classKeyword = "union";
|
||||||
|
root.parentId = 0;
|
||||||
|
int ri = tree.addNode(root);
|
||||||
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
|
// Two union members at offset 0
|
||||||
|
rcx::Node f1;
|
||||||
|
f1.kind = rcx::NodeKind::UInt64;
|
||||||
|
f1.name = "wide";
|
||||||
|
f1.parentId = rootId;
|
||||||
|
f1.offset = 0;
|
||||||
|
tree.addNode(f1);
|
||||||
|
|
||||||
|
rcx::Node f2;
|
||||||
|
f2.kind = rcx::NodeKind::UInt32;
|
||||||
|
f2.name = "narrow";
|
||||||
|
f2.parentId = rootId;
|
||||||
|
f2.offset = 0;
|
||||||
|
tree.addNode(f2);
|
||||||
|
|
||||||
|
QString result = rcx::renderCpp(tree, rootId);
|
||||||
|
|
||||||
|
// Vergilius-style: union keyword, brace on new line
|
||||||
|
QVERIFY(result.contains("union TestUnion\n{"));
|
||||||
|
QVERIFY(result.contains("uint64_t wide;"));
|
||||||
|
QVERIFY(result.contains("uint32_t narrow;"));
|
||||||
|
// Union members overlap by design — no warning
|
||||||
|
QVERIFY(!result.contains("WARNING"));
|
||||||
|
// No padding in unions
|
||||||
|
QVERIFY(!result.contains("_pad"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Nested struct: named sub-type referenced by name ──
|
||||||
|
|
||||||
void testNestedStruct() {
|
void testNestedStruct() {
|
||||||
rcx::NodeTree tree;
|
rcx::NodeTree tree;
|
||||||
@@ -222,23 +272,14 @@ private slots:
|
|||||||
f2.offset = 8;
|
f2.offset = 8;
|
||||||
tree.addNode(f2);
|
tree.addNode(f2);
|
||||||
|
|
||||||
QString result = rcx::renderCpp(tree, outerId);
|
QString result = rcx::renderCpp(tree, outerId, nullptr, true);
|
||||||
|
|
||||||
// Inner struct should be defined before outer
|
// Vergilius-style: named sub-types referenced by name with struct prefix
|
||||||
int innerPos = result.indexOf("struct Vec2f {");
|
// No separate top-level definition for Vec2f in renderCpp
|
||||||
int outerPos = result.indexOf("struct Outer {");
|
QVERIFY(result.contains("struct Outer\n{"));
|
||||||
QVERIFY(innerPos >= 0);
|
QVERIFY(result.contains("struct Vec2f pos;"));
|
||||||
QVERIFY(outerPos >= 0);
|
|
||||||
QVERIFY(innerPos < outerPos);
|
|
||||||
|
|
||||||
// Inner struct fields
|
|
||||||
QVERIFY(result.contains("float x;"));
|
|
||||||
QVERIFY(result.contains("float y;"));
|
|
||||||
QVERIFY(result.contains("static_assert(sizeof(Vec2f) == 0x8"));
|
|
||||||
|
|
||||||
// Outer struct uses inner type
|
|
||||||
QVERIFY(result.contains("Vec2f pos;"));
|
|
||||||
QVERIFY(result.contains("int32_t score;"));
|
QVERIFY(result.contains("int32_t score;"));
|
||||||
|
QVERIFY(result.contains("static_assert(sizeof(Outer) == 0xC"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Primitive array ──
|
// ── Primitive array ──
|
||||||
@@ -325,15 +366,12 @@ private slots:
|
|||||||
|
|
||||||
QString result = rcx::renderCpp(tree, mainId);
|
QString result = rcx::renderCpp(tree, mainId);
|
||||||
|
|
||||||
// ptr64 with target → real C++ pointer
|
// Vergilius-style: struct prefix on pointer targets
|
||||||
QVERIFY(result.contains("TargetData* pTarget;"));
|
QVERIFY(result.contains("struct TargetData* pTarget;"));
|
||||||
// ptr64 without target → void*
|
// ptr64 without target → void*
|
||||||
QVERIFY(result.contains("void* pVoid;"));
|
QVERIFY(result.contains("void* pVoid;"));
|
||||||
// ptr32 with target → uint32_t with comment
|
// ptr32 with target → struct X* (Vergilius-style, no forward decl needed)
|
||||||
QVERIFY(result.contains("uint32_t pTarget32;"));
|
QVERIFY(result.contains("struct TargetData* pTarget32;"));
|
||||||
QVERIFY(result.contains("-> TargetData*"));
|
|
||||||
// Forward declaration for TargetData
|
|
||||||
QVERIFY(result.contains("struct TargetData;"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Vector and matrix types ──
|
// ── Vector and matrix types ──
|
||||||
@@ -457,10 +495,11 @@ private slots:
|
|||||||
bf.offset = 0;
|
bf.offset = 0;
|
||||||
tree.addNode(bf);
|
tree.addNode(bf);
|
||||||
|
|
||||||
QString result = rcx::renderCppAll(tree);
|
QString result = rcx::renderCppAll(tree, nullptr, true);
|
||||||
|
|
||||||
QVERIFY(result.contains("struct StructA {"));
|
// Vergilius-style: brace on new line
|
||||||
QVERIFY(result.contains("struct StructB {"));
|
QVERIFY(result.contains("struct StructA\n{"));
|
||||||
|
QVERIFY(result.contains("struct StructB\n{"));
|
||||||
QVERIFY(result.contains("uint32_t valueA;"));
|
QVERIFY(result.contains("uint32_t valueA;"));
|
||||||
QVERIFY(result.contains("uint64_t valueB;"));
|
QVERIFY(result.contains("uint64_t valueB;"));
|
||||||
QVERIFY(result.contains("static_assert(sizeof(StructA) == 0x4"));
|
QVERIFY(result.contains("static_assert(sizeof(StructA) == 0x4"));
|
||||||
@@ -508,9 +547,9 @@ private slots:
|
|||||||
root.parentId = 0;
|
root.parentId = 0;
|
||||||
tree.addNode(root);
|
tree.addNode(root);
|
||||||
|
|
||||||
QString result = rcx::renderCpp(tree, tree.nodes[0].id);
|
QString result = rcx::renderCpp(tree, tree.nodes[0].id, nullptr, true);
|
||||||
|
|
||||||
QVERIFY(result.contains("struct Empty {"));
|
QVERIFY(result.contains("struct Empty\n{"));
|
||||||
QVERIFY(result.contains("};"));
|
QVERIFY(result.contains("};"));
|
||||||
QVERIFY(result.contains("static_assert(sizeof(Empty) == 0x0"));
|
QVERIFY(result.contains("static_assert(sizeof(Empty) == 0x0"));
|
||||||
}
|
}
|
||||||
@@ -537,7 +576,7 @@ private slots:
|
|||||||
QString result = rcx::renderCpp(tree, rootId);
|
QString result = rcx::renderCpp(tree, rootId);
|
||||||
|
|
||||||
// Spaces and dashes should be replaced with underscores
|
// Spaces and dashes should be replaced with underscores
|
||||||
QVERIFY(result.contains("struct my_struct_name {"));
|
QVERIFY(result.contains("struct my_struct_name\n{"));
|
||||||
QVERIFY(result.contains("uint32_t field_with_spaces;"));
|
QVERIFY(result.contains("uint32_t field_with_spaces;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,7 +585,7 @@ private slots:
|
|||||||
void testExportToFile() {
|
void testExportToFile() {
|
||||||
auto tree = makeSimpleStruct();
|
auto tree = makeSimpleStruct();
|
||||||
uint64_t rootId = tree.nodes[0].id;
|
uint64_t rootId = tree.nodes[0].id;
|
||||||
QString text = rcx::renderCpp(tree, rootId);
|
QString text = rcx::renderCpp(tree, rootId, nullptr, true);
|
||||||
|
|
||||||
QTemporaryFile tmpFile;
|
QTemporaryFile tmpFile;
|
||||||
tmpFile.setAutoRemove(true);
|
tmpFile.setAutoRemove(true);
|
||||||
@@ -561,7 +600,7 @@ private slots:
|
|||||||
|
|
||||||
QString readStr = QString::fromUtf8(readBack);
|
QString readStr = QString::fromUtf8(readBack);
|
||||||
QVERIFY(readStr.contains("#pragma once"));
|
QVERIFY(readStr.contains("#pragma once"));
|
||||||
QVERIFY(readStr.contains("struct Player {"));
|
QVERIFY(readStr.contains("struct Player\n{"));
|
||||||
QVERIFY(readStr.contains("static_assert"));
|
QVERIFY(readStr.contains("static_assert"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,7 +621,7 @@ private slots:
|
|||||||
QVERIFY(!result.contains("struct "));
|
QVERIFY(!result.contains("struct "));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Deeply nested structs ──
|
// ── Deeply nested structs: referenced by name ──
|
||||||
|
|
||||||
void testDeeplyNested() {
|
void testDeeplyNested() {
|
||||||
rcx::NodeTree tree;
|
rcx::NodeTree tree;
|
||||||
@@ -623,20 +662,101 @@ private slots:
|
|||||||
|
|
||||||
QString result = rcx::renderCpp(tree, aId);
|
QString result = rcx::renderCpp(tree, aId);
|
||||||
|
|
||||||
// TypeC defined first, then TypeB, then TypeA
|
// Vergilius-style: named sub-types referenced by name with struct prefix
|
||||||
int cPos = result.indexOf("struct TypeC {");
|
// Only the root type gets a top-level definition
|
||||||
int bPos = result.indexOf("struct TypeB {");
|
QVERIFY(result.contains("struct TypeA\n{"));
|
||||||
int aPos = result.indexOf("struct TypeA {");
|
QVERIFY(result.contains("struct TypeB b;"));
|
||||||
QVERIFY(cPos >= 0);
|
}
|
||||||
QVERIFY(bPos >= 0);
|
|
||||||
QVERIFY(aPos >= 0);
|
|
||||||
QVERIFY(cPos < bPos);
|
|
||||||
QVERIFY(bPos < aPos);
|
|
||||||
|
|
||||||
// TypeA contains TypeB, TypeB contains TypeC
|
// ── Inline anonymous struct/union ──
|
||||||
QVERIFY(result.contains("TypeB b;"));
|
|
||||||
QVERIFY(result.contains("TypeC c;"));
|
void testInlineAnonymousStruct() {
|
||||||
QVERIFY(result.contains("uint8_t val;"));
|
rcx::NodeTree tree;
|
||||||
|
|
||||||
|
rcx::Node root;
|
||||||
|
root.kind = rcx::NodeKind::Struct;
|
||||||
|
root.name = "_MMPFN";
|
||||||
|
root.structTypeName = "_MMPFN";
|
||||||
|
root.parentId = 0;
|
||||||
|
int ri = tree.addNode(root);
|
||||||
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
|
// Anonymous union at offset 0 (no structTypeName)
|
||||||
|
rcx::Node anonUnion;
|
||||||
|
anonUnion.kind = rcx::NodeKind::Struct;
|
||||||
|
anonUnion.name = "";
|
||||||
|
anonUnion.structTypeName = "";
|
||||||
|
anonUnion.classKeyword = "union";
|
||||||
|
anonUnion.parentId = rootId;
|
||||||
|
anonUnion.offset = 0;
|
||||||
|
int ui = tree.addNode(anonUnion);
|
||||||
|
uint64_t unionId = tree.nodes[ui].id;
|
||||||
|
|
||||||
|
// Union member 1: named struct reference
|
||||||
|
rcx::Node listEntry;
|
||||||
|
listEntry.kind = rcx::NodeKind::Struct;
|
||||||
|
listEntry.name = "ListEntry";
|
||||||
|
listEntry.structTypeName = "_LIST_ENTRY";
|
||||||
|
listEntry.parentId = unionId;
|
||||||
|
listEntry.offset = 0;
|
||||||
|
tree.addNode(listEntry);
|
||||||
|
|
||||||
|
// Union member 2: a simple field
|
||||||
|
rcx::Node flags;
|
||||||
|
flags.kind = rcx::NodeKind::UInt64;
|
||||||
|
flags.name = "Flags";
|
||||||
|
flags.parentId = unionId;
|
||||||
|
flags.offset = 0;
|
||||||
|
tree.addNode(flags);
|
||||||
|
|
||||||
|
// Field after the anonymous union
|
||||||
|
rcx::Node pfn;
|
||||||
|
pfn.kind = rcx::NodeKind::UInt64;
|
||||||
|
pfn.name = "PfnCount";
|
||||||
|
pfn.parentId = rootId;
|
||||||
|
pfn.offset = 0x10;
|
||||||
|
tree.addNode(pfn);
|
||||||
|
|
||||||
|
QString result = rcx::renderCpp(tree, rootId);
|
||||||
|
|
||||||
|
// Anonymous union should be inlined, not a top-level anon_XXXX
|
||||||
|
QVERIFY(!result.contains("anon_"));
|
||||||
|
QVERIFY(result.contains("union\n {"));
|
||||||
|
QVERIFY(result.contains("struct _LIST_ENTRY ListEntry;"));
|
||||||
|
QVERIFY(result.contains("uint64_t Flags;"));
|
||||||
|
QVERIFY(result.contains("};"));
|
||||||
|
QVERIFY(result.contains("uint64_t PfnCount;"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Opaque types: no stub definition ──
|
||||||
|
|
||||||
|
void testOpaqueTypeNoStub() {
|
||||||
|
rcx::NodeTree tree;
|
||||||
|
|
||||||
|
rcx::Node root;
|
||||||
|
root.kind = rcx::NodeKind::Struct;
|
||||||
|
root.name = "Container";
|
||||||
|
root.structTypeName = "Container";
|
||||||
|
root.parentId = 0;
|
||||||
|
int ri = tree.addNode(root);
|
||||||
|
uint64_t rootId = tree.nodes[ri].id;
|
||||||
|
|
||||||
|
// Named struct child with no children of its own (opaque reference)
|
||||||
|
rcx::Node opaque;
|
||||||
|
opaque.kind = rcx::NodeKind::Struct;
|
||||||
|
opaque.name = "entry";
|
||||||
|
opaque.structTypeName = "_LIST_ENTRY";
|
||||||
|
opaque.parentId = rootId;
|
||||||
|
opaque.offset = 0;
|
||||||
|
tree.addNode(opaque);
|
||||||
|
|
||||||
|
QString result = rcx::renderCpp(tree, rootId);
|
||||||
|
|
||||||
|
// Should reference by name with struct prefix, no stub body
|
||||||
|
QVERIFY(result.contains("struct _LIST_ENTRY entry;"));
|
||||||
|
// Should NOT have a separate _LIST_ENTRY definition with padding
|
||||||
|
QVERIFY(!result.contains("struct _LIST_ENTRY\n{"));
|
||||||
|
QVERIFY(!result.contains("uint8_t _pad"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user