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:
IChooseYou
2026-02-26 08:21:15 -07:00
committed by IChooseYou
parent 52f751e751
commit 1465e7fbed
9 changed files with 362 additions and 171 deletions

View File

@@ -1468,39 +1468,35 @@ static ColumnSpan headerNameSpan(const LineMeta& lm, const QString& lineText) {
}
// Type name span for struct headers (not arrays)
// Format: "struct TYPENAME NAME {" or collapsed variants
// For "struct NAME {" (no typename), returns invalid span
// Named structs format as: "_MMPTE OriginalPte {" (type column = just the name)
// Anonymous structs format as: "union {" or "struct {" (no clickable type)
static ColumnSpan headerTypeNameSpan(const LineMeta& lm, const QString& lineText) {
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 typeW = lm.effectiveTypeW;
int typeEnd = ind + typeW;
// Clamp to actual line content
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();
if (typeCol.isEmpty()) return {};
// Find first space (after "struct")
int firstSpace = typeCol.indexOf(' ');
if (firstSpace < 0) return {}; // Just "struct", no typename
// Anonymous structs use bare keywords — not clickable
static const QStringList kKeywords = {
QStringLiteral("struct"), QStringLiteral("union"), QStringLiteral("class")
};
if (kKeywords.contains(typeCol)) return {};
// If there's content after "struct ", that's the typename
QString typename_ = typeCol.mid(firstSpace + 1).trimmed();
if (typename_.isEmpty()) return {};
// Named struct: entire type column is the type name (e.g. "_MMPTE")
// Find the actual text bounds within the padded column
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
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};
return {start, end, true};
}
// Type span for array headers: "int32_t[10]" in "int32_t[10] positions {"

View File

@@ -68,6 +68,7 @@ struct GenContext {
QString output;
int padCounter = 0;
const QHash<NodeKind, QString>* typeAliases = nullptr;
bool emitAsserts = false;
QString uniquePadName() {
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());
}
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;
QString ind = indent(depth);
QString name = sanitizeIdent(node.name.isEmpty()
? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0'))
: node.name);
QString oc = offsetComment(node.offset);
QString oc = offsetComment(baseOffset + node.offset);
switch (node.kind) {
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:
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:
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:
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:
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:
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: {
if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
QString target = ctx.structName(tree.nodes[refIdx]);
return QStringLiteral(" %1 %2;").arg(ctx.cType(NodeKind::Pointer32), name) +
offsetComment(node.offset).replace(QStringLiteral("//"), QStringLiteral("// -> %1*").arg(target));
return ind + QStringLiteral("struct %1* %2;").arg(target, name) + oc;
}
}
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: {
if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) {
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:
return QStringLiteral(" void (*%1)();").arg(name) + oc;
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;
case NodeKind::FuncPtr64:
return QStringLiteral(" void (*%1)();").arg(name) + oc;
return ind + QStringLiteral("void (*%1)();").arg(name) + oc;
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;
int idx = tree.indexOfId(structId);
if (idx < 0) return;
int structSize = tree.structSpan(structId, &ctx.childMap);
QString ind = indent(depth);
QVector<int> children = ctx.childMap.value(structId);
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
auto emitPadRun = [&](int offset, int size) {
auto emitPadRun = [&](int relOffset, int size) {
if (size <= 0) return;
ctx.output += QStringLiteral(" %1 %2[0x%3];%4\n")
.arg(QStringLiteral("uint8_t"))
ctx.output += ind + QStringLiteral("uint8_t %1[0x%2];%3\n")
.arg(ctx.uniquePadName())
.arg(QString::number(size, 16).toUpper())
.arg(offsetComment(offset));
.arg(offsetComment(baseOffset + relOffset));
};
int cursor = 0;
@@ -189,13 +195,15 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
else
childSize = child.byteSize();
// Gap before this field
if (child.offset > cursor)
emitPadRun(cursor, child.offset - cursor);
else if (child.offset < cursor)
ctx.output += QStringLiteral(" // WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n")
.arg(QString::number(child.offset, 16).toUpper())
.arg(QString::number(cursor, 16).toUpper());
// Gap/overlap handling (skip for unions)
if (!isUnion) {
if (child.offset > cursor)
emitPadRun(cursor, child.offset - cursor);
else if (child.offset < cursor)
ctx.output += ind + QStringLiteral("// WARNING: overlap at offset 0x%1 (previous field ends at 0x%2)\n")
.arg(QString::number(baseOffset + child.offset, 16).toUpper())
.arg(QString::number(baseOffset + cursor, 16).toUpper());
}
// Collapse consecutive hex nodes into a single padding array
if (isHexNode(child.kind)) {
@@ -206,8 +214,7 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
const Node& next = tree.nodes[children[j]];
if (!isHexNode(next.kind)) break;
int nextSize = next.byteSize();
// Allow gaps within the run (they become part of the pad)
if (next.offset < runEnd) break; // overlap — stop merging
if (next.offset < runEnd) break;
runEnd = next.offset + nextSize;
j++;
}
@@ -219,10 +226,31 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
// Emit the field
if (child.kind == NodeKind::Struct) {
emitStruct(ctx, child.id);
QString typeName = ctx.structName(child);
QString fieldName = sanitizeIdent(child.name);
ctx.output += QStringLiteral(" %1 %2;%3\n").arg(typeName, fieldName, offsetComment(child.offset));
bool isAnonymous = child.structTypeName.isEmpty();
if (isAnonymous) {
// 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) {
QVector<int> arrayKids = ctx.childMap.value(child.id);
bool hasStructChild = false;
@@ -231,7 +259,6 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
for (int ak : arrayKids) {
if (tree.nodes[ak].kind == NodeKind::Struct) {
hasStructChild = true;
emitStruct(ctx, tree.nodes[ak].id);
elemTypeName = ctx.structName(tree.nodes[ak]);
break;
}
@@ -239,14 +266,16 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
QString fieldName = sanitizeIdent(child.name);
if (hasStructChild && !elemTypeName.isEmpty()) {
ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
.arg(elemTypeName, fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
ctx.output += ind + QStringLiteral("struct %1 %2[%3];%4\n")
.arg(elemTypeName, fieldName).arg(child.arrayLen)
.arg(offsetComment(baseOffset + child.offset));
} else {
ctx.output += QStringLiteral(" %1 %2[%3];%4\n")
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen).arg(offsetComment(child.offset));
ctx.output += ind + QStringLiteral("%1 %2[%3];%4\n")
.arg(ctx.cType(child.elementKind), fieldName).arg(child.arrayLen)
.arg(offsetComment(baseOffset + child.offset));
}
} else {
ctx.output += emitField(ctx, child) + QStringLiteral("\n");
ctx.output += emitField(ctx, child, depth, baseOffset) + QStringLiteral("\n");
}
int childEnd = child.offset + childSize;
@@ -254,12 +283,12 @@ static void emitStructBody(GenContext& ctx, uint64_t structId) {
i++;
}
// Tail padding
if (cursor < structSize)
// Tail padding (skip for unions)
if (!isUnion && cursor < structSize)
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) {
if (ctx.emittedIds.contains(structId)) return;
@@ -275,19 +304,12 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
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) {
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);
return;
}
// Deduplicate by struct type name (different nodes may share the same type)
// Deduplicate by struct type name
QString typeName = ctx.structName(node);
if (ctx.emittedTypeNames.contains(typeName)) {
ctx.emittedIds.insert(structId);
@@ -295,34 +317,6 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
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.emittedTypeNames.insert(typeName);
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
@@ -342,15 +336,21 @@ static void emitStruct(GenContext& ctx, uint64_t structId) {
return;
}
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct"); // enum without members: fallback
ctx.output += QStringLiteral("%1 %2 {\n").arg(kw, typeName);
if (kw == QStringLiteral("enum")) kw = QStringLiteral("struct");
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("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n\n")
.arg(typeName)
.arg(QString::number(structSize, 16).toUpper());
if (ctx.emitAsserts)
ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n")
.arg(typeName)
.arg(QString::number(structSize, 16).toUpper());
ctx.output += QStringLiteral("\n");
ctx.visiting.remove(structId);
}
@@ -404,14 +404,15 @@ static QString alignComments(const QString& raw) {
// ── Public API ──
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);
if (idx < 0) return {};
const Node& root = tree.nodes[idx];
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");
@@ -421,8 +422,9 @@ QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
}
QString renderCppAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases) {
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases};
const QHash<NodeKind, QString>* typeAliases,
bool emitAsserts) {
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts};
ctx.output += QStringLiteral("#pragma once\n\n");

View File

@@ -9,11 +9,13 @@ namespace rcx {
// Generate C++ struct definitions for a single root struct and all
// nested/referenced types reachable from it.
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).
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).
QString renderNull(const NodeTree& tree, uint64_t rootStructId);

View File

@@ -267,6 +267,16 @@ public:
// Transparent menu bar background (no CSS needed)
if (elem == PE_PanelMenuBar)
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);
}
void drawControl(ControlElement element, const QStyleOption* opt,
@@ -1804,6 +1814,7 @@ void MainWindow::showOptionsDialog() {
current.safeMode = QSettings("Reclass", "Reclass").value("safeMode", false).toBool();
current.autoStartMcp = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool();
current.refreshMs = QSettings("Reclass", "Reclass").value("refreshMs", 660).toInt();
current.generatorAsserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
OptionsDialog dlg(current, this);
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)
tab.ctrl->setRefreshInterval(r.refreshMs);
}
if (r.generatorAsserts != current.generatorAsserts)
QSettings("Reclass", "Reclass").setValue("generatorAsserts", r.generatorAsserts);
}
void MainWindow::setEditorFont(const QString& fontName) {
@@ -2023,11 +2037,12 @@ void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) {
// Generate text
const QHash<NodeKind, QString>* aliases =
tab.doc->typeAliases.isEmpty() ? nullptr : &tab.doc->typeAliases;
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
QString text;
if (rootId != 0)
text = renderCpp(tab.doc->tree, rootId, aliases);
text = renderCpp(tab.doc->tree, rootId, aliases, asserts);
else
text = renderCppAll(tab.doc->tree, aliases);
text = renderCppAll(tab.doc->tree, aliases, asserts);
// Scroll restoration: save if same root, reset if different
int restoreLine = 0;
@@ -2071,7 +2086,8 @@ void MainWindow::exportCpp() {
const QHash<NodeKind, QString>* aliases =
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);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QMessageBox::warning(this, "Export Failed",

View File

@@ -4,6 +4,7 @@
#include "generator.h"
#include "mainwindow.h"
#include <QCoreApplication>
#include <QSettings>
#include <QDebug>
#include <cstring>
@@ -1094,15 +1095,16 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
if (action == "export_cpp") {
if (!doc) return makeTextResult("No active tab", true);
const QHash<NodeKind, QString>* aliases = doc->typeAliases.isEmpty() ? nullptr : &doc->typeAliases;
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
QString code;
if (!nodeIdStr.isEmpty()) {
// Per-struct export
uint64_t nid = nodeIdStr.toULongLong();
code = renderCpp(doc->tree, nid, aliases);
code = renderCpp(doc->tree, nid, aliases, asserts);
if (code.isEmpty())
return makeTextResult("Node not found or not a struct: " + nodeIdStr, true);
} else {
code = renderCppAll(doc->tree, aliases);
code = renderCppAll(doc->tree, aliases, asserts);
}
// Truncate if too large (64 KB limit)
if (code.size() > 65536) {

View File

@@ -170,6 +170,14 @@ OptionsDialog::OptionsDialog(const OptionsResult& current, QWidget* parent)
auto* generatorLayout = new QVBoxLayout(generatorPage);
generatorLayout->setContentsMargins(0, 0, 0, 0);
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();
m_pages->addWidget(generatorPage); // index 2
@@ -208,6 +216,7 @@ OptionsResult OptionsDialog::result() const {
r.safeMode = m_safeModeCheck->isChecked();
r.autoStartMcp = m_autoMcpCheck->isChecked();
r.refreshMs = m_refreshSpin->value();
r.generatorAsserts = m_assertCheck->isChecked();
return r;
}

View File

@@ -18,6 +18,7 @@ struct OptionsResult {
bool safeMode = false;
bool autoStartMcp = true;
int refreshMs = 660;
bool generatorAsserts = false;
};
class OptionsDialog : public QDialog {
@@ -41,6 +42,7 @@ private:
QCheckBox* m_safeModeCheck = nullptr;
QCheckBox* m_autoMcpCheck = nullptr;
QSpinBox* m_refreshSpin = nullptr;
QCheckBox* m_assertCheck = nullptr;
// searchable keywords per leaf tree item
QHash<QTreeWidgetItem*, QStringList> m_pageKeywords;

View File

@@ -2514,6 +2514,48 @@ private slots:
<< QString("gapRight=%1 gapBottom=%2 (font-independent)")
.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)

View File

@@ -46,27 +46,37 @@ private:
private slots:
// ── Basic struct generation ──
// ── Basic struct generation (Vergilius-style) ──
void testSimpleStruct() {
auto tree = makeSimpleStruct();
uint64_t rootId = tree.nodes[0].id;
QString result = rcx::renderCpp(tree, rootId);
QString result = rcx::renderCpp(tree, rootId, nullptr, true);
// Header
QVERIFY(result.contains("#pragma once"));
QVERIFY(!result.contains("#include <cstdint>"));
QVERIFY(!result.contains("#pragma pack"));
// Struct definition
QVERIFY(result.contains("struct Player {"));
// Size comment (Vergilius-style)
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("float speed;"));
QVERIFY(result.contains("uint64_t id;"));
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"));
// Without emitAsserts, static_assert should not appear
QString noAsserts = rcx::renderCpp(tree, rootId);
QVERIFY(!noAsserts.contains("static_assert"));
}
// ── Padding gap detection ──
@@ -134,7 +144,7 @@ private slots:
f2.offset = 16;
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
QVERIFY(result.contains("[0xF]"));
@@ -175,7 +185,47 @@ private slots:
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() {
rcx::NodeTree tree;
@@ -222,23 +272,14 @@ private slots:
f2.offset = 8;
tree.addNode(f2);
QString result = rcx::renderCpp(tree, outerId);
QString result = rcx::renderCpp(tree, outerId, nullptr, true);
// Inner struct should be defined before outer
int innerPos = result.indexOf("struct Vec2f {");
int outerPos = result.indexOf("struct Outer {");
QVERIFY(innerPos >= 0);
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;"));
// Vergilius-style: named sub-types referenced by name with struct prefix
// No separate top-level definition for Vec2f in renderCpp
QVERIFY(result.contains("struct Outer\n{"));
QVERIFY(result.contains("struct Vec2f pos;"));
QVERIFY(result.contains("int32_t score;"));
QVERIFY(result.contains("static_assert(sizeof(Outer) == 0xC"));
}
// ── Primitive array ──
@@ -325,15 +366,12 @@ private slots:
QString result = rcx::renderCpp(tree, mainId);
// ptr64 with target → real C++ pointer
QVERIFY(result.contains("TargetData* pTarget;"));
// Vergilius-style: struct prefix on pointer targets
QVERIFY(result.contains("struct TargetData* pTarget;"));
// ptr64 without target → void*
QVERIFY(result.contains("void* pVoid;"));
// ptr32 with target → uint32_t with comment
QVERIFY(result.contains("uint32_t pTarget32;"));
QVERIFY(result.contains("-> TargetData*"));
// Forward declaration for TargetData
QVERIFY(result.contains("struct TargetData;"));
// ptr32 with target → struct X* (Vergilius-style, no forward decl needed)
QVERIFY(result.contains("struct TargetData* pTarget32;"));
}
// ── Vector and matrix types ──
@@ -457,10 +495,11 @@ private slots:
bf.offset = 0;
tree.addNode(bf);
QString result = rcx::renderCppAll(tree);
QString result = rcx::renderCppAll(tree, nullptr, true);
QVERIFY(result.contains("struct StructA {"));
QVERIFY(result.contains("struct StructB {"));
// Vergilius-style: brace on new line
QVERIFY(result.contains("struct StructA\n{"));
QVERIFY(result.contains("struct StructB\n{"));
QVERIFY(result.contains("uint32_t valueA;"));
QVERIFY(result.contains("uint64_t valueB;"));
QVERIFY(result.contains("static_assert(sizeof(StructA) == 0x4"));
@@ -508,9 +547,9 @@ private slots:
root.parentId = 0;
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("static_assert(sizeof(Empty) == 0x0"));
}
@@ -537,7 +576,7 @@ private slots:
QString result = rcx::renderCpp(tree, rootId);
// 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;"));
}
@@ -546,7 +585,7 @@ private slots:
void testExportToFile() {
auto tree = makeSimpleStruct();
uint64_t rootId = tree.nodes[0].id;
QString text = rcx::renderCpp(tree, rootId);
QString text = rcx::renderCpp(tree, rootId, nullptr, true);
QTemporaryFile tmpFile;
tmpFile.setAutoRemove(true);
@@ -561,7 +600,7 @@ private slots:
QString readStr = QString::fromUtf8(readBack);
QVERIFY(readStr.contains("#pragma once"));
QVERIFY(readStr.contains("struct Player {"));
QVERIFY(readStr.contains("struct Player\n{"));
QVERIFY(readStr.contains("static_assert"));
}
@@ -582,7 +621,7 @@ private slots:
QVERIFY(!result.contains("struct "));
}
// ── Deeply nested structs ──
// ── Deeply nested structs: referenced by name ──
void testDeeplyNested() {
rcx::NodeTree tree;
@@ -623,20 +662,101 @@ private slots:
QString result = rcx::renderCpp(tree, aId);
// TypeC defined first, then TypeB, then TypeA
int cPos = result.indexOf("struct TypeC {");
int bPos = result.indexOf("struct TypeB {");
int aPos = result.indexOf("struct TypeA {");
QVERIFY(cPos >= 0);
QVERIFY(bPos >= 0);
QVERIFY(aPos >= 0);
QVERIFY(cPos < bPos);
QVERIFY(bPos < aPos);
// Vergilius-style: named sub-types referenced by name with struct prefix
// Only the root type gets a top-level definition
QVERIFY(result.contains("struct TypeA\n{"));
QVERIFY(result.contains("struct TypeB b;"));
}
// TypeA contains TypeB, TypeB contains TypeC
QVERIFY(result.contains("TypeB b;"));
QVERIFY(result.contains("TypeC c;"));
QVERIFY(result.contains("uint8_t val;"));
// ── Inline anonymous struct/union ──
void testInlineAnonymousStruct() {
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"));
}
};