mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add C++ code generator, rendered view, fix Hex node display
- Add generator.h/cpp: C++ struct emission with padding gaps, tail padding, pragma pack, static_assert, nested structs, pointers - Add rendered view (QStackedWidget per tab, QsciScintilla + C++ lexer) - View menu: C/C++ switches to rendered view, Reclass View switches back - File > Export C++ Header writes full SDK to .h file - Fix Hex8-Hex64 display: show name + value columns like normal types instead of hiding name behind ASCII preview (only Padding keeps that) - Update column span detection, compose width calc, controller edit dispatch, and editor dimming to match new Hex layout - Sample data: Entity struct with health, armor, speed, flags fields - Add test_generator (21 tests) and test_validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ add_executable(ReclassX
|
||||
src/controller.cpp
|
||||
src/compose.cpp
|
||||
src/format.cpp
|
||||
src/generator.h
|
||||
src/generator.cpp
|
||||
src/processpicker.h
|
||||
src/processpicker.cpp
|
||||
src/processpicker.ui
|
||||
@@ -58,6 +60,7 @@ foreach(_f
|
||||
\"${CMAKE_SOURCE_DIR}/src/controller.cpp\"
|
||||
\"${CMAKE_SOURCE_DIR}/src/compose.cpp\"
|
||||
\"${CMAKE_SOURCE_DIR}/src/format.cpp\"
|
||||
\"${CMAKE_SOURCE_DIR}/src/generator.cpp\"
|
||||
\"${CMAKE_SOURCE_DIR}/src/main.cpp\")
|
||||
file(READ \${_f} _content)
|
||||
file(APPEND \${_out} \${_content})
|
||||
@@ -125,4 +128,19 @@ if(BUILD_TESTING)
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
add_test(NAME test_controller COMMAND test_controller)
|
||||
|
||||
add_executable(test_validation tests/test_validation.cpp
|
||||
src/editor.cpp src/compose.cpp src/format.cpp src/controller.cpp
|
||||
src/processpicker.cpp src/processpicker.ui)
|
||||
target_include_directories(test_validation PRIVATE src)
|
||||
target_link_libraries(test_validation PRIVATE
|
||||
Qt6::Widgets Qt6::PrintSupport Qt6::Test
|
||||
QScintilla::QScintilla dbghelp psapi)
|
||||
add_test(NAME test_validation COMMAND test_validation)
|
||||
|
||||
add_executable(test_generator tests/test_generator.cpp
|
||||
src/generator.cpp src/compose.cpp src/format.cpp)
|
||||
target_include_directories(test_generator PRIVATE src)
|
||||
target_link_libraries(test_generator PRIVATE Qt6::Core Qt6::Test)
|
||||
add_test(NAME test_generator COMMAND test_generator)
|
||||
endif()
|
||||
|
||||
@@ -401,8 +401,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
||||
// Include struct/array names - they now use columnar layout too
|
||||
int maxNameLen = kMinNameW;
|
||||
for (const Node& node : tree.nodes) {
|
||||
// Skip hex/padding (they show ASCII preview, not name column)
|
||||
if (isHexPreview(node.kind)) continue;
|
||||
// Skip padding (it shows ASCII preview, not name column)
|
||||
if (node.kind == NodeKind::Padding) continue;
|
||||
maxNameLen = qMax(maxNameLen, (int)node.name.size());
|
||||
}
|
||||
state.nameW = qBound(kMinNameW, maxNameLen, kMaxNameW);
|
||||
@@ -420,8 +420,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
scopeMaxType = qMax(scopeMaxType, (int)nodeTypeName(child).size());
|
||||
|
||||
// Name width (skip hex/padding, but include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
// Name width (skip padding, but include hex and containers)
|
||||
if (child.kind != NodeKind::Padding) {
|
||||
scopeMaxName = qMax(scopeMaxName, (int)child.name.size());
|
||||
}
|
||||
}
|
||||
@@ -439,8 +439,8 @@ ComposeResult compose(const NodeTree& tree, const Provider& prov) {
|
||||
const Node& child = tree.nodes[childIdx];
|
||||
rootMaxType = qMax(rootMaxType, (int)nodeTypeName(child).size());
|
||||
|
||||
// Name width (skip hex/padding, include containers)
|
||||
if (!isHexPreview(child.kind)) {
|
||||
// Name width (skip padding, include hex and containers)
|
||||
if (child.kind != NodeKind::Padding) {
|
||||
rootMaxName = qMax(rootMaxName, (int)child.name.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,8 +172,8 @@ void RcxController::connectEditor(RcxEditor* editor) {
|
||||
case EditTarget::Name: {
|
||||
if (text.isEmpty()) break;
|
||||
const Node& node = m_doc->tree.nodes[nodeIdx];
|
||||
// ASCII edit on Hex/Padding nodes
|
||||
if (isHexPreview(node.kind)) {
|
||||
// ASCII edit on Padding nodes
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
setNodeValue(nodeIdx, subLine, text, /*isAscii=*/true);
|
||||
} else {
|
||||
renameNode(nodeIdx, text);
|
||||
|
||||
22
src/core.h
22
src/core.h
@@ -486,8 +486,8 @@ inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int name
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
int start = ind + typeW + kSepWidth;
|
||||
|
||||
// Hex/Padding: ASCII preview takes the name column position (8 chars)
|
||||
if (isHexPreview(lm.nodeKind))
|
||||
// Padding: ASCII preview takes the name column position (8 chars)
|
||||
if (lm.nodeKind == NodeKind::Padding)
|
||||
return {start, start + 8, true};
|
||||
|
||||
return {start, start + nameW, true};
|
||||
@@ -498,12 +498,12 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
||||
lm.lineKind == LineKind::ArrayElementSeparator) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
// Hex/Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
|
||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHexPad ? 23 : kColValue; // hex bytes or value column
|
||||
// Padding layout: [Type][sep][ASCII(8)][sep][hex bytes(23)]
|
||||
bool isPad = (lm.nodeKind == NodeKind::Padding);
|
||||
int valWidth = isPad ? 23 : kColValue;
|
||||
|
||||
if (lm.isContinuation) {
|
||||
int prefixW = isHexPad
|
||||
int prefixW = isPad
|
||||
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (typeW + nameW + 2 * kSepWidth);
|
||||
int start = ind + prefixW;
|
||||
@@ -511,7 +511,7 @@ inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW
|
||||
}
|
||||
if (lm.lineKind != LineKind::Field) return {};
|
||||
|
||||
int start = isHexPad
|
||||
int start = isPad
|
||||
? (ind + typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (ind + typeW + kSepWidth + nameW + kSepWidth);
|
||||
return {start, start + valWidth, true};
|
||||
@@ -521,17 +521,17 @@ inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW =
|
||||
if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer) return {};
|
||||
int ind = kFoldCol + lm.depth * 3;
|
||||
|
||||
bool isHexPad = isHexPreview(lm.nodeKind);
|
||||
int valWidth = isHexPad ? 23 : kColValue;
|
||||
bool isPad = (lm.nodeKind == NodeKind::Padding);
|
||||
int valWidth = isPad ? 23 : kColValue;
|
||||
|
||||
int start;
|
||||
if (lm.isContinuation) {
|
||||
int prefixW = isHexPad
|
||||
int prefixW = isPad
|
||||
? (typeW + kSepWidth + 8 + kSepWidth)
|
||||
: (typeW + nameW + 2 * kSepWidth);
|
||||
start = ind + prefixW + valWidth;
|
||||
} else {
|
||||
start = isHexPad
|
||||
start = isPad
|
||||
? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth)
|
||||
: (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth);
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ void RcxEditor::fillIndicatorCols(int indic, int line, int colA, int colB) {
|
||||
void RcxEditor::applyHexDimming(const QVector<LineMeta>& meta) {
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_SETINDICATORCURRENT, IND_HEX_DIM);
|
||||
for (int i = 0; i < meta.size(); i++) {
|
||||
if (isHexPreview(meta[i].nodeKind)) {
|
||||
if (meta[i].nodeKind == NodeKind::Padding) {
|
||||
long pos, len; lineRangeNoEol(m_sci, i, pos, len);
|
||||
if (len > 0)
|
||||
m_sci->SendScintilla(QsciScintillaBase::SCI_INDICATORFILLRANGE, pos, len);
|
||||
@@ -1678,7 +1678,7 @@ void RcxEditor::applyHoverCursor() {
|
||||
bool inHexDataArea = false;
|
||||
uint64_t hoverNodeId = 0;
|
||||
if (hoverLine >= 0 && hoverLine < m_meta.size()
|
||||
&& isHexPreview(m_meta[hoverLine].nodeKind)) {
|
||||
&& m_meta[hoverLine].nodeKind == NodeKind::Padding) {
|
||||
hoverNodeId = m_meta[hoverLine].nodeId;
|
||||
if (hoverNodeId != 0 && h.col >= 0) {
|
||||
int ind = kFoldCol + m_meta[hoverLine].depth * 3;
|
||||
|
||||
@@ -328,27 +328,18 @@ QString fmtNodeLine(const Node& node, const Provider& prov,
|
||||
return ind + QString(prefixW, ' ') + val + cmtSuffix;
|
||||
}
|
||||
|
||||
// Hex nodes and Padding: ASCII preview + hex bytes (compact)
|
||||
if (isHexPreview(node.kind)) {
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
const int totalSz = qMax(1, node.arrayLen);
|
||||
const int lineOff = subLine * 8;
|
||||
const int lineBytes = qMin(8, totalSz - lineOff);
|
||||
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
|
||||
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
|
||||
QString ascii = bytesToAscii(b, lineBytes);
|
||||
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
|
||||
if (subLine == 0)
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
||||
}
|
||||
// Hex8..Hex64: single line, ASCII padded to 8 chars so hex column aligns
|
||||
const int sz = sizeForKind(node.kind);
|
||||
QByteArray b = prov.isReadable(addr, sz)
|
||||
? prov.readBytes(addr, sz) : QByteArray(sz, '\0');
|
||||
QString ascii = bytesToAscii(b, sz).leftJustified(8, ' ');
|
||||
QString hex = bytesToHex(b, sz).leftJustified(23, ' ');
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
// Padding: ASCII preview + hex bytes (compact, multi-line)
|
||||
if (node.kind == NodeKind::Padding) {
|
||||
const int totalSz = qMax(1, node.arrayLen);
|
||||
const int lineOff = subLine * 8;
|
||||
const int lineBytes = qMin(8, totalSz - lineOff);
|
||||
QByteArray b = prov.isReadable(addr + lineOff, lineBytes)
|
||||
? prov.readBytes(addr + lineOff, lineBytes) : QByteArray(lineBytes, '\0');
|
||||
QString ascii = bytesToAscii(b, lineBytes);
|
||||
QString hex = bytesToHex(b, lineBytes).leftJustified(23, ' '); // 8*3-1
|
||||
if (subLine == 0)
|
||||
return ind + type + SEP + ascii + SEP + hex + cmtSuffix;
|
||||
return ind + QString(colType + (int)SEP.size(), ' ') + ascii + SEP + hex + cmtSuffix;
|
||||
}
|
||||
|
||||
QString val = fit(readValue(node, prov, addr, subLine), COL_VALUE);
|
||||
|
||||
369
src/generator.cpp
Normal file
369
src/generator.cpp
Normal file
@@ -0,0 +1,369 @@
|
||||
#include "generator.h"
|
||||
#include <QHash>
|
||||
#include <QVector>
|
||||
#include <QStringList>
|
||||
#include <algorithm>
|
||||
|
||||
namespace rcx {
|
||||
|
||||
namespace {
|
||||
|
||||
// ── Identifier sanitisation ──
|
||||
|
||||
static QString sanitizeIdent(const QString& name) {
|
||||
if (name.isEmpty()) return QStringLiteral("unnamed");
|
||||
QString out;
|
||||
out.reserve(name.size());
|
||||
for (QChar c : name) {
|
||||
if (c.isLetterOrNumber() || c == '_') out += c;
|
||||
else out += '_';
|
||||
}
|
||||
if (!out[0].isLetter() && out[0] != '_')
|
||||
out.prepend('_');
|
||||
return out;
|
||||
}
|
||||
|
||||
// ── C type name for a primitive NodeKind ──
|
||||
|
||||
static QString cTypeName(NodeKind kind) {
|
||||
switch (kind) {
|
||||
case NodeKind::Hex8: return QStringLiteral("uint8_t");
|
||||
case NodeKind::Hex16: return QStringLiteral("uint16_t");
|
||||
case NodeKind::Hex32: return QStringLiteral("uint32_t");
|
||||
case NodeKind::Hex64: return QStringLiteral("uint64_t");
|
||||
case NodeKind::Int8: return QStringLiteral("int8_t");
|
||||
case NodeKind::Int16: return QStringLiteral("int16_t");
|
||||
case NodeKind::Int32: return QStringLiteral("int32_t");
|
||||
case NodeKind::Int64: return QStringLiteral("int64_t");
|
||||
case NodeKind::UInt8: return QStringLiteral("uint8_t");
|
||||
case NodeKind::UInt16: return QStringLiteral("uint16_t");
|
||||
case NodeKind::UInt32: return QStringLiteral("uint32_t");
|
||||
case NodeKind::UInt64: return QStringLiteral("uint64_t");
|
||||
case NodeKind::Float: return QStringLiteral("float");
|
||||
case NodeKind::Double: return QStringLiteral("double");
|
||||
case NodeKind::Bool: return QStringLiteral("bool");
|
||||
case NodeKind::Pointer32: return QStringLiteral("uint32_t");
|
||||
case NodeKind::Pointer64: return QStringLiteral("uint64_t");
|
||||
case NodeKind::Vec2: return QStringLiteral("float");
|
||||
case NodeKind::Vec3: return QStringLiteral("float");
|
||||
case NodeKind::Vec4: return QStringLiteral("float");
|
||||
case NodeKind::Mat4x4: return QStringLiteral("float");
|
||||
case NodeKind::UTF8: return QStringLiteral("char");
|
||||
case NodeKind::UTF16: return QStringLiteral("wchar_t");
|
||||
case NodeKind::Padding: return QStringLiteral("uint8_t");
|
||||
default: return QStringLiteral("uint8_t");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Generator context ──
|
||||
|
||||
struct GenContext {
|
||||
const NodeTree& tree;
|
||||
QHash<uint64_t, QVector<int>> childMap;
|
||||
QSet<QString> emittedTypeNames; // struct type names already emitted
|
||||
QSet<uint64_t> emittedIds; // struct node IDs already emitted
|
||||
QSet<uint64_t> visiting; // cycle guard
|
||||
QSet<uint64_t> forwardDeclared; // forward-declared type IDs
|
||||
QString output;
|
||||
int padCounter = 0;
|
||||
|
||||
QString uniquePadName() {
|
||||
return QStringLiteral("_pad%1").arg(padCounter++, 4, 16, QChar('0'));
|
||||
}
|
||||
|
||||
// Resolve the canonical type name for a struct/array node
|
||||
QString structName(const Node& n) const {
|
||||
if (!n.structTypeName.isEmpty()) return sanitizeIdent(n.structTypeName);
|
||||
if (!n.name.isEmpty()) return sanitizeIdent(n.name);
|
||||
return QStringLiteral("anon_%1").arg(n.id, 0, 16);
|
||||
}
|
||||
};
|
||||
|
||||
// Forward declarations
|
||||
static void emitStruct(GenContext& ctx, uint64_t structId);
|
||||
|
||||
// ── Emit a single field declaration ──
|
||||
|
||||
static QString emitField(GenContext& ctx, const Node& node) {
|
||||
const NodeTree& tree = ctx.tree;
|
||||
QString name = sanitizeIdent(node.name.isEmpty()
|
||||
? QStringLiteral("field_%1").arg(node.offset, 2, 16, QChar('0'))
|
||||
: node.name);
|
||||
|
||||
switch (node.kind) {
|
||||
case NodeKind::Vec2:
|
||||
return QStringLiteral(" float %1[2];").arg(name);
|
||||
case NodeKind::Vec3:
|
||||
return QStringLiteral(" float %1[3];").arg(name);
|
||||
case NodeKind::Vec4:
|
||||
return QStringLiteral(" float %1[4];").arg(name);
|
||||
case NodeKind::Mat4x4:
|
||||
return QStringLiteral(" float %1[4][4];").arg(name);
|
||||
case NodeKind::UTF8:
|
||||
return QStringLiteral(" char %1[%2];").arg(name).arg(node.strLen);
|
||||
case NodeKind::UTF16:
|
||||
return QStringLiteral(" wchar_t %1[%2];").arg(name).arg(node.strLen);
|
||||
case NodeKind::Padding:
|
||||
return QStringLiteral(" uint8_t %1[%2];").arg(name).arg(qMax(1, node.arrayLen));
|
||||
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(" uint32_t %1; // -> %2*").arg(name, target);
|
||||
}
|
||||
}
|
||||
return QStringLiteral(" uint32_t %1;").arg(name);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
return QStringLiteral(" void* %1;").arg(name);
|
||||
}
|
||||
default:
|
||||
return QStringLiteral(" %1 %2;").arg(cTypeName(node.kind), name);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Emit struct body (fields + padding) ──
|
||||
|
||||
static void emitStructBody(GenContext& ctx, uint64_t structId) {
|
||||
const NodeTree& tree = ctx.tree;
|
||||
int idx = tree.indexOfId(structId);
|
||||
if (idx < 0) return;
|
||||
|
||||
int structSize = tree.structSpan(structId, &ctx.childMap);
|
||||
|
||||
QVector<int> children = ctx.childMap.value(structId);
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
int cursor = 0;
|
||||
|
||||
for (int ci : children) {
|
||||
const Node& child = tree.nodes[ci];
|
||||
int childSize;
|
||||
if (child.kind == NodeKind::Struct || child.kind == NodeKind::Array)
|
||||
childSize = tree.structSpan(child.id, &ctx.childMap);
|
||||
else
|
||||
childSize = child.byteSize();
|
||||
|
||||
// Gap before this field
|
||||
if (child.offset > cursor) {
|
||||
int gap = child.offset - cursor;
|
||||
ctx.output += QStringLiteral(" uint8_t %1[0x%2];\n")
|
||||
.arg(ctx.uniquePadName())
|
||||
.arg(QString::number(gap, 16).toUpper());
|
||||
} else if (child.offset < cursor) {
|
||||
// Overlap
|
||||
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());
|
||||
}
|
||||
|
||||
// Emit the field
|
||||
if (child.kind == NodeKind::Struct) {
|
||||
// Ensure the nested struct type is emitted first
|
||||
emitStruct(ctx, child.id);
|
||||
QString typeName = ctx.structName(child);
|
||||
QString fieldName = sanitizeIdent(child.name);
|
||||
ctx.output += QStringLiteral(" %1 %2;\n").arg(typeName, fieldName);
|
||||
} else if (child.kind == NodeKind::Array) {
|
||||
// Check if array has struct element children
|
||||
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;
|
||||
emitStruct(ctx, tree.nodes[ak].id);
|
||||
elemTypeName = ctx.structName(tree.nodes[ak]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QString fieldName = sanitizeIdent(child.name);
|
||||
if (hasStructChild && !elemTypeName.isEmpty()) {
|
||||
ctx.output += QStringLiteral(" %1 %2[%3];\n")
|
||||
.arg(elemTypeName, fieldName).arg(child.arrayLen);
|
||||
} else {
|
||||
ctx.output += QStringLiteral(" %1 %2[%3];\n")
|
||||
.arg(cTypeName(child.elementKind), fieldName).arg(child.arrayLen);
|
||||
}
|
||||
} else {
|
||||
ctx.output += emitField(ctx, child) + QStringLiteral("\n");
|
||||
}
|
||||
|
||||
int childEnd = child.offset + childSize;
|
||||
if (childEnd > cursor) cursor = childEnd;
|
||||
}
|
||||
|
||||
// Tail padding
|
||||
if (cursor < structSize) {
|
||||
int gap = structSize - cursor;
|
||||
ctx.output += QStringLiteral(" uint8_t %1[0x%2];\n")
|
||||
.arg(ctx.uniquePadName())
|
||||
.arg(QString::number(gap, 16).toUpper());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Emit a complete struct definition ──
|
||||
|
||||
static void emitStruct(GenContext& ctx, uint64_t structId) {
|
||||
if (ctx.emittedIds.contains(structId)) return;
|
||||
if (ctx.visiting.contains(structId)) return; // cycle
|
||||
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 && node.kind != NodeKind::Array) {
|
||||
ctx.visiting.remove(structId);
|
||||
return;
|
||||
}
|
||||
|
||||
// For arrays, we don't emit a top-level struct — the array itself
|
||||
// is a field inside its parent. But we do emit struct element types.
|
||||
if (node.kind == NodeKind::Array) {
|
||||
QVector<int> kids = ctx.childMap.value(structId);
|
||||
for (int ki : kids) {
|
||||
if (ctx.tree.nodes[ki].kind == NodeKind::Struct)
|
||||
emitStruct(ctx, ctx.tree.nodes[ki].id);
|
||||
}
|
||||
ctx.visiting.remove(structId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Emit nested struct types first (dependency order)
|
||||
QVector<int> children = ctx.childMap.value(structId);
|
||||
for (int ci : children) {
|
||||
const Node& child = ctx.tree.nodes[ci];
|
||||
if (child.kind == NodeKind::Struct)
|
||||
emitStruct(ctx, child.id);
|
||||
else if (child.kind == NodeKind::Array) {
|
||||
QVector<int> arrayKids = ctx.childMap.value(child.id);
|
||||
for (int ak : arrayKids) {
|
||||
if (ctx.tree.nodes[ak].kind == NodeKind::Struct)
|
||||
emitStruct(ctx, ctx.tree.nodes[ak].id);
|
||||
}
|
||||
}
|
||||
// Forward-declare pointer target types if they're outside this subtree
|
||||
if (child.kind == NodeKind::Pointer64 && child.refId != 0) {
|
||||
int refIdx = ctx.tree.indexOfId(child.refId);
|
||||
if (refIdx >= 0 && !ctx.emittedIds.contains(child.refId)
|
||||
&& !ctx.forwardDeclared.contains(child.refId)) {
|
||||
QString fwdName = ctx.structName(ctx.tree.nodes[refIdx]);
|
||||
ctx.output += QStringLiteral("struct %1;\n").arg(fwdName);
|
||||
ctx.forwardDeclared.insert(child.refId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.emittedIds.insert(structId);
|
||||
|
||||
QString typeName = ctx.structName(node);
|
||||
int structSize = ctx.tree.structSpan(structId, &ctx.childMap);
|
||||
|
||||
ctx.output += QStringLiteral("#pragma pack(push, 1)\n");
|
||||
ctx.output += QStringLiteral("struct %1 {\n").arg(typeName);
|
||||
|
||||
emitStructBody(ctx, structId);
|
||||
|
||||
ctx.output += QStringLiteral("};\n");
|
||||
ctx.output += QStringLiteral("#pragma pack(pop)\n");
|
||||
ctx.output += QStringLiteral("static_assert(sizeof(%1) == 0x%2, \"Size mismatch for %1\");\n\n")
|
||||
.arg(typeName)
|
||||
.arg(QString::number(structSize, 16).toUpper());
|
||||
|
||||
ctx.visiting.remove(structId);
|
||||
}
|
||||
|
||||
// ── Build the child map used by all generators ──
|
||||
|
||||
static QHash<uint64_t, QVector<int>> buildChildMap(const NodeTree& tree) {
|
||||
QHash<uint64_t, QVector<int>> map;
|
||||
for (int i = 0; i < tree.nodes.size(); i++)
|
||||
map[tree.nodes[i].parentId].append(i);
|
||||
return map;
|
||||
}
|
||||
|
||||
// ── Path breadcrumb for header comment ──
|
||||
|
||||
static QString nodePath(const NodeTree& tree, uint64_t nodeId) {
|
||||
QStringList parts;
|
||||
QSet<uint64_t> seen;
|
||||
uint64_t cur = nodeId;
|
||||
while (cur != 0 && !seen.contains(cur)) {
|
||||
seen.insert(cur);
|
||||
int idx = tree.indexOfId(cur);
|
||||
if (idx < 0) break;
|
||||
const Node& n = tree.nodes[idx];
|
||||
parts << (n.name.isEmpty() ? QStringLiteral("<unnamed>") : n.name);
|
||||
cur = n.parentId;
|
||||
}
|
||||
std::reverse(parts.begin(), parts.end());
|
||||
return parts.join(QStringLiteral(" > "));
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ── Public API ──
|
||||
|
||||
QString renderCpp(const NodeTree& tree, uint64_t rootStructId) {
|
||||
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};
|
||||
int rootSize = tree.structSpan(rootStructId, &ctx.childMap);
|
||||
QString typeName = ctx.structName(root);
|
||||
|
||||
ctx.output += QStringLiteral("// Generated by ReclassX\n");
|
||||
ctx.output += QStringLiteral("// Rendered from: %1 (id=0x%2, size=0x%3)\n\n")
|
||||
.arg(nodePath(tree, rootStructId))
|
||||
.arg(QString::number(rootStructId, 16).toUpper())
|
||||
.arg(QString::number(rootSize, 16).toUpper());
|
||||
ctx.output += QStringLiteral("#pragma once\n");
|
||||
ctx.output += QStringLiteral("#include <cstdint>\n\n");
|
||||
|
||||
emitStruct(ctx, rootStructId);
|
||||
|
||||
return ctx.output;
|
||||
}
|
||||
|
||||
QString renderCppAll(const NodeTree& tree) {
|
||||
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0};
|
||||
|
||||
ctx.output += QStringLiteral("// Generated by ReclassX\n");
|
||||
ctx.output += QStringLiteral("// Full SDK export\n\n");
|
||||
ctx.output += QStringLiteral("#pragma once\n");
|
||||
ctx.output += QStringLiteral("#include <cstdint>\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)
|
||||
emitStruct(ctx, tree.nodes[ri].id);
|
||||
}
|
||||
|
||||
return ctx.output;
|
||||
}
|
||||
|
||||
QString renderNull(const NodeTree&, uint64_t) {
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
18
src/generator.h
Normal file
18
src/generator.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include <QString>
|
||||
#include <QSet>
|
||||
|
||||
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);
|
||||
|
||||
// Generate C++ struct definitions for every root-level struct (full SDK).
|
||||
QString renderCppAll(const NodeTree& tree);
|
||||
|
||||
// Null generator placeholder (returns empty string).
|
||||
QString renderNull(const NodeTree& tree, uint64_t rootStructId);
|
||||
|
||||
} // namespace rcx
|
||||
596
src/main.cpp
596
src/main.cpp
@@ -1,4 +1,5 @@
|
||||
#include "controller.h"
|
||||
#include "generator.h"
|
||||
#include <QApplication>
|
||||
#include <QMainWindow>
|
||||
#include <QMdiArea>
|
||||
@@ -8,6 +9,8 @@
|
||||
#include <QStatusBar>
|
||||
#include <QLabel>
|
||||
#include <QSplitter>
|
||||
#include <QStackedWidget>
|
||||
#include <QPointer>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
@@ -21,6 +24,8 @@
|
||||
#include <QPainter>
|
||||
#include <QSvgRenderer>
|
||||
#include <QSettings>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
#include <Qsci/qscilexercpp.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -122,18 +127,29 @@ private slots:
|
||||
void redo();
|
||||
void about();
|
||||
void setEditorFont(const QString& fontName);
|
||||
void exportCpp();
|
||||
|
||||
private:
|
||||
enum ViewMode { VM_Reclass, VM_Rendered };
|
||||
|
||||
QMdiArea* m_mdiArea;
|
||||
QLabel* m_statusLabel;
|
||||
|
||||
struct TabState {
|
||||
RcxDocument* doc;
|
||||
RcxController* ctrl;
|
||||
QSplitter* splitter;
|
||||
RcxDocument* doc;
|
||||
RcxController* ctrl;
|
||||
QSplitter* splitter;
|
||||
QStackedWidget* stack = nullptr;
|
||||
QPointer<QsciScintilla> rendered;
|
||||
ViewMode viewMode = VM_Reclass;
|
||||
uint64_t lastRenderedRootId = 0;
|
||||
int lastRenderedFirstLine = 0;
|
||||
};
|
||||
QMap<QMdiSubWindow*, TabState> m_tabs;
|
||||
|
||||
QAction* m_actViewReclass = nullptr;
|
||||
QAction* m_actViewRendered = nullptr;
|
||||
|
||||
void createMenus();
|
||||
void createStatusBar();
|
||||
QIcon makeIcon(const QString& svgPath);
|
||||
@@ -142,6 +158,12 @@ private:
|
||||
TabState* activeTab();
|
||||
QMdiSubWindow* createTab(RcxDocument* doc);
|
||||
void updateWindowTitle();
|
||||
|
||||
void setViewMode(ViewMode mode);
|
||||
void updateRenderedView(TabState& tab);
|
||||
void syncRenderMenuState();
|
||||
uint64_t findRootStructForNode(const NodeTree& tree, uint64_t nodeId) const;
|
||||
void setupRenderedSci(QsciScintilla* sci);
|
||||
};
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
@@ -158,7 +180,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
|
||||
createStatusBar();
|
||||
|
||||
connect(m_mdiArea, &QMdiArea::subWindowActivated,
|
||||
this, [this](QMdiSubWindow*) { updateWindowTitle(); });
|
||||
this, [this](QMdiSubWindow*) {
|
||||
updateWindowTitle();
|
||||
syncRenderMenuState();
|
||||
});
|
||||
}
|
||||
|
||||
QIcon MainWindow::makeIcon(const QString& svgPath) {
|
||||
@@ -191,6 +216,8 @@ void MainWindow::createMenus() {
|
||||
file->addSeparator();
|
||||
file->addAction(makeIcon(":/vsicons/file-binary.svg"), "Load &Binary...", this, &MainWindow::loadBinary);
|
||||
file->addSeparator();
|
||||
file->addAction(makeIcon(":/vsicons/export.svg"), "Export &C++ Header...", this, &MainWindow::exportCpp);
|
||||
file->addSeparator();
|
||||
file->addAction(makeIcon(":/vsicons/close.svg"), "E&xit", QKeySequence(Qt::Key_Close), this, &QMainWindow::close);
|
||||
|
||||
// Edit
|
||||
@@ -220,6 +247,10 @@ void MainWindow::createMenus() {
|
||||
connect(actConsolas, &QAction::triggered, this, [this]() { setEditorFont("Consolas"); });
|
||||
connect(actIosevka, &QAction::triggered, this, [this]() { setEditorFont("Iosevka"); });
|
||||
|
||||
view->addSeparator();
|
||||
m_actViewRendered = view->addAction(makeIcon(":/vsicons/code.svg"), "&C/C++", this, [this]() { setViewMode(VM_Rendered); });
|
||||
m_actViewReclass = view->addAction(makeIcon(":/vsicons/eye.svg"), "&Reclass View", this, [this]() { setViewMode(VM_Reclass); });
|
||||
|
||||
// Node
|
||||
auto* node = menuBar()->addMenu("&Node");
|
||||
node->addAction(makeIcon(":/vsicons/add.svg"), "&Add Field", QKeySequence(Qt::Key_Insert), this, &MainWindow::addNode);
|
||||
@@ -240,17 +271,27 @@ void MainWindow::createStatusBar() {
|
||||
}
|
||||
|
||||
QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
// QStackedWidget wraps [0] splitter (Reclass view) and [1] rendered QsciScintilla
|
||||
auto* stack = new QStackedWidget;
|
||||
auto* splitter = new QSplitter(Qt::Horizontal);
|
||||
auto* ctrl = new RcxController(doc, splitter);
|
||||
ctrl->addSplitEditor(splitter);
|
||||
|
||||
auto* sub = m_mdiArea->addSubWindow(splitter);
|
||||
stack->addWidget(splitter); // index 0 = Reclass view
|
||||
|
||||
auto* renderedSci = new QsciScintilla;
|
||||
setupRenderedSci(renderedSci);
|
||||
stack->addWidget(renderedSci); // index 1 = Rendered view
|
||||
stack->setCurrentIndex(0);
|
||||
|
||||
auto* sub = m_mdiArea->addSubWindow(stack);
|
||||
sub->setWindowTitle(doc->filePath.isEmpty()
|
||||
? "Untitled" : QFileInfo(doc->filePath).fileName());
|
||||
sub->setAttribute(Qt::WA_DeleteOnClose);
|
||||
sub->showMaximized();
|
||||
|
||||
m_tabs[sub] = { doc, ctrl, splitter };
|
||||
m_tabs[sub] = { doc, ctrl, splitter, stack, renderedSci,
|
||||
VM_Reclass, 0, 0 };
|
||||
|
||||
connect(sub, &QObject::destroyed, this, [this, sub]() {
|
||||
auto it = m_tabs.find(sub);
|
||||
@@ -261,18 +302,29 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
});
|
||||
|
||||
connect(ctrl, &RcxController::nodeSelected,
|
||||
this, [this, ctrl](int nodeIdx) {
|
||||
this, [this, ctrl, sub](int nodeIdx) {
|
||||
if (nodeIdx >= 0 && nodeIdx < ctrl->document()->tree.nodes.size()) {
|
||||
auto& node = ctrl->document()->tree.nodes[nodeIdx];
|
||||
m_statusLabel->setText(
|
||||
QString("%1 %2 offset: 0x%3 size: %4 bytes")
|
||||
.arg(kindToString(node.kind))
|
||||
.arg(node.name)
|
||||
.arg(node.offset, 4, 16, QChar('0'))
|
||||
.arg(node.byteSize()));
|
||||
auto it = m_tabs.find(sub);
|
||||
if (it != m_tabs.end() && it->viewMode == VM_Rendered)
|
||||
m_statusLabel->setText(
|
||||
QString("Rendered: %1 %2")
|
||||
.arg(kindToString(node.kind))
|
||||
.arg(node.name));
|
||||
else
|
||||
m_statusLabel->setText(
|
||||
QString("%1 %2 offset: 0x%3 size: %4 bytes")
|
||||
.arg(kindToString(node.kind))
|
||||
.arg(node.name)
|
||||
.arg(node.offset, 4, 16, QChar('0'))
|
||||
.arg(node.byteSize()));
|
||||
} else {
|
||||
m_statusLabel->setText("Ready");
|
||||
}
|
||||
// Update rendered view on selection change
|
||||
auto it = m_tabs.find(sub);
|
||||
if (it != m_tabs.end())
|
||||
updateRenderedView(*it);
|
||||
});
|
||||
connect(ctrl, &RcxController::selectionChanged,
|
||||
this, [this](int count) {
|
||||
@@ -282,6 +334,26 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
m_statusLabel->setText(QString("%1 nodes selected").arg(count));
|
||||
});
|
||||
|
||||
// Update rendered view on document changes and undo/redo
|
||||
connect(doc, &RcxDocument::documentChanged,
|
||||
this, [this, sub]() {
|
||||
auto it = m_tabs.find(sub);
|
||||
if (it != m_tabs.end())
|
||||
QTimer::singleShot(0, this, [this, sub]() {
|
||||
auto it2 = m_tabs.find(sub);
|
||||
if (it2 != m_tabs.end()) updateRenderedView(*it2);
|
||||
});
|
||||
});
|
||||
connect(&doc->undoStack, &QUndoStack::indexChanged,
|
||||
this, [this, sub](int) {
|
||||
auto it = m_tabs.find(sub);
|
||||
if (it != m_tabs.end())
|
||||
QTimer::singleShot(0, this, [this, sub]() {
|
||||
auto it2 = m_tabs.find(sub);
|
||||
if (it2 != m_tabs.end()) updateRenderedView(*it2);
|
||||
});
|
||||
});
|
||||
|
||||
ctrl->refresh();
|
||||
return sub;
|
||||
}
|
||||
@@ -289,314 +361,23 @@ QMdiSubWindow* MainWindow::createTab(RcxDocument* doc) {
|
||||
void MainWindow::newFile() {
|
||||
auto* doc = new RcxDocument(this);
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// _PEB64 Demo — Process Environment Block + stub structs
|
||||
// Buffer covers PEB (0x7D0) + _PEB_LDR_DATA (0x800) + _RTL_USER_PROCESS_PARAMETERS (0x900)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
QByteArray data(16, '\0');
|
||||
doc->loadData(data);
|
||||
doc->tree.baseAddress = 0x00400000;
|
||||
|
||||
QByteArray pebData(0x940, '\0');
|
||||
char* d = pebData.data();
|
||||
Node root;
|
||||
root.kind = NodeKind::Struct;
|
||||
root.name = "Entity";
|
||||
root.structTypeName = "Entity";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = doc->tree.addNode(root);
|
||||
uint64_t rootId = doc->tree.nodes[ri].id;
|
||||
|
||||
auto w8 = [&](int off, uint8_t v) { d[off] = (char)v; };
|
||||
auto w16 = [&](int off, uint16_t v) { memcpy(d+off, &v, 2); };
|
||||
auto w32 = [&](int off, uint32_t v) { memcpy(d+off, &v, 4); };
|
||||
auto w64 = [&](int off, uint64_t v) { memcpy(d+off, &v, 8); };
|
||||
|
||||
w8 (0x002, 1); // BeingDebugged
|
||||
w8 (0x003, 0x04); // BitField
|
||||
w64(0x008, 0xFFFFFFFFFFFFFFFFULL); // Mutant (-1)
|
||||
w64(0x010, 0x00007FF6DE120000ULL); // ImageBaseAddress
|
||||
w64(0x018, 0x000000D87B5E5800ULL); // Ldr (baseAddress + 0x800)
|
||||
w64(0x020, 0x000000D87B5E5900ULL); // ProcessParameters (baseAddress + 0x900)
|
||||
w64(0x030, 0x000001A4C3D40000ULL); // ProcessHeap
|
||||
w64(0x038, 0x00007FFE3B8D4260ULL); // FastPebLock
|
||||
w32(0x050, 0x01); // CrossProcessFlags
|
||||
w64(0x058, 0x00007FFE3B720000ULL); // KernelCallbackTable
|
||||
w64(0x068, 0x00007FFE3E570000ULL); // ApiSetMap
|
||||
w64(0x078, 0x00007FFE3B8D3F50ULL); // TlsBitmap
|
||||
w32(0x080, 0x00000003); // TlsBitmapBits[0]
|
||||
w64(0x088, 0x00007FFE38800000ULL); // ReadOnlySharedMemoryBase
|
||||
w64(0x090, 0x00007FFE38820000ULL); // SharedData
|
||||
w64(0x0A0, 0x00007FFE3B8D1000ULL); // AnsiCodePageData
|
||||
w64(0x0A8, 0x00007FFE3B8D2040ULL); // OemCodePageData
|
||||
w64(0x0B0, 0x00007FFE3B8CE020ULL); // UnicodeCaseTableData
|
||||
w32(0x0B8, 8); // NumberOfProcessors
|
||||
w32(0x0BC, 0x70); // NtGlobalFlag
|
||||
w64(0x0C0, 0xFFFFFFFF7C91E000ULL); // CriticalSectionTimeout
|
||||
w64(0x0C8, 0x0000000000100000ULL); // HeapSegmentReserve
|
||||
w64(0x0D0, 0x0000000000002000ULL); // HeapSegmentCommit
|
||||
w32(0x0E8, 4); // NumberOfHeaps
|
||||
w32(0x0EC, 16); // MaximumNumberOfHeaps
|
||||
w64(0x0F0, 0x000001A4C3D40688ULL); // ProcessHeaps
|
||||
w64(0x0F8, 0x00007FFE388B0000ULL); // GdiSharedHandleTable
|
||||
w64(0x110, 0x00007FFE3B8D42E8ULL); // LoaderLock
|
||||
w32(0x118, 10); // OSMajorVersion
|
||||
w16(0x120, 19045); // OSBuildNumber
|
||||
w32(0x124, 2); // OSPlatformId
|
||||
w32(0x128, 3); // ImageSubsystem (CUI)
|
||||
w32(0x12C, 10); // ImageSubsystemMajorVersion
|
||||
w64(0x138, 0x00000000000000FFULL); // ActiveProcessAffinityMask
|
||||
w64(0x238, 0x00007FFE3B8D3F70ULL); // TlsExpansionBitmap
|
||||
w32(0x2C0, 1); // SessionId
|
||||
w64(0x2F8, 0x000001A4C3E21000ULL); // ActivationContextData
|
||||
w64(0x308, 0x00007FFE38840000ULL); // SystemDefaultActivationContextData
|
||||
w64(0x318, 0x0000000000002000ULL); // MinimumStackCommit
|
||||
w16(0x34C, 1252); // ActiveCodePage
|
||||
w16(0x34E, 437); // OemCodePage
|
||||
w64(0x358, 0x000001A4C3E30000ULL); // WerRegistrationData
|
||||
w64(0x380, 0x00007FFE38890000ULL); // CsrServerReadOnlySharedMemoryBase
|
||||
w64(0x390, 0x000000D87B5E5390ULL); // TppWorkerpList.Flink (self)
|
||||
w64(0x398, 0x000000D87B5E5390ULL); // TppWorkerpList.Blink (self)
|
||||
w64(0x7B8, 0x00007FFE38860000ULL); // LeapSecondData
|
||||
|
||||
// ── _PEB_LDR_DATA at offset 0x800 ──
|
||||
w32(0x800, 0x48); // Length
|
||||
w8 (0x804, 0x01); // Initialized
|
||||
w64(0x808, 0x0000000000000000ULL); // SsHandle
|
||||
w64(0x810, 0x000001A4C3D40100ULL); // InLoadOrderModuleList.Flink
|
||||
w64(0x818, 0x000001A4C3D40200ULL); // InLoadOrderModuleList.Blink
|
||||
w64(0x820, 0x000001A4C3D40110ULL); // InMemoryOrderModuleList.Flink
|
||||
w64(0x828, 0x000001A4C3D40210ULL); // InMemoryOrderModuleList.Blink
|
||||
|
||||
// ── _RTL_USER_PROCESS_PARAMETERS at offset 0x900 ──
|
||||
w32(0x900, 0x07B0); // MaximumLength
|
||||
w32(0x904, 0x07B0); // Length
|
||||
w32(0x908, 0x0001); // Flags (NORMALIZED)
|
||||
w32(0x90C, 0x0000); // DebugFlags
|
||||
w64(0x910, 0x0000000000000044ULL); // ConsoleHandle
|
||||
w32(0x918, 0x0000); // ConsoleFlags
|
||||
w64(0x920, 0x0000000000000008ULL); // StandardInput
|
||||
w64(0x928, 0x000000000000000CULL); // StandardOutput
|
||||
w64(0x930, 0x0000000000000010ULL); // StandardError
|
||||
|
||||
doc->loadData(pebData);
|
||||
doc->tree.baseAddress = 0x000000D87B5E5000ULL;
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// Build _PEB64 Node Tree (0x7D0 bytes, unions mapped to first member)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
auto addField = [&](uint64_t parent, int offset, NodeKind kind, const QString& name) -> uint64_t {
|
||||
Node n; n.kind = kind; n.name = name;
|
||||
n.parentId = parent; n.offset = offset;
|
||||
int idx = doc->tree.addNode(n);
|
||||
return doc->tree.nodes[idx].id;
|
||||
};
|
||||
auto addPad = [&](uint64_t parent, int offset, int len, const QString& name) {
|
||||
Node n; n.kind = NodeKind::Padding; n.name = name;
|
||||
n.parentId = parent; n.offset = offset; n.arrayLen = len;
|
||||
doc->tree.addNode(n);
|
||||
};
|
||||
auto addStruct = [&](uint64_t parent, int offset, const QString& typeName, const QString& name, bool collapse = true) -> uint64_t {
|
||||
Node n; n.kind = NodeKind::Struct;
|
||||
n.structTypeName = typeName; n.name = name;
|
||||
n.parentId = parent; n.offset = offset; n.collapsed = collapse;
|
||||
int idx = doc->tree.addNode(n);
|
||||
return doc->tree.nodes[idx].id;
|
||||
};
|
||||
auto addArray = [&](uint64_t parent, int offset, const QString& name, int count, NodeKind elemKind) {
|
||||
Node n; n.kind = NodeKind::Array; n.name = name;
|
||||
n.parentId = parent; n.offset = offset;
|
||||
n.arrayLen = count; n.elementKind = elemKind;
|
||||
n.collapsed = true;
|
||||
int idx = doc->tree.addNode(n);
|
||||
uint64_t arrId = doc->tree.nodes[idx].id;
|
||||
int elemSz = sizeForKind(elemKind);
|
||||
if (elemSz > 0) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
Node e; e.kind = elemKind;
|
||||
e.name = QStringLiteral("[%1]").arg(i);
|
||||
e.parentId = arrId; e.offset = i * elemSz;
|
||||
doc->tree.addNode(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Root struct (not collapsed so fields are visible on open)
|
||||
uint64_t peb = addStruct(0, 0, "_PEB64", "Peb", false);
|
||||
|
||||
// 0x000 – 0x007
|
||||
addField(peb, 0x000, NodeKind::UInt8, "InheritedAddressSpace");
|
||||
addField(peb, 0x001, NodeKind::UInt8, "ReadImageFileExecOptions");
|
||||
addField(peb, 0x002, NodeKind::UInt8, "BeingDebugged");
|
||||
addField(peb, 0x003, NodeKind::UInt8, "BitField");
|
||||
addPad (peb, 0x004, 4, "Padding0");
|
||||
|
||||
// 0x008 – 0x04F
|
||||
addField(peb, 0x008, NodeKind::Pointer64, "Mutant");
|
||||
addField(peb, 0x010, NodeKind::Pointer64, "ImageBaseAddress");
|
||||
uint64_t ldrPtrId = addField(peb, 0x018, NodeKind::Pointer64, "Ldr");
|
||||
uint64_t ppPtrId = addField(peb, 0x020, NodeKind::Pointer64, "ProcessParameters");
|
||||
addField(peb, 0x028, NodeKind::Pointer64, "SubSystemData");
|
||||
addField(peb, 0x030, NodeKind::Pointer64, "ProcessHeap");
|
||||
addField(peb, 0x038, NodeKind::Pointer64, "FastPebLock");
|
||||
addField(peb, 0x040, NodeKind::Pointer64, "AtlThunkSListPtr");
|
||||
addField(peb, 0x048, NodeKind::Pointer64, "IFEOKey");
|
||||
|
||||
// 0x050 – 0x07F
|
||||
addField(peb, 0x050, NodeKind::UInt32, "CrossProcessFlags");
|
||||
addPad (peb, 0x054, 4, "Padding1");
|
||||
addField(peb, 0x058, NodeKind::Pointer64, "KernelCallbackTable");
|
||||
addField(peb, 0x060, NodeKind::UInt32, "SystemReserved");
|
||||
addField(peb, 0x064, NodeKind::UInt32, "AtlThunkSListPtr32");
|
||||
addField(peb, 0x068, NodeKind::Pointer64, "ApiSetMap");
|
||||
addField(peb, 0x070, NodeKind::UInt32, "TlsExpansionCounter");
|
||||
addPad (peb, 0x074, 4, "Padding2");
|
||||
addField(peb, 0x078, NodeKind::Pointer64, "TlsBitmap");
|
||||
addArray(peb, 0x080, "TlsBitmapBits", 2, NodeKind::UInt32);
|
||||
|
||||
// 0x088 – 0x0BF
|
||||
addField(peb, 0x088, NodeKind::Pointer64, "ReadOnlySharedMemoryBase");
|
||||
addField(peb, 0x090, NodeKind::Pointer64, "SharedData");
|
||||
addField(peb, 0x098, NodeKind::Pointer64, "ReadOnlyStaticServerData");
|
||||
addField(peb, 0x0A0, NodeKind::Pointer64, "AnsiCodePageData");
|
||||
addField(peb, 0x0A8, NodeKind::Pointer64, "OemCodePageData");
|
||||
addField(peb, 0x0B0, NodeKind::Pointer64, "UnicodeCaseTableData");
|
||||
addField(peb, 0x0B8, NodeKind::UInt32, "NumberOfProcessors");
|
||||
addField(peb, 0x0BC, NodeKind::Hex32, "NtGlobalFlag");
|
||||
|
||||
// 0x0C0 – 0x0EF
|
||||
addField(peb, 0x0C0, NodeKind::UInt64, "CriticalSectionTimeout");
|
||||
addField(peb, 0x0C8, NodeKind::UInt64, "HeapSegmentReserve");
|
||||
addField(peb, 0x0D0, NodeKind::UInt64, "HeapSegmentCommit");
|
||||
addField(peb, 0x0D8, NodeKind::UInt64, "HeapDeCommitTotalFreeThreshold");
|
||||
addField(peb, 0x0E0, NodeKind::UInt64, "HeapDeCommitFreeBlockThreshold");
|
||||
addField(peb, 0x0E8, NodeKind::UInt32, "NumberOfHeaps");
|
||||
addField(peb, 0x0EC, NodeKind::UInt32, "MaximumNumberOfHeaps");
|
||||
|
||||
// 0x0F0 – 0x13F
|
||||
addField(peb, 0x0F0, NodeKind::Pointer64, "ProcessHeaps");
|
||||
addField(peb, 0x0F8, NodeKind::Pointer64, "GdiSharedHandleTable");
|
||||
addField(peb, 0x100, NodeKind::Pointer64, "ProcessStarterHelper");
|
||||
addField(peb, 0x108, NodeKind::UInt32, "GdiDCAttributeList");
|
||||
addPad (peb, 0x10C, 4, "Padding3");
|
||||
addField(peb, 0x110, NodeKind::Pointer64, "LoaderLock");
|
||||
addField(peb, 0x118, NodeKind::UInt32, "OSMajorVersion");
|
||||
addField(peb, 0x11C, NodeKind::UInt32, "OSMinorVersion");
|
||||
addField(peb, 0x120, NodeKind::UInt16, "OSBuildNumber");
|
||||
addField(peb, 0x122, NodeKind::UInt16, "OSCSDVersion");
|
||||
addField(peb, 0x124, NodeKind::UInt32, "OSPlatformId");
|
||||
addField(peb, 0x128, NodeKind::UInt32, "ImageSubsystem");
|
||||
addField(peb, 0x12C, NodeKind::UInt32, "ImageSubsystemMajorVersion");
|
||||
addField(peb, 0x130, NodeKind::UInt32, "ImageSubsystemMinorVersion");
|
||||
addPad (peb, 0x134, 4, "Padding4");
|
||||
addField(peb, 0x138, NodeKind::UInt64, "ActiveProcessAffinityMask");
|
||||
|
||||
// 0x140 – 0x22F
|
||||
addArray(peb, 0x140, "GdiHandleBuffer", 60, NodeKind::UInt32);
|
||||
|
||||
// 0x230 – 0x2BF
|
||||
addField(peb, 0x230, NodeKind::Pointer64, "PostProcessInitRoutine");
|
||||
addField(peb, 0x238, NodeKind::Pointer64, "TlsExpansionBitmap");
|
||||
addArray(peb, 0x240, "TlsExpansionBitmapBits", 32, NodeKind::UInt32);
|
||||
|
||||
// 0x2C0 – 0x2E7
|
||||
addField(peb, 0x2C0, NodeKind::UInt32, "SessionId");
|
||||
addPad (peb, 0x2C4, 4, "Padding5");
|
||||
addField(peb, 0x2C8, NodeKind::UInt64, "AppCompatFlags");
|
||||
addField(peb, 0x2D0, NodeKind::UInt64, "AppCompatFlagsUser");
|
||||
addField(peb, 0x2D8, NodeKind::Pointer64, "pShimData");
|
||||
addField(peb, 0x2E0, NodeKind::Pointer64, "AppCompatInfo");
|
||||
|
||||
// 0x2E8 – 0x2F7: _STRING64 CSDVersion
|
||||
{
|
||||
uint64_t sid = addStruct(peb, 0x2E8, "_STRING64", "CSDVersion");
|
||||
addField(sid, 0, NodeKind::UInt16, "Length");
|
||||
addField(sid, 2, NodeKind::UInt16, "MaximumLength");
|
||||
addPad (sid, 4, 4, "Pad");
|
||||
addField(sid, 8, NodeKind::Pointer64, "Buffer");
|
||||
}
|
||||
|
||||
// 0x2F8 – 0x31F
|
||||
addField(peb, 0x2F8, NodeKind::Pointer64, "ActivationContextData");
|
||||
addField(peb, 0x300, NodeKind::Pointer64, "ProcessAssemblyStorageMap");
|
||||
addField(peb, 0x308, NodeKind::Pointer64, "SystemDefaultActivationContextData");
|
||||
addField(peb, 0x310, NodeKind::Pointer64, "SystemAssemblyStorageMap");
|
||||
addField(peb, 0x318, NodeKind::UInt64, "MinimumStackCommit");
|
||||
|
||||
// 0x320 – 0x34B
|
||||
addArray(peb, 0x320, "SparePointers", 2, NodeKind::UInt64);
|
||||
addField(peb, 0x330, NodeKind::Pointer64, "PatchLoaderData");
|
||||
addField(peb, 0x338, NodeKind::Pointer64, "ChpeV2ProcessInfo");
|
||||
addField(peb, 0x340, NodeKind::UInt32, "AppModelFeatureState");
|
||||
addArray(peb, 0x344, "SpareUlongs", 2, NodeKind::UInt32);
|
||||
addField(peb, 0x34C, NodeKind::UInt16, "ActiveCodePage");
|
||||
addField(peb, 0x34E, NodeKind::UInt16, "OemCodePage");
|
||||
addField(peb, 0x350, NodeKind::UInt16, "UseCaseMapping");
|
||||
addField(peb, 0x352, NodeKind::UInt16, "UnusedNlsField");
|
||||
|
||||
// 0x354 – 0x37F
|
||||
addPad (peb, 0x354, 4, "Pad354");
|
||||
addField(peb, 0x358, NodeKind::Pointer64, "WerRegistrationData");
|
||||
addField(peb, 0x360, NodeKind::Pointer64, "WerShipAssertPtr");
|
||||
addField(peb, 0x368, NodeKind::Pointer64, "EcCodeBitMap");
|
||||
addField(peb, 0x370, NodeKind::Pointer64, "pImageHeaderHash");
|
||||
addField(peb, 0x378, NodeKind::UInt32, "TracingFlags");
|
||||
addPad (peb, 0x37C, 4, "Padding6");
|
||||
|
||||
// 0x380 – 0x39F
|
||||
addField(peb, 0x380, NodeKind::Pointer64, "CsrServerReadOnlySharedMemoryBase");
|
||||
addField(peb, 0x388, NodeKind::UInt64, "TppWorkerpListLock");
|
||||
|
||||
// LIST_ENTRY64 TppWorkerpList
|
||||
{
|
||||
uint64_t sid = addStruct(peb, 0x390, "LIST_ENTRY64", "TppWorkerpList");
|
||||
addField(sid, 0, NodeKind::Pointer64, "Flink");
|
||||
addField(sid, 8, NodeKind::Pointer64, "Blink");
|
||||
}
|
||||
|
||||
// 0x3A0 – 0x79F
|
||||
addArray(peb, 0x3A0, "WaitOnAddressHashTable", 128, NodeKind::UInt64);
|
||||
|
||||
// 0x7A0 – 0x7CF
|
||||
addField(peb, 0x7A0, NodeKind::Pointer64, "TelemetryCoverageHeader");
|
||||
addField(peb, 0x7A8, NodeKind::UInt32, "CloudFileFlags");
|
||||
addField(peb, 0x7AC, NodeKind::UInt32, "CloudFileDiagFlags");
|
||||
addField(peb, 0x7B0, NodeKind::Int8, "PlaceholderCompatibilityMode");
|
||||
addArray(peb, 0x7B1, "PlaceholderCompatibilityModeReserved", 7, NodeKind::Int8);
|
||||
addField(peb, 0x7B8, NodeKind::Pointer64, "LeapSecondData");
|
||||
addField(peb, 0x7C0, NodeKind::UInt32, "LeapSecondFlags");
|
||||
addField(peb, 0x7C4, NodeKind::UInt32, "NtGlobalFlag2");
|
||||
addField(peb, 0x7C8, NodeKind::UInt64, "ExtendedFeatureDisableMask");
|
||||
|
||||
// ── Stub structs for pointer deref demo ──
|
||||
// _PEB_LDR_DATA (Ldr target)
|
||||
uint64_t ldrData = addStruct(0, 0x800, "_PEB_LDR_DATA", "LdrData");
|
||||
addField(ldrData, 0x00, NodeKind::UInt32, "Length");
|
||||
addField(ldrData, 0x04, NodeKind::UInt8, "Initialized");
|
||||
addPad (ldrData, 0x05, 3, "Pad");
|
||||
addField(ldrData, 0x08, NodeKind::Pointer64, "SsHandle");
|
||||
{
|
||||
uint64_t le = addStruct(ldrData, 0x10, "LIST_ENTRY64", "InLoadOrderModuleList");
|
||||
addField(le, 0, NodeKind::Pointer64, "Flink");
|
||||
addField(le, 8, NodeKind::Pointer64, "Blink");
|
||||
}
|
||||
{
|
||||
uint64_t le = addStruct(ldrData, 0x20, "LIST_ENTRY64", "InMemoryOrderModuleList");
|
||||
addField(le, 0, NodeKind::Pointer64, "Flink");
|
||||
addField(le, 8, NodeKind::Pointer64, "Blink");
|
||||
}
|
||||
|
||||
// _RTL_USER_PROCESS_PARAMETERS (ProcessParameters target)
|
||||
uint64_t procParams = addStruct(0, 0x900, "_RTL_USER_PROCESS_PARAMETERS", "ProcessParams");
|
||||
addField(procParams, 0x00, NodeKind::UInt32, "MaximumLength");
|
||||
addField(procParams, 0x04, NodeKind::UInt32, "Length");
|
||||
addField(procParams, 0x08, NodeKind::UInt32, "Flags");
|
||||
addField(procParams, 0x0C, NodeKind::UInt32, "DebugFlags");
|
||||
addField(procParams, 0x10, NodeKind::Pointer64, "ConsoleHandle");
|
||||
addField(procParams, 0x18, NodeKind::UInt32, "ConsoleFlags");
|
||||
addPad (procParams, 0x1C, 4, "Pad");
|
||||
addField(procParams, 0x20, NodeKind::Pointer64, "StandardInput");
|
||||
addField(procParams, 0x28, NodeKind::Pointer64, "StandardOutput");
|
||||
addField(procParams, 0x30, NodeKind::Pointer64, "StandardError");
|
||||
|
||||
// Wire up pointer refIds
|
||||
{
|
||||
int li = doc->tree.indexOfId(ldrPtrId);
|
||||
if (li >= 0) doc->tree.nodes[li].refId = ldrData;
|
||||
int pi = doc->tree.indexOfId(ppPtrId);
|
||||
if (pi >= 0) doc->tree.nodes[pi].refId = procParams;
|
||||
}
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "health"; n.parentId = rootId; n.offset = 0; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Int32; n.name = "armor"; n.parentId = rootId; n.offset = 4; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Float; n.name = "speed"; n.parentId = rootId; n.offset = 8; doc->tree.addNode(n); }
|
||||
{ Node n; n.kind = NodeKind::Hex32; n.name = "flags"; n.parentId = rootId; n.offset = 12; doc->tree.addNode(n); }
|
||||
|
||||
createTab(doc);
|
||||
}
|
||||
@@ -735,9 +516,20 @@ void MainWindow::about() {
|
||||
void MainWindow::setEditorFont(const QString& fontName) {
|
||||
QSettings settings("ReclassX", "ReclassX");
|
||||
settings.setValue("font", fontName);
|
||||
// Notify all controllers to refresh fonts
|
||||
for (auto& state : m_tabs) {
|
||||
state.ctrl->setEditorFont(fontName);
|
||||
// Also update the rendered view font
|
||||
if (state.rendered) {
|
||||
QFont f(fontName, 12);
|
||||
f.setFixedPitch(true);
|
||||
state.rendered->setFont(f);
|
||||
if (auto* lex = state.rendered->lexer()) {
|
||||
lex->setFont(f);
|
||||
for (int i = 0; i <= 127; i++)
|
||||
lex->setFont(f, i);
|
||||
}
|
||||
state.rendered->setMarginsFont(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,6 +560,170 @@ void MainWindow::updateWindowTitle() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Rendered view setup ──
|
||||
|
||||
void MainWindow::setupRenderedSci(QsciScintilla* sci) {
|
||||
QSettings settings("ReclassX", "ReclassX");
|
||||
QString fontName = settings.value("font", "Consolas").toString();
|
||||
QFont f(fontName, 12);
|
||||
f.setFixedPitch(true);
|
||||
|
||||
sci->setFont(f);
|
||||
sci->setReadOnly(true);
|
||||
sci->setWrapMode(QsciScintilla::WrapNone);
|
||||
sci->setCaretLineVisible(false);
|
||||
sci->setPaper(QColor("#1e1e1e"));
|
||||
sci->setColor(QColor("#d4d4d4"));
|
||||
sci->setTabWidth(4);
|
||||
sci->setIndentationsUseTabs(false);
|
||||
sci->setCaretForegroundColor(QColor("#d4d4d4"));
|
||||
sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRAASCENT, (long)2);
|
||||
sci->SendScintilla(QsciScintillaBase::SCI_SETEXTRADESCENT, (long)2);
|
||||
|
||||
// Line number margin
|
||||
sci->setMarginType(0, QsciScintilla::NumberMargin);
|
||||
sci->setMarginWidth(0, "00000");
|
||||
sci->setMarginsBackgroundColor(QColor("#252526"));
|
||||
sci->setMarginsForegroundColor(QColor("#858585"));
|
||||
sci->setMarginsFont(f);
|
||||
|
||||
// Hide other margins
|
||||
sci->setMarginWidth(1, 0);
|
||||
sci->setMarginWidth(2, 0);
|
||||
|
||||
// C++ lexer for syntax highlighting
|
||||
auto* lexer = new QsciLexerCPP(sci);
|
||||
lexer->setFont(f);
|
||||
lexer->setColor(QColor("#569cd6"), QsciLexerCPP::Keyword);
|
||||
lexer->setColor(QColor("#569cd6"), QsciLexerCPP::KeywordSet2);
|
||||
lexer->setColor(QColor("#b5cea8"), QsciLexerCPP::Number);
|
||||
lexer->setColor(QColor("#ce9178"), QsciLexerCPP::DoubleQuotedString);
|
||||
lexer->setColor(QColor("#ce9178"), QsciLexerCPP::SingleQuotedString);
|
||||
lexer->setColor(QColor("#6a9955"), QsciLexerCPP::Comment);
|
||||
lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentLine);
|
||||
lexer->setColor(QColor("#6a9955"), QsciLexerCPP::CommentDoc);
|
||||
lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Default);
|
||||
lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Identifier);
|
||||
lexer->setColor(QColor("#c586c0"), QsciLexerCPP::PreProcessor);
|
||||
lexer->setColor(QColor("#d4d4d4"), QsciLexerCPP::Operator);
|
||||
for (int i = 0; i <= 127; i++) {
|
||||
lexer->setPaper(QColor("#1e1e1e"), i);
|
||||
lexer->setFont(f, i);
|
||||
}
|
||||
sci->setLexer(lexer);
|
||||
sci->setBraceMatching(QsciScintilla::NoBraceMatch);
|
||||
}
|
||||
|
||||
// ── View mode / generator switching ──
|
||||
|
||||
void MainWindow::setViewMode(ViewMode mode) {
|
||||
auto* tab = activeTab();
|
||||
if (!tab) return;
|
||||
tab->viewMode = mode;
|
||||
if (tab->stack) {
|
||||
tab->stack->setCurrentIndex(mode == VM_Rendered ? 1 : 0);
|
||||
}
|
||||
if (mode == VM_Rendered) {
|
||||
updateRenderedView(*tab);
|
||||
}
|
||||
syncRenderMenuState();
|
||||
}
|
||||
|
||||
void MainWindow::syncRenderMenuState() {
|
||||
auto* tab = activeTab();
|
||||
bool rendered = tab && tab->viewMode == VM_Rendered;
|
||||
if (m_actViewRendered) m_actViewRendered->setEnabled(!rendered);
|
||||
if (m_actViewReclass) m_actViewReclass->setEnabled(rendered);
|
||||
}
|
||||
|
||||
// ── Find the root-level struct ancestor for a node ──
|
||||
|
||||
uint64_t MainWindow::findRootStructForNode(const NodeTree& tree, uint64_t nodeId) const {
|
||||
QSet<uint64_t> visited;
|
||||
uint64_t cur = nodeId;
|
||||
uint64_t lastStruct = 0;
|
||||
while (cur != 0 && !visited.contains(cur)) {
|
||||
visited.insert(cur);
|
||||
int idx = tree.indexOfId(cur);
|
||||
if (idx < 0) break;
|
||||
const Node& n = tree.nodes[idx];
|
||||
if (n.kind == NodeKind::Struct)
|
||||
lastStruct = n.id;
|
||||
if (n.parentId == 0)
|
||||
return (n.kind == NodeKind::Struct) ? n.id : lastStruct;
|
||||
cur = n.parentId;
|
||||
}
|
||||
return lastStruct;
|
||||
}
|
||||
|
||||
// ── Update the rendered view for a tab ──
|
||||
|
||||
void MainWindow::updateRenderedView(TabState& tab) {
|
||||
if (tab.viewMode != VM_Rendered) return;
|
||||
if (!tab.rendered) return;
|
||||
|
||||
// Determine which struct to render based on selection
|
||||
uint64_t rootId = 0;
|
||||
QSet<uint64_t> selIds = tab.ctrl->selectedIds();
|
||||
if (selIds.size() >= 1) {
|
||||
uint64_t selId = *selIds.begin();
|
||||
selId &= ~kFooterIdBit;
|
||||
rootId = findRootStructForNode(tab.doc->tree, selId);
|
||||
}
|
||||
|
||||
// Generate text
|
||||
QString text;
|
||||
if (rootId != 0)
|
||||
text = renderCpp(tab.doc->tree, rootId);
|
||||
else
|
||||
text = renderCppAll(tab.doc->tree);
|
||||
|
||||
// Scroll restoration: save if same root, reset if different
|
||||
int restoreLine = 0;
|
||||
if (rootId != 0 && rootId == tab.lastRenderedRootId) {
|
||||
restoreLine = (int)tab.rendered->SendScintilla(
|
||||
QsciScintillaBase::SCI_GETFIRSTVISIBLELINE);
|
||||
}
|
||||
tab.lastRenderedRootId = rootId;
|
||||
|
||||
// Set text
|
||||
tab.rendered->setReadOnly(false);
|
||||
tab.rendered->setText(text);
|
||||
tab.rendered->setReadOnly(true);
|
||||
|
||||
// Update margin width for line count
|
||||
int lineCount = tab.rendered->lines();
|
||||
QString marginStr = QString(QString::number(lineCount).size() + 2, '0');
|
||||
tab.rendered->setMarginWidth(0, marginStr);
|
||||
|
||||
// Restore scroll
|
||||
if (restoreLine > 0) {
|
||||
tab.rendered->SendScintilla(QsciScintillaBase::SCI_SETFIRSTVISIBLELINE,
|
||||
(unsigned long)restoreLine);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Export C++ header to file ──
|
||||
|
||||
void MainWindow::exportCpp() {
|
||||
auto* tab = activeTab();
|
||||
if (!tab) return;
|
||||
|
||||
QString path = QFileDialog::getSaveFileName(this,
|
||||
"Export C++ Header", {}, "C++ Header (*.h);;All Files (*)");
|
||||
if (path.isEmpty()) return;
|
||||
|
||||
QString text = renderCppAll(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());
|
||||
m_statusLabel->setText("Exported to " + QFileInfo(path).fileName());
|
||||
}
|
||||
|
||||
} // namespace rcx
|
||||
|
||||
// ── Entry point ──
|
||||
|
||||
@@ -27,5 +27,9 @@
|
||||
<file alias="files.svg">vsicons/files.svg</file>
|
||||
<file alias="extensions.svg">vsicons/extensions.svg</file>
|
||||
<file alias="question.svg">vsicons/question.svg</file>
|
||||
<file alias="eye.svg">vsicons/eye.svg</file>
|
||||
<file alias="code.svg">vsicons/code.svg</file>
|
||||
<file alias="export.svg">vsicons/export.svg</file>
|
||||
<file alias="preview.svg">vsicons/preview.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
671
tests/test_generator.cpp
Normal file
671
tests/test_generator.cpp
Normal file
@@ -0,0 +1,671 @@
|
||||
#include <QtTest/QTest>
|
||||
#include <QFile>
|
||||
#include <QTemporaryFile>
|
||||
#include "core.h"
|
||||
#include "generator.h"
|
||||
|
||||
class TestGenerator : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
// Helper: build a simple struct with a few fields
|
||||
rcx::NodeTree makeSimpleStruct() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Player";
|
||||
root.structTypeName = "Player";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node f1;
|
||||
f1.kind = rcx::NodeKind::Int32;
|
||||
f1.name = "health";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
rcx::Node f2;
|
||||
f2.kind = rcx::NodeKind::Float;
|
||||
f2.name = "speed";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 4;
|
||||
tree.addNode(f2);
|
||||
|
||||
rcx::Node f3;
|
||||
f3.kind = rcx::NodeKind::UInt64;
|
||||
f3.name = "id";
|
||||
f3.parentId = rootId;
|
||||
f3.offset = 8;
|
||||
tree.addNode(f3);
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
private slots:
|
||||
|
||||
// ── Basic struct generation ──
|
||||
|
||||
void testSimpleStruct() {
|
||||
auto tree = makeSimpleStruct();
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
// Header
|
||||
QVERIFY(result.contains("Generated by ReclassX"));
|
||||
QVERIFY(result.contains("#pragma once"));
|
||||
QVERIFY(result.contains("#include <cstdint>"));
|
||||
|
||||
// Struct definition
|
||||
QVERIFY(result.contains("#pragma pack(push, 1)"));
|
||||
QVERIFY(result.contains("struct Player {"));
|
||||
QVERIFY(result.contains("int32_t health;"));
|
||||
QVERIFY(result.contains("float speed;"));
|
||||
QVERIFY(result.contains("uint64_t id;"));
|
||||
QVERIFY(result.contains("};"));
|
||||
QVERIFY(result.contains("#pragma pack(pop)"));
|
||||
|
||||
// static_assert - struct is 16 bytes (0+4 + 4+4 + 8+8 = 16)
|
||||
QVERIFY(result.contains("static_assert(sizeof(Player) == 0x10"));
|
||||
}
|
||||
|
||||
// ── Padding gap detection ──
|
||||
|
||||
void testPaddingGaps() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "GappyStruct";
|
||||
root.structTypeName = "GappyStruct";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Field at offset 0, size 4
|
||||
rcx::Node f1;
|
||||
f1.kind = rcx::NodeKind::UInt32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
// Field at offset 8, size 4 (gap of 4 bytes at offset 4)
|
||||
rcx::Node f2;
|
||||
f2.kind = rcx::NodeKind::UInt32;
|
||||
f2.name = "b";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 8;
|
||||
tree.addNode(f2);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
// Should contain a padding field between a and b
|
||||
QVERIFY(result.contains("uint8_t _pad"));
|
||||
QVERIFY(result.contains("[0x4]"));
|
||||
QVERIFY(result.contains("uint32_t a;"));
|
||||
QVERIFY(result.contains("uint32_t b;"));
|
||||
}
|
||||
|
||||
// ── Tail padding ──
|
||||
|
||||
void testTailPadding() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "TailPad";
|
||||
root.structTypeName = "TailPad";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Only field at offset 0, size 1
|
||||
rcx::Node f1;
|
||||
f1.kind = rcx::NodeKind::UInt8;
|
||||
f1.name = "flag";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
// Add another field at offset 16 to make struct bigger
|
||||
rcx::Node f2;
|
||||
f2.kind = rcx::NodeKind::UInt8;
|
||||
f2.name = "end";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 16;
|
||||
tree.addNode(f2);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
// Gap between offset 1 and 16 = 15 bytes padding
|
||||
QVERIFY(result.contains("[0xF]"));
|
||||
// Total size = 17
|
||||
QVERIFY(result.contains("static_assert(sizeof(TailPad) == 0x11"));
|
||||
}
|
||||
|
||||
// ── Overlap warning ──
|
||||
|
||||
void testOverlapWarning() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "OverlapStruct";
|
||||
root.structTypeName = "OverlapStruct";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
// Two fields that overlap: both at offset 0, size 8 and size 4
|
||||
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 = 4; // starts at 4, but wide ends at 8 => overlap
|
||||
tree.addNode(f2);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
// Should contain overlap warning
|
||||
QVERIFY(result.contains("WARNING: overlap"));
|
||||
}
|
||||
|
||||
// ── Nested struct ──
|
||||
|
||||
void testNestedStruct() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
// Outer struct
|
||||
rcx::Node outer;
|
||||
outer.kind = rcx::NodeKind::Struct;
|
||||
outer.name = "Outer";
|
||||
outer.structTypeName = "Outer";
|
||||
outer.parentId = 0;
|
||||
int oi = tree.addNode(outer);
|
||||
uint64_t outerId = tree.nodes[oi].id;
|
||||
|
||||
// Inner struct as child
|
||||
rcx::Node inner;
|
||||
inner.kind = rcx::NodeKind::Struct;
|
||||
inner.name = "pos";
|
||||
inner.structTypeName = "Vec2f";
|
||||
inner.parentId = outerId;
|
||||
inner.offset = 0;
|
||||
int ii = tree.addNode(inner);
|
||||
uint64_t innerId = tree.nodes[ii].id;
|
||||
|
||||
// Inner fields
|
||||
rcx::Node ix;
|
||||
ix.kind = rcx::NodeKind::Float;
|
||||
ix.name = "x";
|
||||
ix.parentId = innerId;
|
||||
ix.offset = 0;
|
||||
tree.addNode(ix);
|
||||
|
||||
rcx::Node iy;
|
||||
iy.kind = rcx::NodeKind::Float;
|
||||
iy.name = "y";
|
||||
iy.parentId = innerId;
|
||||
iy.offset = 4;
|
||||
tree.addNode(iy);
|
||||
|
||||
// Another field in outer after inner
|
||||
rcx::Node f2;
|
||||
f2.kind = rcx::NodeKind::Int32;
|
||||
f2.name = "score";
|
||||
f2.parentId = outerId;
|
||||
f2.offset = 8;
|
||||
tree.addNode(f2);
|
||||
|
||||
QString result = rcx::renderCpp(tree, outerId);
|
||||
|
||||
// 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;"));
|
||||
QVERIFY(result.contains("int32_t score;"));
|
||||
}
|
||||
|
||||
// ── Primitive array ──
|
||||
|
||||
void testPrimitiveArray() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "WithArray";
|
||||
root.structTypeName = "WithArray";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node arr;
|
||||
arr.kind = rcx::NodeKind::Array;
|
||||
arr.name = "data";
|
||||
arr.parentId = rootId;
|
||||
arr.offset = 0;
|
||||
arr.arrayLen = 16;
|
||||
arr.elementKind = rcx::NodeKind::UInt32;
|
||||
tree.addNode(arr);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
QVERIFY(result.contains("uint32_t data[16];"));
|
||||
}
|
||||
|
||||
// ── Pointer fields ──
|
||||
|
||||
void testPointerFields() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
// Target struct (separate root)
|
||||
rcx::Node target;
|
||||
target.kind = rcx::NodeKind::Struct;
|
||||
target.name = "Target";
|
||||
target.structTypeName = "TargetData";
|
||||
target.parentId = 0;
|
||||
target.offset = 0x100;
|
||||
int ti = tree.addNode(target);
|
||||
uint64_t targetId = tree.nodes[ti].id;
|
||||
|
||||
rcx::Node tf;
|
||||
tf.kind = rcx::NodeKind::UInt32;
|
||||
tf.name = "value";
|
||||
tf.parentId = targetId;
|
||||
tf.offset = 0;
|
||||
tree.addNode(tf);
|
||||
|
||||
// Main struct with pointers
|
||||
rcx::Node main;
|
||||
main.kind = rcx::NodeKind::Struct;
|
||||
main.name = "Main";
|
||||
main.structTypeName = "MainStruct";
|
||||
main.parentId = 0;
|
||||
int mi = tree.addNode(main);
|
||||
uint64_t mainId = tree.nodes[mi].id;
|
||||
|
||||
// ptr64 with reference
|
||||
rcx::Node p64;
|
||||
p64.kind = rcx::NodeKind::Pointer64;
|
||||
p64.name = "pTarget";
|
||||
p64.parentId = mainId;
|
||||
p64.offset = 0;
|
||||
p64.refId = targetId;
|
||||
tree.addNode(p64);
|
||||
|
||||
// ptr64 without reference
|
||||
rcx::Node p64n;
|
||||
p64n.kind = rcx::NodeKind::Pointer64;
|
||||
p64n.name = "pVoid";
|
||||
p64n.parentId = mainId;
|
||||
p64n.offset = 8;
|
||||
tree.addNode(p64n);
|
||||
|
||||
// ptr32 with reference
|
||||
rcx::Node p32;
|
||||
p32.kind = rcx::NodeKind::Pointer32;
|
||||
p32.name = "pTarget32";
|
||||
p32.parentId = mainId;
|
||||
p32.offset = 16;
|
||||
p32.refId = targetId;
|
||||
tree.addNode(p32);
|
||||
|
||||
QString result = rcx::renderCpp(tree, mainId);
|
||||
|
||||
// ptr64 with target → real C++ pointer
|
||||
QVERIFY(result.contains("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;"));
|
||||
}
|
||||
|
||||
// ── Vector and matrix types ──
|
||||
|
||||
void testVectorTypes() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Vectors";
|
||||
root.structTypeName = "Vectors";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node v2;
|
||||
v2.kind = rcx::NodeKind::Vec2;
|
||||
v2.name = "pos2d";
|
||||
v2.parentId = rootId;
|
||||
v2.offset = 0;
|
||||
tree.addNode(v2);
|
||||
|
||||
rcx::Node v3;
|
||||
v3.kind = rcx::NodeKind::Vec3;
|
||||
v3.name = "pos3d";
|
||||
v3.parentId = rootId;
|
||||
v3.offset = 8;
|
||||
tree.addNode(v3);
|
||||
|
||||
rcx::Node v4;
|
||||
v4.kind = rcx::NodeKind::Vec4;
|
||||
v4.name = "color";
|
||||
v4.parentId = rootId;
|
||||
v4.offset = 20;
|
||||
tree.addNode(v4);
|
||||
|
||||
rcx::Node mat;
|
||||
mat.kind = rcx::NodeKind::Mat4x4;
|
||||
mat.name = "transform";
|
||||
mat.parentId = rootId;
|
||||
mat.offset = 36;
|
||||
tree.addNode(mat);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
QVERIFY(result.contains("float pos2d[2];"));
|
||||
QVERIFY(result.contains("float pos3d[3];"));
|
||||
QVERIFY(result.contains("float color[4];"));
|
||||
QVERIFY(result.contains("float transform[4][4];"));
|
||||
}
|
||||
|
||||
// ── String types ──
|
||||
|
||||
void testStringTypes() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Strings";
|
||||
root.structTypeName = "Strings";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node utf8;
|
||||
utf8.kind = rcx::NodeKind::UTF8;
|
||||
utf8.name = "name";
|
||||
utf8.parentId = rootId;
|
||||
utf8.offset = 0;
|
||||
utf8.strLen = 64;
|
||||
tree.addNode(utf8);
|
||||
|
||||
rcx::Node utf16;
|
||||
utf16.kind = rcx::NodeKind::UTF16;
|
||||
utf16.name = "wname";
|
||||
utf16.parentId = rootId;
|
||||
utf16.offset = 64;
|
||||
utf16.strLen = 32;
|
||||
tree.addNode(utf16);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
QVERIFY(result.contains("char name[64];"));
|
||||
QVERIFY(result.contains("wchar_t wname[32];"));
|
||||
}
|
||||
|
||||
// ── Padding node ──
|
||||
|
||||
void testPaddingNode() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "PadTest";
|
||||
root.structTypeName = "PadTest";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node pad;
|
||||
pad.kind = rcx::NodeKind::Padding;
|
||||
pad.name = "reserved";
|
||||
pad.parentId = rootId;
|
||||
pad.offset = 0;
|
||||
pad.arrayLen = 16;
|
||||
tree.addNode(pad);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
QVERIFY(result.contains("uint8_t reserved[16];"));
|
||||
}
|
||||
|
||||
// ── Full SDK export (multiple root structs) ──
|
||||
|
||||
void testFullSdkExport() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
// Struct A at offset 0
|
||||
rcx::Node a;
|
||||
a.kind = rcx::NodeKind::Struct;
|
||||
a.name = "StructA";
|
||||
a.structTypeName = "StructA";
|
||||
a.parentId = 0;
|
||||
a.offset = 0;
|
||||
int ai = tree.addNode(a);
|
||||
uint64_t aId = tree.nodes[ai].id;
|
||||
|
||||
rcx::Node af;
|
||||
af.kind = rcx::NodeKind::UInt32;
|
||||
af.name = "valueA";
|
||||
af.parentId = aId;
|
||||
af.offset = 0;
|
||||
tree.addNode(af);
|
||||
|
||||
// Struct B at offset 0x100
|
||||
rcx::Node b;
|
||||
b.kind = rcx::NodeKind::Struct;
|
||||
b.name = "StructB";
|
||||
b.structTypeName = "StructB";
|
||||
b.parentId = 0;
|
||||
b.offset = 0x100;
|
||||
int bi = tree.addNode(b);
|
||||
uint64_t bId = tree.nodes[bi].id;
|
||||
|
||||
rcx::Node bf;
|
||||
bf.kind = rcx::NodeKind::UInt64;
|
||||
bf.name = "valueB";
|
||||
bf.parentId = bId;
|
||||
bf.offset = 0;
|
||||
tree.addNode(bf);
|
||||
|
||||
QString result = rcx::renderCppAll(tree);
|
||||
|
||||
QVERIFY(result.contains("Full SDK export"));
|
||||
QVERIFY(result.contains("struct StructA {"));
|
||||
QVERIFY(result.contains("struct StructB {"));
|
||||
QVERIFY(result.contains("uint32_t valueA;"));
|
||||
QVERIFY(result.contains("uint64_t valueB;"));
|
||||
QVERIFY(result.contains("static_assert(sizeof(StructA) == 0x4"));
|
||||
QVERIFY(result.contains("static_assert(sizeof(StructB) == 0x8"));
|
||||
}
|
||||
|
||||
// ── Null generator ──
|
||||
|
||||
void testNullGenerator() {
|
||||
auto tree = makeSimpleStruct();
|
||||
QString result = rcx::renderNull(tree, tree.nodes[0].id);
|
||||
QVERIFY(result.isEmpty());
|
||||
}
|
||||
|
||||
// ── Invalid root ID ──
|
||||
|
||||
void testInvalidRootId() {
|
||||
auto tree = makeSimpleStruct();
|
||||
QString result = rcx::renderCpp(tree, 9999);
|
||||
QVERIFY(result.isEmpty());
|
||||
}
|
||||
|
||||
// ── Non-struct root ──
|
||||
|
||||
void testNonStructRoot() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node n;
|
||||
n.kind = rcx::NodeKind::UInt32;
|
||||
n.name = "scalar";
|
||||
n.parentId = 0;
|
||||
tree.addNode(n);
|
||||
|
||||
QString result = rcx::renderCpp(tree, tree.nodes[0].id);
|
||||
QVERIFY(result.isEmpty());
|
||||
}
|
||||
|
||||
// ── Empty struct ──
|
||||
|
||||
void testEmptyStruct() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Empty";
|
||||
root.structTypeName = "Empty";
|
||||
root.parentId = 0;
|
||||
tree.addNode(root);
|
||||
|
||||
QString result = rcx::renderCpp(tree, tree.nodes[0].id);
|
||||
|
||||
QVERIFY(result.contains("struct Empty {"));
|
||||
QVERIFY(result.contains("};"));
|
||||
QVERIFY(result.contains("static_assert(sizeof(Empty) == 0x0"));
|
||||
}
|
||||
|
||||
// ── Name sanitization ──
|
||||
|
||||
void testNameSanitization() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "my struct-name";
|
||||
root.structTypeName = "my struct-name";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node f;
|
||||
f.kind = rcx::NodeKind::UInt32;
|
||||
f.name = "field with spaces";
|
||||
f.parentId = rootId;
|
||||
f.offset = 0;
|
||||
tree.addNode(f);
|
||||
|
||||
QString result = rcx::renderCpp(tree, rootId);
|
||||
|
||||
// Spaces and dashes should be replaced with underscores
|
||||
QVERIFY(result.contains("struct my_struct_name {"));
|
||||
QVERIFY(result.contains("uint32_t field_with_spaces;"));
|
||||
}
|
||||
|
||||
// ── Export produces valid file content ──
|
||||
|
||||
void testExportToFile() {
|
||||
auto tree = makeSimpleStruct();
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
QString text = rcx::renderCpp(tree, rootId);
|
||||
|
||||
QTemporaryFile tmpFile;
|
||||
tmpFile.setAutoRemove(true);
|
||||
QVERIFY(tmpFile.open());
|
||||
tmpFile.write(text.toUtf8());
|
||||
tmpFile.close();
|
||||
|
||||
// Read back and verify
|
||||
QVERIFY(tmpFile.open());
|
||||
QByteArray readBack = tmpFile.readAll();
|
||||
tmpFile.close();
|
||||
|
||||
QString readStr = QString::fromUtf8(readBack);
|
||||
QVERIFY(readStr.contains("#pragma once"));
|
||||
QVERIFY(readStr.contains("struct Player {"));
|
||||
QVERIFY(readStr.contains("static_assert"));
|
||||
}
|
||||
|
||||
// ── Full SDK with no structs (only primitives) ──
|
||||
|
||||
void testFullSdkNoStructs() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node n;
|
||||
n.kind = rcx::NodeKind::UInt32;
|
||||
n.name = "scalar";
|
||||
n.parentId = 0;
|
||||
tree.addNode(n);
|
||||
|
||||
QString result = rcx::renderCppAll(tree);
|
||||
|
||||
// Header present but no struct definitions
|
||||
QVERIFY(result.contains("#pragma once"));
|
||||
QVERIFY(!result.contains("struct "));
|
||||
}
|
||||
|
||||
// ── Deeply nested structs ──
|
||||
|
||||
void testDeeplyNested() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
// A > B > C, each containing one field
|
||||
rcx::Node a;
|
||||
a.kind = rcx::NodeKind::Struct;
|
||||
a.name = "A";
|
||||
a.structTypeName = "TypeA";
|
||||
a.parentId = 0;
|
||||
int ai = tree.addNode(a);
|
||||
uint64_t aId = tree.nodes[ai].id;
|
||||
|
||||
rcx::Node b;
|
||||
b.kind = rcx::NodeKind::Struct;
|
||||
b.name = "b";
|
||||
b.structTypeName = "TypeB";
|
||||
b.parentId = aId;
|
||||
b.offset = 0;
|
||||
int bi = tree.addNode(b);
|
||||
uint64_t bId = tree.nodes[bi].id;
|
||||
|
||||
rcx::Node c;
|
||||
c.kind = rcx::NodeKind::Struct;
|
||||
c.name = "c";
|
||||
c.structTypeName = "TypeC";
|
||||
c.parentId = bId;
|
||||
c.offset = 0;
|
||||
int ci = tree.addNode(c);
|
||||
uint64_t cId = tree.nodes[ci].id;
|
||||
|
||||
rcx::Node leaf;
|
||||
leaf.kind = rcx::NodeKind::UInt8;
|
||||
leaf.name = "val";
|
||||
leaf.parentId = cId;
|
||||
leaf.offset = 0;
|
||||
tree.addNode(leaf);
|
||||
|
||||
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);
|
||||
|
||||
// TypeA contains TypeB, TypeB contains TypeC
|
||||
QVERIFY(result.contains("TypeB b;"));
|
||||
QVERIFY(result.contains("TypeC c;"));
|
||||
QVERIFY(result.contains("uint8_t val;"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestGenerator)
|
||||
#include "test_generator.moc"
|
||||
1160
tests/test_validation.cpp
Normal file
1160
tests/test_validation.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user