diff --git a/src/editor.cpp b/src/editor.cpp index af17c48..e816fb8 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -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 {" diff --git a/src/generator.cpp b/src/generator.cpp index b0b87b0..06bb52c 100644 --- a/src/generator.cpp +++ b/src/generator.cpp @@ -68,6 +68,7 @@ struct GenContext { QString output; int padCounter = 0; const QHash* 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 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 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 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 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 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* typeAliases) { + const QHash* 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* typeAliases) { - GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases}; + const QHash* typeAliases, + bool emitAsserts) { + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts}; ctx.output += QStringLiteral("#pragma once\n\n"); diff --git a/src/generator.h b/src/generator.h index fcf31ac..61bafe3 100644 --- a/src/generator.h +++ b/src/generator.h @@ -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* typeAliases = nullptr); + const QHash* typeAliases = nullptr, + bool emitAsserts = false); // Generate C++ struct definitions for every root-level struct (full SDK). QString renderCppAll(const NodeTree& tree, - const QHash* typeAliases = nullptr); + const QHash* typeAliases = nullptr, + bool emitAsserts = false); // Null generator placeholder (returns empty string). QString renderNull(const NodeTree& tree, uint64_t rootStructId); diff --git a/src/main.cpp b/src/main.cpp index 9425958..4c82214 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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(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* 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* 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", diff --git a/src/mcp/mcp_bridge.cpp b/src/mcp/mcp_bridge.cpp index a48a12b..7067087 100644 --- a/src/mcp/mcp_bridge.cpp +++ b/src/mcp/mcp_bridge.cpp @@ -4,6 +4,7 @@ #include "generator.h" #include "mainwindow.h" #include +#include #include #include @@ -1094,15 +1095,16 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) { if (action == "export_cpp") { if (!doc) return makeTextResult("No active tab", true); const QHash* 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) { diff --git a/src/optionsdialog.cpp b/src/optionsdialog.cpp index 413b5bc..40dc931 100644 --- a/src/optionsdialog.cpp +++ b/src/optionsdialog.cpp @@ -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; } diff --git a/src/optionsdialog.h b/src/optionsdialog.h index dea2d4d..314c797 100644 --- a/src/optionsdialog.h +++ b/src/optionsdialog.h @@ -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 m_pageKeywords; diff --git a/tests/test_editor.cpp b/tests/test_editor.cpp index 3ac16c7..10587b0 100644 --- a/tests/test_editor.cpp +++ b/tests/test_editor.cpp @@ -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) diff --git a/tests/test_generator.cpp b/tests/test_generator.cpp index 6e2e84e..364deb7 100644 --- a/tests/test_generator.cpp +++ b/tests/test_generator.cpp @@ -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 ")); - 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")); } };