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
This commit is contained in:
IChooseYou
2026-03-10 15:05:23 -06:00
committed by IChooseYou
parent d1321b5165
commit 6c8b7d3d97
2 changed files with 757 additions and 12 deletions

View File

@@ -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; return ind + QStringLiteral("%1 %2[%3];").arg(ctx.cType(NodeKind::UTF16), name).arg(node.strLen) + oc;
case NodeKind::Pointer32: case NodeKind::Pointer32:
case NodeKind::Pointer64: { 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) { if (node.refId != 0) {
int refIdx = tree.indexOfId(node.refId); int refIdx = tree.indexOfId(node.refId);
if (refIdx >= 0) { if (refIdx >= 0) {
@@ -434,10 +444,440 @@ static QString alignComments(const QString& raw) {
return result; 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<unsafe extern \"C\" fn()>,").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<int> allChildren = ctx.childMap.value(structId);
QVector<int> 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<int> 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<int> 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<uint64_t> collectReachableStructs(
const NodeTree& tree, const QHash<uint64_t, QVector<int>>& childMap,
uint64_t rootId)
{
QVector<uint64_t> result;
QSet<uint64_t> visited;
std::function<void(uint64_t)> 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 } // anonymous namespace
// ── Public API ── // ── 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, QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* typeAliases, const QHash<NodeKind, QString>* typeAliases,
bool emitAsserts) { bool emitAsserts) {
@@ -456,6 +896,23 @@ QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
return alignComments(ctx.output); return alignComments(ctx.output);
} }
QString renderCppTree(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* 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, QString renderCppAll(const NodeTree& tree,
const QHash<NodeKind, QString>* typeAliases, const QHash<NodeKind, QString>* typeAliases,
bool emitAsserts) { bool emitAsserts) {
@@ -476,6 +933,127 @@ QString renderCppAll(const NodeTree& tree,
return alignComments(ctx.output); return alignComments(ctx.output);
} }
// ── Rust public API ──
QString renderRust(const NodeTree& tree, uint64_t rootStructId,
const QHash<NodeKind, QString>* 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<NodeKind, QString>* 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<NodeKind, QString>* typeAliases,
bool emitAsserts) {
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts};
ctx.output += QStringLiteral("// Generated by Reclass 2027\n\n");
QVector<int> 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<int> 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<NodeKind, QString>* 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<NodeKind, QString>* 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<NodeKind, QString>* 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) { QString renderNull(const NodeTree&, uint64_t) {
return {}; return {};
} }

View File

@@ -731,6 +731,8 @@ void MainWindow::createMenus() {
Qt5Qt6AddAction(importMenu, "&PDB...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importPdb); Qt5Qt6AddAction(importMenu, "&PDB...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::importPdb);
auto* exportMenu = file->addMenu("E&xport"); auto* exportMenu = file->addMenu("E&xport");
Qt5Qt6AddAction(exportMenu, "&C++ Header...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportCpp); 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); Qt5Qt6AddAction(exportMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
// Examples submenu — scan once at init // 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"; 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); m_mcpAction = Qt5Qt6AddAction(tools, mcpName, QKeySequence::UnknownKey, QIcon(), this, &MainWindow::toggleMcp);
tools->addSeparator(); 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<void(MainWindow::*)()>(&MainWindow::showOptionsDialog));
// Plugins // Plugins
auto* plugins = m_menuBar->addMenu("&Plugins"); auto* plugins = m_menuBar->addMenu("&Plugins");
@@ -1194,7 +1197,7 @@ void MainWindow::createStatusBar() {
m_statusLabel->setContentsMargins(0, 0, 0, 0); m_statusLabel->setContentsMargins(0, 0, 0, 0);
m_statusLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); 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->tabRow = nullptr;
sb->label = m_statusLabel; sb->label = m_statusLabel;
@@ -1414,7 +1417,87 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
sci->setFocus(); 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<int>(CodeFormat::_Count); ++fi)
pane.fmtCombo->addItem(codeFormatName(static_cast<CodeFormat>(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<int>(CodeScope::_Count); ++si)
pane.scopeCombo->addItem(codeScopeName(static_cast<CodeScope>(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<int>::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<int>::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.tabWidget->setCurrentIndex(0);
pane.viewMode = VM_Reclass; pane.viewMode = VM_Reclass;
@@ -1428,6 +1511,10 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
SplitPane* p = findPaneByTabWidget(tw); SplitPane* p = findPaneByTabWidget(tw);
if (!p) return; 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; p->viewMode = (index == 1) ? VM_Rendered : VM_Reclass;
// Sync status bar buttons if this is the active pane // 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 editorFont = QSettings("Reclass", "Reclass").value("font", "JetBrains Mono").toString();
QString paneTabStyle = QStringLiteral( QString paneTabStyle = QStringLiteral(
@@ -2569,10 +2656,31 @@ void MainWindow::applyTheme(const Theme& theme) {
.arg(theme.background.name(), theme.textMuted.name(), theme.text.name(), .arg(theme.background.name(), theme.textMuted.name(), theme.text.name(),
theme.backgroundAlt.name(), theme.hover.name(), theme.indHoverSpan.name(), theme.backgroundAlt.name(), theme.hover.name(), theme.indHoverSpan.name(),
editorFont); 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 it = m_tabs.begin(); it != m_tabs.end(); ++it) {
for (auto& pane : it->panes) { for (auto& pane : it->panes) {
if (pane.tabWidget) if (pane.tabWidget)
pane.tabWidget->setStyleSheet(paneTabStyle); 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& tab : m_tabs) {
for (auto& pane : tab.panes) { for (auto& pane : tab.panes) {
auto* sci = pane.rendered; auto* sci = pane.rendered;
@@ -2763,7 +2871,9 @@ void MainWindow::editTheme() {
} }
// TODO: when adding more and more options, this func becomes very clunky. Fix // 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(); auto& tm = ThemeManager::instance();
OptionsResult current; OptionsResult current;
current.themeIndex = tm.currentIndex(); current.themeIndex = tm.currentIndex();
@@ -2778,7 +2888,9 @@ void MainWindow::showOptionsDialog() {
current.braceWrap = QSettings("Reclass", "Reclass").value("braceWrap", false).toBool(); current.braceWrap = QSettings("Reclass", "Reclass").value("braceWrap", false).toBool();
OptionsDialog dlg(current, this); OptionsDialog dlg(current, this);
if (dlg.exec() != QDialog::Accepted) return; // OptionsDialog doesn't apply anything. Only apply on OK if (initialPage >= 0)
dlg.selectPage(initialPage);
if (dlg.exec() != QDialog::Accepted) return;
auto r = dlg.result(); auto r = dlg.result();
@@ -2874,7 +2986,7 @@ void MainWindow::setEditorFont(const QString& fontName) {
tabBar->update(); 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) // (stylesheet overrides setFont, so font must be in the CSS)
applyTheme(ThemeManager::instance().current()); applyTheme(ThemeManager::instance().current());
} }
@@ -3049,11 +3161,21 @@ void MainWindow::updateRenderedView(TabState& tab, SplitPane& pane) {
const QHash<NodeKind, QString>* aliases = const QHash<NodeKind, QString>* aliases =
tab.doc->typeAliases.isEmpty() ? nullptr : &tab.doc->typeAliases; tab.doc->typeAliases.isEmpty() ? nullptr : &tab.doc->typeAliases;
bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool(); bool asserts = QSettings("Reclass", "Reclass").value("generatorAsserts", false).toBool();
CodeFormat fmt = static_cast<CodeFormat>(
QSettings("Reclass", "Reclass").value("codeFormat", 0).toInt());
CodeScope scope = static_cast<CodeScope>(
QSettings("Reclass", "Reclass").value("codeScope", 0).toInt());
QString text; QString text;
if (rootId != 0) if (scope == CodeScope::FullSdk) {
text = renderCpp(tab.doc->tree, rootId, aliases, asserts); text = renderCodeAll(fmt, tab.doc->tree, aliases, asserts);
else } else if (rootId != 0) {
text = renderCppAll(tab.doc->tree, aliases, asserts); 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 // Scroll restoration: save if same root, reset if different
int restoreLine = 0; int restoreLine = 0;
@@ -3124,6 +3246,51 @@ void MainWindow::exportCpp() {
setAppStatus("Exported to " + QFileInfo(path).fileName()); 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<NodeKind, QString>* 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 ── // ── Export ReClass XML ──
void MainWindow::exportReclassXmlAction() { void MainWindow::exportReclassXmlAction() {