From 6c8b7d3d97d65912bc79e3eb45a1a81f6303ed15 Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Tue, 10 Mar 2026 15:05:23 -0600 Subject: [PATCH] feat: Rust/#define generators, code tab format/scope combos, enum #define support - Add Rust #[repr(C)] and #define offset code generators with dispatch - Add format combo + scope combo + gear button as corner widget on Code tab - Corner controls hidden on Reclass tab, shown only on Code tab - Chevron-down SVG arrows on combo dropdowns for consistent styling - Fix enum #define output: emit named members instead of empty 0x0 struct --- src/generator.cpp | 578 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 191 ++++++++++++++- 2 files changed, 757 insertions(+), 12 deletions(-) diff --git a/src/generator.cpp b/src/generator.cpp index 1560527..18a783d 100644 --- a/src/generator.cpp +++ b/src/generator.cpp @@ -134,6 +134,16 @@ static QString emitField(GenContext& ctx, const Node& node, int depth, int baseO return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc; case NodeKind::Pointer32: case NodeKind::Pointer64: { + // Relative pointer (RVA): emit as integer with comment, not a C pointer + if (node.isRelative) { + QString rvaComment = QStringLiteral(" // rva"); + if (node.refId != 0) { + int refIdx = tree.indexOfId(node.refId); + if (refIdx >= 0) + rvaComment += QStringLiteral(" -> ") + ctx.structName(tree.nodes[refIdx]); + } + return ind + QStringLiteral("%1 %2;").arg(ctx.cType(node.kind), name) + rvaComment + oc; + } if (node.refId != 0) { int refIdx = tree.indexOfId(node.refId); if (refIdx >= 0) { @@ -434,10 +444,440 @@ static QString alignComments(const QString& raw) { return result; } +// ═══════════════════════════════════════════════════════════════════ +// ── Rust backend ── +// ═══════════════════════════════════════════════════════════════════ + +static QString rustTypeName(NodeKind kind) { + switch (kind) { + case NodeKind::Hex8: return QStringLiteral("u8"); + case NodeKind::Hex16: return QStringLiteral("u16"); + case NodeKind::Hex32: return QStringLiteral("u32"); + case NodeKind::Hex64: return QStringLiteral("u64"); + case NodeKind::Int8: return QStringLiteral("i8"); + case NodeKind::Int16: return QStringLiteral("i16"); + case NodeKind::Int32: return QStringLiteral("i32"); + case NodeKind::Int64: return QStringLiteral("i64"); + case NodeKind::UInt8: return QStringLiteral("u8"); + case NodeKind::UInt16: return QStringLiteral("u16"); + case NodeKind::UInt32: return QStringLiteral("u32"); + case NodeKind::UInt64: return QStringLiteral("u64"); + case NodeKind::Float: return QStringLiteral("f32"); + case NodeKind::Double: return QStringLiteral("f64"); + case NodeKind::Bool: return QStringLiteral("bool"); + case NodeKind::Pointer32: return QStringLiteral("u32"); + case NodeKind::Pointer64: return QStringLiteral("u64"); + case NodeKind::FuncPtr32: return QStringLiteral("u32"); + case NodeKind::FuncPtr64: return QStringLiteral("u64"); + case NodeKind::Vec2: return QStringLiteral("f32"); + case NodeKind::Vec3: return QStringLiteral("f32"); + case NodeKind::Vec4: return QStringLiteral("f32"); + case NodeKind::Mat4x4: return QStringLiteral("f32"); + case NodeKind::UTF8: return QStringLiteral("u8"); + case NodeKind::UTF16: return QStringLiteral("u16"); + default: return QStringLiteral("u8"); + } +} + +// Forward declaration +static void emitRustStruct(GenContext& ctx, uint64_t structId); + +static QString rustType(GenContext& ctx, NodeKind kind) { + if (ctx.typeAliases) { + auto it = ctx.typeAliases->find(kind); + if (it != ctx.typeAliases->end() && !it.value().isEmpty()) + return it.value(); + } + return rustTypeName(kind); +} + +static QString emitRustField(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(baseOffset + node.offset); + + switch (node.kind) { + case NodeKind::Vec2: + return ind + QStringLiteral("pub %1: [f32; 2],").arg(name) + oc; + case NodeKind::Vec3: + return ind + QStringLiteral("pub %1: [f32; 3],").arg(name) + oc; + case NodeKind::Vec4: + return ind + QStringLiteral("pub %1: [f32; 4],").arg(name) + oc; + case NodeKind::Mat4x4: + return ind + QStringLiteral("pub %1: [[f32; 4]; 4],").arg(name) + oc; + case NodeKind::UTF8: + return ind + QStringLiteral("pub %1: [u8; %2],").arg(name).arg(node.strLen) + oc; + case NodeKind::UTF16: + return ind + QStringLiteral("pub %1: [u16; %2],").arg(name).arg(node.strLen) + oc; + case NodeKind::Pointer32: + case NodeKind::Pointer64: { + if (node.isRelative) { + QString comment = QStringLiteral(" // rva"); + if (node.refId != 0) { + int refIdx = tree.indexOfId(node.refId); + if (refIdx >= 0) + comment += QStringLiteral(" -> ") + ctx.structName(tree.nodes[refIdx]); + } + return ind + QStringLiteral("pub %1: %2,").arg(name, rustType(ctx, node.kind)) + comment + oc; + } + if (node.refId != 0) { + int refIdx = tree.indexOfId(node.refId); + if (refIdx >= 0) { + QString target = ctx.structName(tree.nodes[refIdx]); + return ind + QStringLiteral("pub %1: *mut %2,").arg(name, target) + oc; + } + } + bool isNativePtr = (node.kind == NodeKind::Pointer32 && ctx.tree.pointerSize <= 4) + || (node.kind == NodeKind::Pointer64 && ctx.tree.pointerSize >= 8); + if (isNativePtr) + return ind + QStringLiteral("pub %1: *mut core::ffi::c_void,").arg(name) + oc; + return ind + QStringLiteral("pub %1: %2,").arg(name, rustType(ctx, node.kind)) + oc; + } + case NodeKind::FuncPtr32: + case NodeKind::FuncPtr64: + return ind + QStringLiteral("pub %1: Option,").arg(name) + oc; + default: + return ind + QStringLiteral("pub %1: %2,").arg(name, rustType(ctx, node.kind)) + oc; + } +} + +static void emitRustStructBody(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 allChildren = ctx.childMap.value(structId); + QVector children, staticIdxs; + for (int ci : allChildren) { + if (tree.nodes[ci].isStatic) + staticIdxs.append(ci); + else + children.append(ci); + } + std::sort(children.begin(), children.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + + auto emitPadRun = [&](int relOffset, int size) { + if (size <= 0) return; + ctx.output += ind + QStringLiteral("pub %1: [u8; 0x%2],") + .arg(ctx.uniquePadName()) + .arg(QString::number(size, 16).toUpper()) + + offsetComment(baseOffset + relOffset) + QStringLiteral("\n"); + }; + + int cursor = 0; + int i = 0; + + while (i < children.size()) { + const Node& child = tree.nodes[children[i]]; + int childSize; + if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array) + childSize = tree.structSpan(child.id, &ctx.childMap); + else + childSize = child.byteSize(); + + if (!isUnion) { + if (child.offset > cursor) + emitPadRun(cursor, child.offset - cursor); + } + + if (isHexNode(child.kind)) { + int runStart = child.offset; + int runEnd = child.offset + childSize; + int j = i + 1; + while (j < children.size()) { + const Node& next = tree.nodes[children[j]]; + if (!isHexNode(next.kind)) break; + int nextSize = next.byteSize(); + if (next.offset < runEnd) break; + runEnd = next.offset + nextSize; + j++; + } + emitPadRun(runStart, runEnd - runStart); + cursor = runEnd; + i = j; + continue; + } + + if (child.kind == NodeKind::Struct) { + if (child.classKeyword == QStringLiteral("bitfield") + && !child.bitfieldMembers.isEmpty()) { + // Rust has no native bitfields — emit container + comment + QString bfType = rustType(ctx, child.elementKind); + if (bfType.isEmpty()) bfType = QStringLiteral("u32"); + QString fieldName = sanitizeIdent(child.name.isEmpty() + ? QStringLiteral("bitfield_%1").arg(child.offset, 2, 16, QChar('0')) + : child.name); + QStringList bits; + for (const auto& m : child.bitfieldMembers) + bits << QStringLiteral("%1:%2").arg(sanitizeIdent(m.name)).arg(m.bitWidth); + ctx.output += ind + QStringLiteral("pub %1: %2,") + .arg(fieldName, bfType) + + QStringLiteral(" // bits: ") + bits.join(QStringLiteral(", ")) + + offsetComment(baseOffset + child.offset) + QStringLiteral("\n"); + } else { + bool isAnonymous = child.structTypeName.isEmpty(); + if (isAnonymous) { + // Rust can't do anonymous inline structs — flatten as byte array + int span = tree.structSpan(child.id, &ctx.childMap); + QString fieldName = sanitizeIdent(child.name.isEmpty() + ? QStringLiteral("anon_%1").arg(child.offset, 2, 16, QChar('0')) + : child.name); + ctx.output += ind + QStringLiteral("pub %1: [u8; 0x%2],") + .arg(fieldName) + .arg(QString::number(span, 16).toUpper()) + + offsetComment(baseOffset + child.offset) + QStringLiteral("\n"); + } else { + 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 + QStringLiteral("pub %1: %2,") + .arg(fieldName, typeName) + + offsetComment(baseOffset + child.offset) + QStringLiteral("\n"); + } + } + } else if (child.kind == NodeKind::Array) { + QVector arrayKids = ctx.childMap.value(child.id); + bool hasStructChild = false; + QString elemTypeName; + for (int ak : arrayKids) { + if (tree.nodes[ak].kind == NodeKind::Struct) { + hasStructChild = true; + elemTypeName = ctx.structName(tree.nodes[ak]); + break; + } + } + QString fieldName = sanitizeIdent(child.name); + if (hasStructChild && !elemTypeName.isEmpty()) { + ctx.output += ind + QStringLiteral("pub %1: [%2; %3],") + .arg(fieldName, elemTypeName).arg(child.arrayLen) + + offsetComment(baseOffset + child.offset) + QStringLiteral("\n"); + } else { + ctx.output += ind + QStringLiteral("pub %1: [%2; %3],") + .arg(fieldName, rustType(ctx, child.elementKind)).arg(child.arrayLen) + + offsetComment(baseOffset + child.offset) + QStringLiteral("\n"); + } + } else { + ctx.output += emitRustField(ctx, child, depth, baseOffset) + QStringLiteral("\n"); + } + + int childEnd = child.offset + childSize; + if (childEnd > cursor) cursor = childEnd; + i++; + } + + if (!isUnion && cursor < structSize) + emitPadRun(cursor, structSize - cursor); + + for (int si : staticIdxs) { + const Node& sf = tree.nodes[si]; + QString sfType = sf.structTypeName.isEmpty() ? rustType(ctx, sf.kind) : sf.structTypeName; + ctx.output += ind + QStringLiteral("// static: %1 %2 @ %3\n") + .arg(sfType, sanitizeIdent(sf.name), sf.offsetExpr); + } +} + +static void emitRustStruct(GenContext& ctx, uint64_t structId) { + if (ctx.emittedIds.contains(structId)) return; + if (ctx.visiting.contains(structId)) return; + ctx.visiting.insert(structId); + + int idx = ctx.tree.indexOfId(structId); + if (idx < 0) { ctx.visiting.remove(structId); return; } + + const Node& node = ctx.tree.nodes[idx]; + if (node.kind != NodeKind::Struct) { ctx.visiting.remove(structId); return; } + + QString typeName = ctx.structName(node); + if (ctx.emittedTypeNames.contains(typeName)) { + ctx.emittedIds.insert(structId); + ctx.visiting.remove(structId); + return; + } + + ctx.emittedIds.insert(structId); + ctx.emittedTypeNames.insert(typeName); + int structSize = ctx.tree.structSpan(structId, &ctx.childMap); + + QString kw = node.resolvedClassKeyword(); + + // Enum with members + if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) { + ctx.output += QStringLiteral("#[repr(i64)]\npub enum %1 {\n").arg(typeName); + for (const auto& m : node.enumMembers) { + ctx.output += QStringLiteral(" %1 = %2,\n") + .arg(sanitizeIdent(m.first)) + .arg(m.second); + } + ctx.output += QStringLiteral("}\n\n"); + ctx.visiting.remove(structId); + return; + } + + bool isUnion = (kw == QStringLiteral("union")); + + if (isUnion) + ctx.output += QStringLiteral("#[repr(C)]\n#[derive(Copy, Clone)]\npub union %1 {\n").arg(typeName); + else + ctx.output += QStringLiteral("#[repr(C)]\npub struct %1 {\n").arg(typeName); + + emitRustStructBody(ctx, structId, isUnion, 1, 0); + + ctx.output += QStringLiteral("}") + + offsetComment(structSize, true) + + QStringLiteral("\n"); + if (ctx.emitAsserts) + ctx.output += QStringLiteral("const _: () = assert!(core::mem::size_of::<%1>() == 0x%2);\n") + .arg(typeName) + .arg(QString::number(structSize, 16).toUpper()); + ctx.output += QStringLiteral("\n"); + + ctx.visiting.remove(structId); +} + +// ═══════════════════════════════════════════════════════════════════ +// ── #define offsets backend ── +// ═══════════════════════════════════════════════════════════════════ + +static void emitDefinesForStruct(GenContext& ctx, uint64_t structId, + const QString& prefix, int baseOffset) { + int idx = ctx.tree.indexOfId(structId); + if (idx < 0) return; + + const Node& node = ctx.tree.nodes[idx]; + QString typeName = prefix.isEmpty() ? ctx.structName(node) : prefix; + QString kw = node.resolvedClassKeyword(); + + // Enum with members: emit #define EnumName_MemberName value + if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) { + ctx.output += QStringLiteral("// %1 (enum)\n").arg(typeName); + for (const auto& m : node.enumMembers) { + ctx.output += QStringLiteral("#define %1_%2 %3\n") + .arg(typeName, sanitizeIdent(m.first)) + .arg(m.second); + } + ctx.output += QStringLiteral("\n"); + return; + } + + int structSize = ctx.tree.structSpan(structId, &ctx.childMap); + ctx.output += QStringLiteral("// %1 (0x%2 bytes)\n") + .arg(typeName) + .arg(QString::number(structSize, 16).toUpper()); + + QVector children = ctx.childMap.value(structId); + std::sort(children.begin(), children.end(), [&](int a, int b) { + return ctx.tree.nodes[a].offset < ctx.tree.nodes[b].offset; + }); + + for (int ci : children) { + const Node& child = ctx.tree.nodes[ci]; + if (child.isStatic) continue; + if (isHexNode(child.kind)) continue; + + QString fieldName = sanitizeIdent(child.name.isEmpty() + ? QStringLiteral("field_%1").arg(child.offset, 2, 16, QChar('0')) + : child.name); + int absOffset = baseOffset + child.offset; + + ctx.output += QStringLiteral("#define %1_%2 0x%3\n") + .arg(typeName, fieldName) + .arg(QString::number(absOffset, 16).toUpper()); + + // Recurse into named sub-structs + if (child.kind == NodeKind::Struct && !child.structTypeName.isEmpty() + && child.classKeyword != QStringLiteral("bitfield")) { + emitDefinesForStruct(ctx, child.id, + typeName + QStringLiteral("_") + fieldName, absOffset); + } + } + ctx.output += QStringLiteral("\n"); +} + +// ═══════════════════════════════════════════════════════════════════ +// ── Reachable struct collector (for "Current + Children" scope) ── +// ═══════════════════════════════════════════════════════════════════ + +// Walk the tree from rootId, collecting all struct IDs reachable via +// named struct children and pointer references. Returns them in +// dependency order (leaves first, root last). +static QVector collectReachableStructs( + const NodeTree& tree, const QHash>& childMap, + uint64_t rootId) +{ + QVector result; + QSet visited; + + std::function walk = [&](uint64_t id) { + if (visited.contains(id)) return; + visited.insert(id); + + int idx = tree.indexOfId(id); + if (idx < 0) return; + const Node& node = tree.nodes[idx]; + if (node.kind != NodeKind::Struct) return; + + // Walk children first so dependencies come before the parent + for (int ci : childMap.value(id)) { + const Node& child = tree.nodes[ci]; + if (child.kind == NodeKind::Struct && !child.structTypeName.isEmpty()) + walk(child.id); + if ((child.kind == NodeKind::Pointer32 || child.kind == NodeKind::Pointer64) + && child.refId != 0) + walk(child.refId); + if (child.kind == NodeKind::Array) { + for (int ak : childMap.value(child.id)) + if (tree.nodes[ak].kind == NodeKind::Struct) + walk(tree.nodes[ak].id); + } + } + result.append(id); + }; + walk(rootId); + return result; +} + } // anonymous namespace // ── Public API ── +const char* codeFormatName(CodeFormat fmt) { + switch (fmt) { + case CodeFormat::CppHeader: return "C/C++"; + case CodeFormat::RustStruct: return "Rust"; + case CodeFormat::DefineOffsets: return "#define"; + default: return "C/C++"; + } +} + +const char* codeFormatFileFilter(CodeFormat fmt) { + switch (fmt) { + case CodeFormat::CppHeader: return "C++ Header (*.h);;All Files (*)"; + case CodeFormat::RustStruct: return "Rust Source (*.rs);;All Files (*)"; + case CodeFormat::DefineOffsets: return "C Header (*.h);;All Files (*)"; + default: return "All Files (*)"; + } +} + +const char* codeScopeName(CodeScope scope) { + switch (scope) { + case CodeScope::Current: return "Current"; + case CodeScope::WithChildren: return "Current + Deps"; + case CodeScope::FullSdk: return "Full SDK"; + default: return "Current"; + } +} + QString renderCpp(const NodeTree& tree, uint64_t rootStructId, const QHash* typeAliases, bool emitAsserts) { @@ -456,6 +896,23 @@ QString renderCpp(const NodeTree& tree, uint64_t rootStructId, return alignComments(ctx.output); } +QString renderCppTree(const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases, + bool emitAsserts) { + int idx = tree.indexOfId(rootStructId); + if (idx < 0) return {}; + if (tree.nodes[idx].kind != NodeKind::Struct) return {}; + + auto childMap = buildChildMap(tree); + GenContext ctx{tree, childMap, {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts}; + ctx.output += QStringLiteral("#pragma once\n\n"); + + for (uint64_t sid : collectReachableStructs(tree, childMap, rootStructId)) + emitStruct(ctx, sid); + + return alignComments(ctx.output); +} + QString renderCppAll(const NodeTree& tree, const QHash* typeAliases, bool emitAsserts) { @@ -476,6 +933,127 @@ QString renderCppAll(const NodeTree& tree, return alignComments(ctx.output); } +// ── Rust public API ── + +QString renderRust(const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases, + bool emitAsserts) { + int idx = tree.indexOfId(rootStructId); + if (idx < 0) return {}; + if (tree.nodes[idx].kind != NodeKind::Struct) return {}; + + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts}; + ctx.output += QStringLiteral("// Generated by Reclass 2027\n\n"); + emitRustStruct(ctx, rootStructId); + return alignComments(ctx.output); +} + +QString renderRustTree(const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases, + bool emitAsserts) { + int idx = tree.indexOfId(rootStructId); + if (idx < 0) return {}; + if (tree.nodes[idx].kind != NodeKind::Struct) return {}; + + auto childMap = buildChildMap(tree); + GenContext ctx{tree, childMap, {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts}; + ctx.output += QStringLiteral("// Generated by Reclass 2027\n\n"); + + for (uint64_t sid : collectReachableStructs(tree, childMap, rootStructId)) + emitRustStruct(ctx, sid); + + return alignComments(ctx.output); +} + +QString renderRustAll(const NodeTree& tree, + const QHash* typeAliases, + bool emitAsserts) { + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts}; + ctx.output += QStringLiteral("// Generated by Reclass 2027\n\n"); + + QVector roots = ctx.childMap.value(0); + std::sort(roots.begin(), roots.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + for (int ri : roots) { + if (tree.nodes[ri].kind == NodeKind::Struct) + emitRustStruct(ctx, tree.nodes[ri].id); + } + return alignComments(ctx.output); +} + +// ── #define public API ── + +QString renderDefines(const NodeTree& tree, uint64_t rootStructId) { + int idx = tree.indexOfId(rootStructId); + if (idx < 0) return {}; + if (tree.nodes[idx].kind != NodeKind::Struct) return {}; + + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, nullptr, false}; + ctx.output += QStringLiteral("#pragma once\n\n"); + emitDefinesForStruct(ctx, rootStructId, QString(), 0); + return ctx.output; +} + +QString renderDefinesTree(const NodeTree& tree, uint64_t rootStructId) { + int idx = tree.indexOfId(rootStructId); + if (idx < 0) return {}; + if (tree.nodes[idx].kind != NodeKind::Struct) return {}; + + auto childMap = buildChildMap(tree); + GenContext ctx{tree, childMap, {}, {}, {}, {}, {}, 0, nullptr, false}; + ctx.output += QStringLiteral("#pragma once\n\n"); + + for (uint64_t sid : collectReachableStructs(tree, childMap, rootStructId)) + emitDefinesForStruct(ctx, sid, QString(), 0); + + return ctx.output; +} + +QString renderDefinesAll(const NodeTree& tree) { + GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, nullptr, false}; + ctx.output += QStringLiteral("#pragma once\n\n"); + + QVector roots = ctx.childMap.value(0); + std::sort(roots.begin(), roots.end(), [&](int a, int b) { + return tree.nodes[a].offset < tree.nodes[b].offset; + }); + for (int ri : roots) { + if (tree.nodes[ri].kind == NodeKind::Struct) + emitDefinesForStruct(ctx, tree.nodes[ri].id, QString(), 0); + } + return ctx.output; +} + +// ── Format dispatch ── + +QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases, bool emitAsserts) { + switch (fmt) { + case CodeFormat::RustStruct: return renderRust(tree, rootStructId, typeAliases, emitAsserts); + case CodeFormat::DefineOffsets: return renderDefines(tree, rootStructId); + default: return renderCpp(tree, rootStructId, typeAliases, emitAsserts); + } +} + +QString renderCodeTree(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId, + const QHash* typeAliases, bool emitAsserts) { + switch (fmt) { + case CodeFormat::RustStruct: return renderRustTree(tree, rootStructId, typeAliases, emitAsserts); + case CodeFormat::DefineOffsets: return renderDefinesTree(tree, rootStructId); + default: return renderCppTree(tree, rootStructId, typeAliases, emitAsserts); + } +} + +QString renderCodeAll(CodeFormat fmt, const NodeTree& tree, + const QHash* typeAliases, bool emitAsserts) { + switch (fmt) { + case CodeFormat::RustStruct: return renderRustAll(tree, typeAliases, emitAsserts); + case CodeFormat::DefineOffsets: return renderDefinesAll(tree); + default: return renderCppAll(tree, typeAliases, emitAsserts); + } +} + QString renderNull(const NodeTree&, uint64_t) { return {}; } diff --git a/src/main.cpp b/src/main.cpp index 23fde32..4a54a1e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -731,6 +731,8 @@ void MainWindow::createMenus() { Qt5Qt6AddAction(importMenu, "&PDB...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importPdb); auto* exportMenu = file->addMenu("E&xport"); Qt5Qt6AddAction(exportMenu, "&C++ Header...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportCpp); + Qt5Qt6AddAction(exportMenu, "&Rust Structs...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportRust); + Qt5Qt6AddAction(exportMenu, "#&define Offsets...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportDefines); Qt5Qt6AddAction(exportMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction); // Examples submenu — scan once at init { @@ -872,7 +874,8 @@ void MainWindow::createMenus() { const auto mcpName = QSettings("Reclass", "Reclass").value("autoStartMcp", true).toBool() ? "Stop &MCP Server" : "Start &MCP Server"; m_mcpAction = Qt5Qt6AddAction(tools, mcpName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp); tools->addSeparator(); - Qt5Qt6AddAction(tools, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, &MainWindow::showOptionsDialog); + Qt5Qt6AddAction(tools, "&Options...", QKeySequence::UnknownKey, makeIcon(":/vsicons/settings-gear.svg"), this, + static_cast(&MainWindow::showOptionsDialog)); // Plugins auto* plugins = m_menuBar->addMenu("&Plugins"); @@ -1194,7 +1197,7 @@ void MainWindow::createStatusBar() { m_statusLabel->setContentsMargins(0, 0, 0, 0); m_statusLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); - // View toggle is now per-pane via QTabWidget tab bar (Reclass / C/C++ tabs) + // View toggle is now per-pane via QTabWidget tab bar (Reclass / Code tabs) sb->tabRow = nullptr; sb->label = m_statusLabel; @@ -1414,7 +1417,87 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) { sci->setFocus(); }); - pane.tabWidget->addTab(pane.renderedContainer, "C/C++"); // index 1 + pane.tabWidget->addTab(pane.renderedContainer, "Code"); // index 1 + + // Corner widget: format combo + gear icon + { + const auto& ct = ThemeManager::instance().current(); + QSettings cs("Reclass", "Reclass"); + QString ef = cs.value("font", "JetBrains Mono").toString(); + + auto* cornerWidget = new QWidget; + auto* cornerLayout = new QHBoxLayout(cornerWidget); + cornerLayout->setContentsMargins(0, 0, 4, 0); + cornerLayout->setSpacing(2); + + pane.fmtCombo = new QComboBox; + for (int fi = 0; fi < static_cast(CodeFormat::_Count); ++fi) + pane.fmtCombo->addItem(codeFormatName(static_cast(fi))); + pane.fmtCombo->setCurrentIndex(cs.value("codeFormat", 0).toInt()); + pane.fmtCombo->setFixedHeight(22); + pane.fmtCombo->setStyleSheet(QStringLiteral( + "QComboBox { background: %1; color: %2; border: 1px solid %3;" + " padding: 1px 6px; font-family: '%6'; font-size: 9pt; }" + "QComboBox::drop-down { border: none; width: 14px; }" + "QComboBox::down-arrow { image: url(:/vsicons/chevron-down.svg);" + " width: 10px; height: 10px; }" + "QComboBox QAbstractItemView { background: %4; color: %2;" + " selection-background-color: %5; border: 1px solid %3; }") + .arg(ct.background.name(), ct.textMuted.name(), ct.border.name(), + ct.backgroundAlt.name(), ct.hover.name(), ef)); + + pane.fmtGear = new QToolButton; + pane.fmtGear->setIcon(QIcon(":/vsicons/settings-gear.svg")); + pane.fmtGear->setFixedSize(22, 22); + pane.fmtGear->setToolTip("Generator Options"); + pane.fmtGear->setStyleSheet(QStringLiteral( + "QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }" + "QToolButton:hover { background: %4; }") + .arg(ct.background.name(), ct.textMuted.name(), ct.border.name(), + ct.hover.name())); + + pane.scopeCombo = new QComboBox; + for (int si = 0; si < static_cast(CodeScope::_Count); ++si) + pane.scopeCombo->addItem(codeScopeName(static_cast(si))); + pane.scopeCombo->setCurrentIndex(cs.value("codeScope", 0).toInt()); + pane.scopeCombo->setFixedHeight(22); + pane.scopeCombo->setStyleSheet(pane.fmtCombo->styleSheet()); + + cornerLayout->addWidget(pane.fmtCombo); + cornerLayout->addWidget(pane.scopeCombo); + cornerLayout->addWidget(pane.fmtGear); + pane.tabWidget->setCornerWidget(cornerWidget, Qt::BottomRightCorner); + cornerWidget->setVisible(false); // hidden until Code tab selected + + auto refreshAllRendered = [this]() { + for (auto& tab : m_tabs) + for (auto& p : tab.panes) + if (p.viewMode == VM_Rendered) + updateRenderedView(tab, p); + }; + + connect(pane.fmtCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, [this, refreshAllRendered](int idx) { + QSettings("Reclass", "Reclass").setValue("codeFormat", idx); + refreshAllRendered(); + for (auto& tab : m_tabs) + for (auto& p : tab.panes) + if (p.fmtCombo && p.fmtCombo->currentIndex() != idx) + p.fmtCombo->setCurrentIndex(idx); + }); + connect(pane.scopeCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, [this, refreshAllRendered](int idx) { + QSettings("Reclass", "Reclass").setValue("codeScope", idx); + refreshAllRendered(); + for (auto& tab : m_tabs) + for (auto& p : tab.panes) + if (p.scopeCombo && p.scopeCombo->currentIndex() != idx) + p.scopeCombo->setCurrentIndex(idx); + }); + connect(pane.fmtGear, &QToolButton::clicked, this, [this]() { + showOptionsDialog(2); // Generator page + }); + } pane.tabWidget->setCurrentIndex(0); pane.viewMode = VM_Reclass; @@ -1428,6 +1511,10 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) { SplitPane* p = findPaneByTabWidget(tw); if (!p) return; + // Show/hide corner controls (format combo, scope combo, gear) + if (auto* cw = tw->cornerWidget(Qt::BottomRightCorner)) + cw->setVisible(index == 1); + p->viewMode = (index == 1) ? VM_Rendered : VM_Reclass; // Sync status bar buttons if this is the active pane @@ -2553,7 +2640,7 @@ void MainWindow::applyTheme(const Theme& theme) { } } - // Restyle per-pane view tab bars (Reclass / C++) + // Restyle per-pane view tab bars (Reclass / Code) { QString editorFont = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString(); QString paneTabStyle = QStringLiteral( @@ -2569,10 +2656,31 @@ void MainWindow::applyTheme(const Theme& theme) { .arg(theme.background.name(), theme.textMuted.name(), theme.text.name(), theme.backgroundAlt.name(), theme.hover.name(), theme.indHoverSpan.name(), editorFont); + QString comboStyle = QStringLiteral( + "QComboBox { background: %1; color: %2; border: 1px solid %3;" + " padding: 1px 6px; font-family: '%6'; font-size: 9pt; }" + "QComboBox::drop-down { border: none; width: 14px; }" + "QComboBox::down-arrow { image: url(:/vsicons/chevron-down.svg);" + " width: 10px; height: 10px; }" + "QComboBox QAbstractItemView { background: %4; color: %2;" + " selection-background-color: %5; border: 1px solid %3; }") + .arg(theme.background.name(), theme.textMuted.name(), theme.border.name(), + theme.backgroundAlt.name(), theme.hover.name(), editorFont); + QString gearStyle = QStringLiteral( + "QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }" + "QToolButton:hover { background: %4; }") + .arg(theme.background.name(), theme.textMuted.name(), theme.border.name(), + theme.hover.name()); for (auto it = m_tabs.begin(); it != m_tabs.end(); ++it) { for (auto& pane : it->panes) { if (pane.tabWidget) pane.tabWidget->setStyleSheet(paneTabStyle); + if (pane.fmtCombo) + pane.fmtCombo->setStyleSheet(comboStyle); + if (pane.scopeCombo) + pane.scopeCombo->setStyleSheet(comboStyle); + if (pane.fmtGear) + pane.fmtGear->setStyleSheet(gearStyle); } } } @@ -2717,7 +2825,7 @@ void MainWindow::applyTheme(const Theme& theme) { } } - // Rendered C/C++ views: update lexer colors, paper, margins + // Rendered Code views: update lexer colors, paper, margins for (auto& tab : m_tabs) { for (auto& pane : tab.panes) { auto* sci = pane.rendered; @@ -2763,7 +2871,9 @@ void MainWindow::editTheme() { } // TODO: when adding more and more options, this func becomes very clunky. Fix -void MainWindow::showOptionsDialog() { +void MainWindow::showOptionsDialog() { showOptionsDialog(-1); } + +void MainWindow::showOptionsDialog(int initialPage) { auto& tm = ThemeManager::instance(); OptionsResult current; current.themeIndex = tm.currentIndex(); @@ -2778,7 +2888,9 @@ void MainWindow::showOptionsDialog() { current.braceWrap = QSettings("Reclass", "Reclass").value("braceWrap", false).toBool(); OptionsDialog dlg(current, this); - if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK + if (initialPage >= 0) + dlg.selectPage(initialPage); + if (dlg.exec() != QDialog::Accepted) return; auto r = dlg.result(); @@ -2874,7 +2986,7 @@ void MainWindow::setEditorFont(const QString& fontName) { tabBar->update(); } } - // Pane tab bars (Reclass / C++) — re-apply stylesheet with new font + // Pane tab bars (Reclass / Code) — re-apply stylesheet with new font // (stylesheet overrides setFont, so font must be in the CSS) applyTheme(ThemeManager::instance().current()); } @@ -3049,11 +3161,21 @@ void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) { const QHash* aliases = tab.doc->typeAliases.isEmpty() ? nullptr : &tab.doc->typeAliases; bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool(); + CodeFormat fmt = static_cast( + QSettings("Reclass", "Reclass").value("codeFormat", 0).toInt()); + CodeScope scope = static_cast( + QSettings("Reclass", "Reclass").value("codeScope", 0).toInt()); QString text; - if (rootId != 0) - text = renderCpp(tab.doc->tree, rootId, aliases, asserts); - else - text = renderCppAll(tab.doc->tree, aliases, asserts); + if (scope == CodeScope::FullSdk) { + text = renderCodeAll(fmt, tab.doc->tree, aliases, asserts); + } else if (rootId != 0) { + if (scope == CodeScope::WithChildren) + text = renderCodeTree(fmt, tab.doc->tree, rootId, aliases, asserts); + else + text = renderCode(fmt, tab.doc->tree, rootId, aliases, asserts); + } else { + text = renderCodeAll(fmt, tab.doc->tree, aliases, asserts); + } // Scroll restoration: save if same root, reset if different int restoreLine = 0; @@ -3124,6 +3246,51 @@ void MainWindow::exportCpp() { setAppStatus("Exported to " + QFileInfo(path).fileName()); } +// ── Export Rust structs ── + +void MainWindow::exportRust() { + auto* tab = activeTab(); + if (!tab) return; + + QString path = QFileDialog::getSaveFileName(this, + "Export Rust Structs", {}, "Rust Source (*.rs);;All Files (*)"); + if (path.isEmpty()) return; + + const QHash* aliases = + tab->doc->typeAliases.isEmpty() ? nullptr : &tab->doc->typeAliases; + bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool(); + QString text = renderRustAll(tab->doc->tree, aliases, asserts); + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(this, "Export Failed", + "Could not write to: " + path); + return; + } + file.write(text.toUtf8()); + setAppStatus("Exported to " + QFileInfo(path).fileName()); +} + +// ── Export #define offsets ── + +void MainWindow::exportDefines() { + auto* tab = activeTab(); + if (!tab) return; + + QString path = QFileDialog::getSaveFileName(this, + "Export #define Offsets", {}, "C Header (*.h);;All Files (*)"); + if (path.isEmpty()) return; + + QString text = renderDefinesAll(tab->doc->tree); + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::warning(this, "Export Failed", + "Could not write to: " + path); + return; + } + file.write(text.toUtf8()); + setAppStatus("Exported to " + QFileInfo(path).fileName()); +} + // ── Export ReClass XML ── void MainWindow::exportReclassXmlAction() {