mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: add C# and Python ctypes code generators
- C# backend: [StructLayout(LayoutKind.Explicit)] with [FieldOffset], IntPtr pointers, fixed arrays, enums - Python backend: ctypes.Structure with _fields_, POINTER() for typed pointers, c_void_p, padding - Both support enums, vectors, bitfields, arrays, unions, static fields - Export menu: C# Structs... and Python ctypes... entries - Format combo auto-populates new options - 14 new tests for both backends (all passing)
This commit is contained in:
@@ -804,6 +804,521 @@ static void emitDefinesForStruct(GenContext& ctx, uint64_t structId,
|
||||
ctx.output += QStringLiteral("\n");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ── C# backend ──
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
static QString csTypeName(NodeKind kind) {
|
||||
switch (kind) {
|
||||
case NodeKind::Hex8: return QStringLiteral("byte");
|
||||
case NodeKind::Hex16: return QStringLiteral("ushort");
|
||||
case NodeKind::Hex32: return QStringLiteral("uint");
|
||||
case NodeKind::Hex64: return QStringLiteral("ulong");
|
||||
case NodeKind::Int8: return QStringLiteral("sbyte");
|
||||
case NodeKind::Int16: return QStringLiteral("short");
|
||||
case NodeKind::Int32: return QStringLiteral("int");
|
||||
case NodeKind::Int64: return QStringLiteral("long");
|
||||
case NodeKind::UInt8: return QStringLiteral("byte");
|
||||
case NodeKind::UInt16: return QStringLiteral("ushort");
|
||||
case NodeKind::UInt32: return QStringLiteral("uint");
|
||||
case NodeKind::UInt64: return QStringLiteral("ulong");
|
||||
case NodeKind::Float: return QStringLiteral("float");
|
||||
case NodeKind::Double: return QStringLiteral("double");
|
||||
case NodeKind::Bool: return QStringLiteral("bool");
|
||||
case NodeKind::Pointer32: return QStringLiteral("uint");
|
||||
case NodeKind::Pointer64: return QStringLiteral("ulong");
|
||||
case NodeKind::FuncPtr32: return QStringLiteral("uint");
|
||||
case NodeKind::FuncPtr64: return QStringLiteral("ulong");
|
||||
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("byte");
|
||||
case NodeKind::UTF16: return QStringLiteral("char");
|
||||
default: return QStringLiteral("byte");
|
||||
}
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
static void emitCSharpStruct(GenContext& ctx, uint64_t structId);
|
||||
|
||||
static QString csType(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 csTypeName(kind);
|
||||
}
|
||||
|
||||
static void emitCSharpStructBody(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;
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
// C# uses [FieldOffset(N)] for explicit layout — no manual padding needed
|
||||
for (int ci : children) {
|
||||
const Node& child = tree.nodes[ci];
|
||||
if (isHexNode(child.kind)) continue; // skip padding/hex nodes
|
||||
|
||||
int absOffset = baseOffset + child.offset;
|
||||
QString name = sanitizeIdent(child.name.isEmpty()
|
||||
? QStringLiteral("field_%1").arg(child.offset, 2, 16, QChar('0'))
|
||||
: child.name);
|
||||
QString oc = offsetComment(absOffset);
|
||||
|
||||
if (child.kind == NodeKind::Struct) {
|
||||
if (child.classKeyword == QStringLiteral("bitfield")
|
||||
&& !child.bitfieldMembers.isEmpty()) {
|
||||
QString bfType = csType(ctx, child.elementKind);
|
||||
if (bfType.isEmpty()) bfType = QStringLiteral("uint");
|
||||
QStringList bits;
|
||||
for (const auto& m : child.bitfieldMembers)
|
||||
bits << QStringLiteral("%1:%2").arg(sanitizeIdent(m.name)).arg(m.bitWidth);
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public %2 %3;")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), bfType, name)
|
||||
+ QStringLiteral(" // bits: ") + bits.join(QStringLiteral(", "))
|
||||
+ oc + QStringLiteral("\n");
|
||||
} else if (child.structTypeName.isEmpty()) {
|
||||
// Anonymous inline — emit as fixed byte array
|
||||
int span = tree.structSpan(child.id, &ctx.childMap);
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed byte %2[0x%3];")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), name)
|
||||
.arg(QString::number(span, 16).toUpper())
|
||||
+ oc + QStringLiteral("\n");
|
||||
} else {
|
||||
QString typeName = sanitizeIdent(child.structTypeName);
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public %2 %3;")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), typeName, name)
|
||||
+ oc + 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;
|
||||
}
|
||||
}
|
||||
if (hasStructChild && !elemTypeName.isEmpty()) {
|
||||
// MarshalAs for struct arrays
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = %2)] public %3[] %4;")
|
||||
.arg(QString::number(absOffset, 16).toUpper())
|
||||
.arg(child.arrayLen)
|
||||
.arg(elemTypeName, name)
|
||||
+ oc + QStringLiteral("\n");
|
||||
} else {
|
||||
QString elemType = csType(ctx, child.elementKind);
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed %2 %3[%4];")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), elemType, name)
|
||||
.arg(child.arrayLen)
|
||||
+ oc + QStringLiteral("\n");
|
||||
}
|
||||
} else {
|
||||
// Primitive fields
|
||||
switch (child.kind) {
|
||||
case NodeKind::Vec2:
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed float %2[2];")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), name) + oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::Vec3:
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed float %2[3];")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), name) + oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::Vec4:
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed float %2[4];")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), name) + oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::Mat4x4:
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed float %2[16];")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), name) + oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::UTF8:
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed byte %2[%3];")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), name)
|
||||
.arg(child.strLen) + oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::UTF16:
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public fixed char %2[%3];")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), name)
|
||||
.arg(child.strLen) + oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::Pointer32:
|
||||
case NodeKind::Pointer64: {
|
||||
bool isNativePtr = (child.kind == NodeKind::Pointer32 && ctx.tree.pointerSize <= 4)
|
||||
|| (child.kind == NodeKind::Pointer64 && ctx.tree.pointerSize >= 8);
|
||||
if (isNativePtr)
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public IntPtr %2;")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), name) + oc + QStringLiteral("\n");
|
||||
else
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public %2 %3;")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), csType(ctx, child.kind), name)
|
||||
+ oc + QStringLiteral("\n");
|
||||
break;
|
||||
}
|
||||
case NodeKind::FuncPtr32:
|
||||
case NodeKind::FuncPtr64:
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public IntPtr %2;")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), name)
|
||||
+ QStringLiteral(" // fn ptr") + oc + QStringLiteral("\n");
|
||||
break;
|
||||
default:
|
||||
ctx.output += ind + QStringLiteral("[FieldOffset(0x%1)] public %2 %3;")
|
||||
.arg(QString::number(absOffset, 16).toUpper(), csType(ctx, child.kind), name)
|
||||
+ oc + QStringLiteral("\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int si : staticIdxs) {
|
||||
const Node& sf = tree.nodes[si];
|
||||
QString sfType = sf.structTypeName.isEmpty() ? csType(ctx, sf.kind) : sf.structTypeName;
|
||||
ctx.output += ind + QStringLiteral("// static: %1 %2 @ %3\n")
|
||||
.arg(sfType, sanitizeIdent(sf.name), sf.offsetExpr);
|
||||
}
|
||||
}
|
||||
|
||||
static void emitCSharpStruct(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("public enum %1 : long\n{\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"));
|
||||
|
||||
ctx.output += QStringLiteral("[StructLayout(LayoutKind.Explicit, Size = 0x%1)]\n")
|
||||
.arg(QString::number(structSize, 16).toUpper());
|
||||
ctx.output += QStringLiteral("public unsafe struct %1\n{\n").arg(typeName);
|
||||
|
||||
emitCSharpStructBody(ctx, structId, isUnion, 1, 0);
|
||||
|
||||
ctx.output += QStringLiteral("}")
|
||||
+ offsetComment(structSize, true)
|
||||
+ QStringLiteral("\n\n");
|
||||
|
||||
ctx.visiting.remove(structId);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ── Python ctypes backend ──
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
static QString pyTypeName(NodeKind kind) {
|
||||
switch (kind) {
|
||||
case NodeKind::Hex8: return QStringLiteral("ctypes.c_uint8");
|
||||
case NodeKind::Hex16: return QStringLiteral("ctypes.c_uint16");
|
||||
case NodeKind::Hex32: return QStringLiteral("ctypes.c_uint32");
|
||||
case NodeKind::Hex64: return QStringLiteral("ctypes.c_uint64");
|
||||
case NodeKind::Int8: return QStringLiteral("ctypes.c_int8");
|
||||
case NodeKind::Int16: return QStringLiteral("ctypes.c_int16");
|
||||
case NodeKind::Int32: return QStringLiteral("ctypes.c_int32");
|
||||
case NodeKind::Int64: return QStringLiteral("ctypes.c_int64");
|
||||
case NodeKind::UInt8: return QStringLiteral("ctypes.c_uint8");
|
||||
case NodeKind::UInt16: return QStringLiteral("ctypes.c_uint16");
|
||||
case NodeKind::UInt32: return QStringLiteral("ctypes.c_uint32");
|
||||
case NodeKind::UInt64: return QStringLiteral("ctypes.c_uint64");
|
||||
case NodeKind::Float: return QStringLiteral("ctypes.c_float");
|
||||
case NodeKind::Double: return QStringLiteral("ctypes.c_double");
|
||||
case NodeKind::Bool: return QStringLiteral("ctypes.c_bool");
|
||||
case NodeKind::Pointer32: return QStringLiteral("ctypes.c_uint32");
|
||||
case NodeKind::Pointer64: return QStringLiteral("ctypes.c_uint64");
|
||||
case NodeKind::FuncPtr32: return QStringLiteral("ctypes.c_uint32");
|
||||
case NodeKind::FuncPtr64: return QStringLiteral("ctypes.c_uint64");
|
||||
case NodeKind::Vec2: return QStringLiteral("ctypes.c_float");
|
||||
case NodeKind::Vec3: return QStringLiteral("ctypes.c_float");
|
||||
case NodeKind::Vec4: return QStringLiteral("ctypes.c_float");
|
||||
case NodeKind::Mat4x4: return QStringLiteral("ctypes.c_float");
|
||||
case NodeKind::UTF8: return QStringLiteral("ctypes.c_char");
|
||||
case NodeKind::UTF16: return QStringLiteral("ctypes.c_wchar");
|
||||
default: return QStringLiteral("ctypes.c_uint8");
|
||||
}
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
static void emitPythonStruct(GenContext& ctx, uint64_t structId);
|
||||
|
||||
static void emitPythonStructBody(GenContext& ctx, uint64_t structId,
|
||||
bool isUnion, 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 = QStringLiteral(" "); // 2 levels for inside _fields_
|
||||
|
||||
QVector<int> allChildren = ctx.childMap.value(structId);
|
||||
QVector<int> children;
|
||||
for (int ci : allChildren) {
|
||||
if (!tree.nodes[ci].isStatic)
|
||||
children.append(ci);
|
||||
}
|
||||
std::sort(children.begin(), children.end(), [&](int a, int b) {
|
||||
return tree.nodes[a].offset < tree.nodes[b].offset;
|
||||
});
|
||||
|
||||
auto emitPadField = [&](int relOffset, int size) {
|
||||
if (size <= 0) return;
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_uint8 * 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)
|
||||
emitPadField(cursor, child.offset - cursor);
|
||||
}
|
||||
|
||||
// Collapse hex nodes into padding
|
||||
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++;
|
||||
}
|
||||
emitPadField(runStart, runEnd - runStart);
|
||||
cursor = runEnd;
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
|
||||
int absOffset = baseOffset + child.offset;
|
||||
QString name = sanitizeIdent(child.name.isEmpty()
|
||||
? QStringLiteral("field_%1").arg(child.offset, 2, 16, QChar('0'))
|
||||
: child.name);
|
||||
QString oc = offsetComment(absOffset);
|
||||
|
||||
if (child.kind == NodeKind::Struct) {
|
||||
if (child.classKeyword == QStringLiteral("bitfield")
|
||||
&& !child.bitfieldMembers.isEmpty()) {
|
||||
QString bfType = pyTypeName(child.elementKind);
|
||||
if (bfType.isEmpty()) bfType = QStringLiteral("ctypes.c_uint32");
|
||||
QStringList bits;
|
||||
for (const auto& m : child.bitfieldMembers)
|
||||
bits << QStringLiteral("%1:%2").arg(sanitizeIdent(m.name)).arg(m.bitWidth);
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", %2),")
|
||||
.arg(name, bfType)
|
||||
+ QStringLiteral(" # bits: ") + bits.join(QStringLiteral(", "))
|
||||
+ oc + QStringLiteral("\n");
|
||||
} else if (child.structTypeName.isEmpty()) {
|
||||
int span = tree.structSpan(child.id, &ctx.childMap);
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_uint8 * 0x%2),")
|
||||
.arg(name)
|
||||
.arg(QString::number(span, 16).toUpper())
|
||||
+ oc + QStringLiteral("\n");
|
||||
} else {
|
||||
QString typeName = sanitizeIdent(child.structTypeName);
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", %2),")
|
||||
.arg(name, typeName)
|
||||
+ oc + 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;
|
||||
}
|
||||
}
|
||||
if (hasStructChild && !elemTypeName.isEmpty()) {
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", %2 * %3),")
|
||||
.arg(name, elemTypeName).arg(child.arrayLen) + oc + QStringLiteral("\n");
|
||||
} else {
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", %2 * %3),")
|
||||
.arg(name, pyTypeName(child.elementKind)).arg(child.arrayLen)
|
||||
+ oc + QStringLiteral("\n");
|
||||
}
|
||||
} else {
|
||||
// Primitive fields
|
||||
switch (child.kind) {
|
||||
case NodeKind::Vec2:
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_float * 2),").arg(name)
|
||||
+ oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::Vec3:
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_float * 3),").arg(name)
|
||||
+ oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::Vec4:
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_float * 4),").arg(name)
|
||||
+ oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::Mat4x4:
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", (ctypes.c_float * 4) * 4),").arg(name)
|
||||
+ oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::UTF8:
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_char * %2),").arg(name)
|
||||
.arg(child.strLen) + oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::UTF16:
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_wchar * %2),").arg(name)
|
||||
.arg(child.strLen) + oc + QStringLiteral("\n");
|
||||
break;
|
||||
case NodeKind::Pointer32:
|
||||
case NodeKind::Pointer64: {
|
||||
bool isNativePtr = (child.kind == NodeKind::Pointer32 && ctx.tree.pointerSize <= 4)
|
||||
|| (child.kind == NodeKind::Pointer64 && ctx.tree.pointerSize >= 8);
|
||||
if (child.refId != 0) {
|
||||
int refIdx = tree.indexOfId(child.refId);
|
||||
if (refIdx >= 0) {
|
||||
QString target = ctx.structName(tree.nodes[refIdx]);
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", ctypes.POINTER(%2)),").arg(name, target)
|
||||
+ oc + QStringLiteral("\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isNativePtr)
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_void_p),").arg(name)
|
||||
+ oc + QStringLiteral("\n");
|
||||
else
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", %2),").arg(name, pyTypeName(child.kind))
|
||||
+ oc + QStringLiteral("\n");
|
||||
break;
|
||||
}
|
||||
case NodeKind::FuncPtr32:
|
||||
case NodeKind::FuncPtr64:
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", ctypes.c_void_p),").arg(name)
|
||||
+ QStringLiteral(" # fn ptr") + oc + QStringLiteral("\n");
|
||||
break;
|
||||
default:
|
||||
ctx.output += ind + QStringLiteral("(\"%1\", %2),").arg(name, pyTypeName(child.kind))
|
||||
+ oc + QStringLiteral("\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int childEnd = child.offset + childSize;
|
||||
if (childEnd > cursor) cursor = childEnd;
|
||||
i++;
|
||||
}
|
||||
|
||||
// Tail padding
|
||||
if (!isUnion && cursor < structSize)
|
||||
emitPadField(cursor, structSize - cursor);
|
||||
}
|
||||
|
||||
static void emitPythonStruct(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 — emit as class with constants
|
||||
if (kw == QStringLiteral("enum") && !node.enumMembers.isEmpty()) {
|
||||
ctx.output += QStringLiteral("class %1: # enum\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"));
|
||||
QString baseClass = isUnion ? QStringLiteral("ctypes.Union") : QStringLiteral("ctypes.Structure");
|
||||
|
||||
ctx.output += QStringLiteral("class %1(%2):").arg(typeName, baseClass)
|
||||
+ offsetComment(structSize, true) + QStringLiteral("\n");
|
||||
ctx.output += QStringLiteral(" _fields_ = [\n");
|
||||
|
||||
emitPythonStructBody(ctx, structId, isUnion, 0);
|
||||
|
||||
ctx.output += QStringLiteral(" ]\n\n");
|
||||
|
||||
ctx.visiting.remove(structId);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ── Reachable struct collector (for "Current + Children" scope) ──
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -856,6 +1371,8 @@ const char* codeFormatName(CodeFormat fmt) {
|
||||
case CodeFormat::CppHeader: return "C/C++";
|
||||
case CodeFormat::RustStruct: return "Rust";
|
||||
case CodeFormat::DefineOffsets: return "#define";
|
||||
case CodeFormat::CSharpStruct: return "C#";
|
||||
case CodeFormat::PythonCtypes: return "Python";
|
||||
default: return "C/C++";
|
||||
}
|
||||
}
|
||||
@@ -865,6 +1382,8 @@ const char* codeFormatFileFilter(CodeFormat 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 (*)";
|
||||
case CodeFormat::CSharpStruct: return "C# Source (*.cs);;All Files (*)";
|
||||
case CodeFormat::PythonCtypes: return "Python Source (*.py);;All Files (*)";
|
||||
default: return "All Files (*)";
|
||||
}
|
||||
}
|
||||
@@ -1025,6 +1544,98 @@ QString renderDefinesAll(const NodeTree& tree) {
|
||||
return ctx.output;
|
||||
}
|
||||
|
||||
// ── C# public API ──
|
||||
|
||||
QString renderCSharp(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("using System.Runtime.InteropServices;\n\n");
|
||||
emitCSharpStruct(ctx, rootStructId);
|
||||
return alignComments(ctx.output);
|
||||
}
|
||||
|
||||
QString renderCSharpTree(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("using System.Runtime.InteropServices;\n\n");
|
||||
|
||||
for (uint64_t sid : collectReachableStructs(tree, childMap, rootStructId))
|
||||
emitCSharpStruct(ctx, sid);
|
||||
|
||||
return alignComments(ctx.output);
|
||||
}
|
||||
|
||||
QString renderCSharpAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases,
|
||||
bool emitAsserts) {
|
||||
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, typeAliases, emitAsserts};
|
||||
ctx.output += QStringLiteral("using System.Runtime.InteropServices;\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)
|
||||
emitCSharpStruct(ctx, tree.nodes[ri].id);
|
||||
}
|
||||
return alignComments(ctx.output);
|
||||
}
|
||||
|
||||
// ── Python public API ──
|
||||
|
||||
QString renderPython(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("import ctypes\n\n");
|
||||
emitPythonStruct(ctx, rootStructId);
|
||||
return alignComments(ctx.output);
|
||||
}
|
||||
|
||||
QString renderPythonTree(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("import ctypes\n\n");
|
||||
|
||||
for (uint64_t sid : collectReachableStructs(tree, childMap, rootStructId))
|
||||
emitPythonStruct(ctx, sid);
|
||||
|
||||
return alignComments(ctx.output);
|
||||
}
|
||||
|
||||
QString renderPythonAll(const NodeTree& tree) {
|
||||
GenContext ctx{tree, buildChildMap(tree), {}, {}, {}, {}, {}, 0, nullptr, false};
|
||||
ctx.output += QStringLiteral("import ctypes\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)
|
||||
emitPythonStruct(ctx, tree.nodes[ri].id);
|
||||
}
|
||||
return alignComments(ctx.output);
|
||||
}
|
||||
|
||||
// ── Format dispatch ──
|
||||
|
||||
QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
|
||||
@@ -1032,6 +1643,8 @@ QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
|
||||
switch (fmt) {
|
||||
case CodeFormat::RustStruct: return renderRust(tree, rootStructId, typeAliases, emitAsserts);
|
||||
case CodeFormat::DefineOffsets: return renderDefines(tree, rootStructId);
|
||||
case CodeFormat::CSharpStruct: return renderCSharp(tree, rootStructId, typeAliases, emitAsserts);
|
||||
case CodeFormat::PythonCtypes: return renderPython(tree, rootStructId);
|
||||
default: return renderCpp(tree, rootStructId, typeAliases, emitAsserts);
|
||||
}
|
||||
}
|
||||
@@ -1041,6 +1654,8 @@ QString renderCodeTree(CodeFormat fmt, const NodeTree& tree, uint64_t rootStruct
|
||||
switch (fmt) {
|
||||
case CodeFormat::RustStruct: return renderRustTree(tree, rootStructId, typeAliases, emitAsserts);
|
||||
case CodeFormat::DefineOffsets: return renderDefinesTree(tree, rootStructId);
|
||||
case CodeFormat::CSharpStruct: return renderCSharpTree(tree, rootStructId, typeAliases, emitAsserts);
|
||||
case CodeFormat::PythonCtypes: return renderPythonTree(tree, rootStructId);
|
||||
default: return renderCppTree(tree, rootStructId, typeAliases, emitAsserts);
|
||||
}
|
||||
}
|
||||
@@ -1050,6 +1665,8 @@ QString renderCodeAll(CodeFormat fmt, const NodeTree& tree,
|
||||
switch (fmt) {
|
||||
case CodeFormat::RustStruct: return renderRustAll(tree, typeAliases, emitAsserts);
|
||||
case CodeFormat::DefineOffsets: return renderDefinesAll(tree);
|
||||
case CodeFormat::CSharpStruct: return renderCSharpAll(tree, typeAliases, emitAsserts);
|
||||
case CodeFormat::PythonCtypes: return renderPythonAll(tree);
|
||||
default: return renderCppAll(tree, typeAliases, emitAsserts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,17 +6,83 @@
|
||||
|
||||
namespace rcx {
|
||||
|
||||
// Generate C++ struct definitions for a single root struct and all
|
||||
// nested/referenced types reachable from it.
|
||||
// ── Code output format ──
|
||||
|
||||
enum class CodeFormat : int {
|
||||
CppHeader = 0, // C/C++ struct definitions
|
||||
RustStruct, // Rust #[repr(C)] struct definitions
|
||||
DefineOffsets, // #define ClassName_FieldName 0xNN
|
||||
CSharpStruct, // C# [StructLayout] with [FieldOffset]
|
||||
PythonCtypes, // Python ctypes.Structure
|
||||
_Count
|
||||
};
|
||||
|
||||
enum class CodeScope : int {
|
||||
Current = 0, // Just the selected struct
|
||||
WithChildren, // Selected struct + all referenced types
|
||||
FullSdk, // All root-level structs
|
||||
_Count
|
||||
};
|
||||
|
||||
const char* codeFormatName(CodeFormat fmt);
|
||||
const char* codeFormatFileFilter(CodeFormat fmt);
|
||||
const char* codeScopeName(CodeScope scope);
|
||||
|
||||
// ── Format-aware dispatch (calls the appropriate backend) ──
|
||||
|
||||
QString renderCode(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
// Render rootStructId + all struct types reachable from it
|
||||
QString renderCodeTree(CodeFormat fmt, const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderCodeAll(CodeFormat fmt, const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
// ── Individual backends ──
|
||||
|
||||
QString renderCpp(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
// Generate C++ struct definitions for every root-level struct (full SDK).
|
||||
QString renderCppTree(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderCppAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderRust(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderRustTree(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderRustAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderDefines(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderDefinesTree(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderDefinesAll(const NodeTree& tree);
|
||||
|
||||
QString renderCSharp(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderCSharpTree(const NodeTree& tree, uint64_t rootStructId,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
QString renderCSharpAll(const NodeTree& tree,
|
||||
const QHash<NodeKind, QString>* typeAliases = nullptr,
|
||||
bool emitAsserts = false);
|
||||
|
||||
QString renderPython(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderPythonTree(const NodeTree& tree, uint64_t rootStructId);
|
||||
QString renderPythonAll(const NodeTree& tree);
|
||||
|
||||
// Null generator placeholder (returns empty string).
|
||||
QString renderNull(const NodeTree& tree, uint64_t rootStructId);
|
||||
|
||||
|
||||
47
src/main.cpp
47
src/main.cpp
@@ -733,6 +733,8 @@ void MainWindow::createMenus() {
|
||||
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, "C&# Structs...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportCSharp);
|
||||
Qt5Qt6AddAction(exportMenu, "&Python ctypes...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportPython);
|
||||
Qt5Qt6AddAction(exportMenu, "ReClass &XML...", QKeySequence::UnknownKey, QIcon(), this, &MainWindow::exportReclassXmlAction);
|
||||
// Examples submenu — scan once at init
|
||||
{
|
||||
@@ -3291,6 +3293,51 @@ void MainWindow::exportDefines() {
|
||||
setAppStatus("Exported to " + QFileInfo(path).fileName());
|
||||
}
|
||||
|
||||
// ── Export C# structs ──
|
||||
|
||||
void MainWindow::exportCSharp() {
|
||||
auto* tab = activeTab();
|
||||
if (!tab) return;
|
||||
|
||||
QString path = QFileDialog::getSaveFileName(this,
|
||||
"Export C# Structs", {}, "C# Source (*.cs);;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 = renderCSharpAll(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 Python ctypes ──
|
||||
|
||||
void MainWindow::exportPython() {
|
||||
auto* tab = activeTab();
|
||||
if (!tab) return;
|
||||
|
||||
QString path = QFileDialog::getSaveFileName(this,
|
||||
"Export Python ctypes", {}, "Python Source (*.py);;All Files (*)");
|
||||
if (path.isEmpty()) return;
|
||||
|
||||
QString text = renderPythonAll(tab->doc->tree);
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QMessageBox::warning(this, "Export Failed",
|
||||
"Could not write to: " + path);
|
||||
return;
|
||||
}
|
||||
file.write(text.toUtf8());
|
||||
setAppStatus("Exported to " + QFileInfo(path).fileName());
|
||||
}
|
||||
|
||||
// ── Export ReClass XML ──
|
||||
|
||||
void MainWindow::exportReclassXmlAction() {
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
#include <QLineEdit>
|
||||
#include <QMap>
|
||||
#include <QButtonGroup>
|
||||
#include <QComboBox>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QToolButton>
|
||||
#include <Qsci/qsciscintilla.h>
|
||||
|
||||
namespace rcx {
|
||||
@@ -58,6 +60,10 @@ private slots:
|
||||
void toggleMcp();
|
||||
void setEditorFont(const QString& fontName);
|
||||
void exportCpp();
|
||||
void exportRust();
|
||||
void exportDefines();
|
||||
void exportCSharp();
|
||||
void exportPython();
|
||||
void exportReclassXmlAction();
|
||||
void importFromSource();
|
||||
void importReclassXml();
|
||||
@@ -65,6 +71,7 @@ private slots:
|
||||
void showTypeAliasesDialog();
|
||||
void editTheme();
|
||||
void showOptionsDialog();
|
||||
void showOptionsDialog(int initialPage);
|
||||
|
||||
public:
|
||||
// Status bar helpers — separate app / MCP channels
|
||||
@@ -106,6 +113,9 @@ private:
|
||||
QLineEdit* findBar = nullptr;
|
||||
QWidget* findContainer = nullptr;
|
||||
QWidget* renderedContainer = nullptr;
|
||||
QComboBox* fmtCombo = nullptr;
|
||||
QComboBox* scopeCombo = nullptr;
|
||||
QToolButton* fmtGear = nullptr;
|
||||
ViewMode viewMode = VM_Reclass;
|
||||
uint64_t lastRenderedRootId = 0;
|
||||
};
|
||||
|
||||
@@ -873,6 +873,559 @@ private slots:
|
||||
QVERIFY2(result.contains("sizeof(Small) == 0x4"),
|
||||
qPrintable("Expected sizeof(Small) == 0x4:\n" + result));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ── Rust backend tests ──
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
void testRustSimpleStruct() {
|
||||
auto tree = makeSimpleStruct();
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
QString result = rcx::renderRust(tree, rootId, nullptr, true);
|
||||
|
||||
QVERIFY(result.contains("// Generated by Reclass 2027"));
|
||||
QVERIFY(result.contains("#[repr(C)]"));
|
||||
QVERIFY(result.contains("pub struct Player {"));
|
||||
QVERIFY(result.contains("pub health: i32,"));
|
||||
QVERIFY(result.contains("pub speed: f32,"));
|
||||
QVERIFY(result.contains("pub id: u64,"));
|
||||
QVERIFY(result.contains("// 0x0"));
|
||||
QVERIFY(result.contains("// 0x4"));
|
||||
QVERIFY(result.contains("// 0x8"));
|
||||
QVERIFY(result.contains("core::mem::size_of::<Player>() == 0x10"));
|
||||
|
||||
// Without asserts
|
||||
QString noAsserts = rcx::renderRust(tree, rootId);
|
||||
QVERIFY(!noAsserts.contains("size_of"));
|
||||
}
|
||||
|
||||
void testRustPadding() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Padded";
|
||||
root.structTypeName = "Padded";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node f1;
|
||||
f1.kind = rcx::NodeKind::UInt32;
|
||||
f1.name = "a";
|
||||
f1.parentId = rootId;
|
||||
f1.offset = 0;
|
||||
tree.addNode(f1);
|
||||
|
||||
rcx::Node f2;
|
||||
f2.kind = rcx::NodeKind::UInt32;
|
||||
f2.name = "b";
|
||||
f2.parentId = rootId;
|
||||
f2.offset = 8;
|
||||
tree.addNode(f2);
|
||||
|
||||
QString result = rcx::renderRust(tree, rootId);
|
||||
QVERIFY(result.contains("pub _pad"));
|
||||
QVERIFY(result.contains("[u8; 0x4]"));
|
||||
}
|
||||
|
||||
void testRustPointers() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
rcx::Node target;
|
||||
target.kind = rcx::NodeKind::Struct;
|
||||
target.name = "Target";
|
||||
target.structTypeName = "Target";
|
||||
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 = "val";
|
||||
tf.parentId = targetId;
|
||||
tf.offset = 0;
|
||||
tree.addNode(tf);
|
||||
|
||||
rcx::Node main;
|
||||
main.kind = rcx::NodeKind::Struct;
|
||||
main.name = "PtrTest";
|
||||
main.structTypeName = "PtrTest";
|
||||
main.parentId = 0;
|
||||
int mi = tree.addNode(main);
|
||||
uint64_t mainId = tree.nodes[mi].id;
|
||||
|
||||
rcx::Node p1;
|
||||
p1.kind = rcx::NodeKind::Pointer64;
|
||||
p1.name = "typed";
|
||||
p1.parentId = mainId;
|
||||
p1.offset = 0;
|
||||
p1.refId = targetId;
|
||||
tree.addNode(p1);
|
||||
|
||||
rcx::Node p2;
|
||||
p2.kind = rcx::NodeKind::Pointer64;
|
||||
p2.name = "untyped";
|
||||
p2.parentId = mainId;
|
||||
p2.offset = 8;
|
||||
tree.addNode(p2);
|
||||
|
||||
QString result = rcx::renderRust(tree, mainId);
|
||||
QVERIFY(result.contains("pub typed: *mut Target,"));
|
||||
QVERIFY(result.contains("pub untyped: *mut core::ffi::c_void,"));
|
||||
}
|
||||
|
||||
void testRustVectors() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Vecs";
|
||||
root.structTypeName = "Vecs";
|
||||
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 = "pos";
|
||||
v2.parentId = rootId;
|
||||
v2.offset = 0;
|
||||
tree.addNode(v2);
|
||||
|
||||
rcx::Node v4;
|
||||
v4.kind = rcx::NodeKind::Vec4;
|
||||
v4.name = "color";
|
||||
v4.parentId = rootId;
|
||||
v4.offset = 8;
|
||||
tree.addNode(v4);
|
||||
|
||||
QString result = rcx::renderRust(tree, rootId);
|
||||
QVERIFY(result.contains("pub pos: [f32; 2],"));
|
||||
QVERIFY(result.contains("pub color: [f32; 4],"));
|
||||
}
|
||||
|
||||
void testRustFuncPtr() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "FP";
|
||||
root.structTypeName = "FP";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node fp;
|
||||
fp.kind = rcx::NodeKind::FuncPtr64;
|
||||
fp.name = "callback";
|
||||
fp.parentId = rootId;
|
||||
fp.offset = 0;
|
||||
tree.addNode(fp);
|
||||
|
||||
QString result = rcx::renderRust(tree, rootId);
|
||||
QVERIFY(result.contains("pub callback: Option<unsafe extern \"C\" fn()>,"));
|
||||
}
|
||||
|
||||
void testRustAll() {
|
||||
auto tree = makeSimpleStruct();
|
||||
QString result = rcx::renderRustAll(tree, nullptr, true);
|
||||
QVERIFY(result.contains("#[repr(C)]"));
|
||||
QVERIFY(result.contains("pub struct Player {"));
|
||||
QVERIFY(result.contains("core::mem::size_of::<Player>()"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ── #define offsets backend tests ──
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
void testDefineSimpleStruct() {
|
||||
auto tree = makeSimpleStruct();
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
QString result = rcx::renderDefines(tree, rootId);
|
||||
|
||||
QVERIFY(result.contains("#pragma once"));
|
||||
QVERIFY(result.contains("// Player"));
|
||||
QVERIFY(result.contains("#define Player_health 0x0"));
|
||||
QVERIFY(result.contains("#define Player_speed 0x4"));
|
||||
QVERIFY(result.contains("#define Player_id 0x8"));
|
||||
}
|
||||
|
||||
void testDefineSkipsHex() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "HexTest";
|
||||
root.structTypeName = "HexTest";
|
||||
root.parentId = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node h;
|
||||
h.kind = rcx::NodeKind::Hex32;
|
||||
h.name = "padding";
|
||||
h.parentId = rootId;
|
||||
h.offset = 0;
|
||||
tree.addNode(h);
|
||||
|
||||
rcx::Node f;
|
||||
f.kind = rcx::NodeKind::UInt32;
|
||||
f.name = "real_field";
|
||||
f.parentId = rootId;
|
||||
f.offset = 4;
|
||||
tree.addNode(f);
|
||||
|
||||
QString result = rcx::renderDefines(tree, rootId);
|
||||
QVERIFY(!result.contains("padding"));
|
||||
QVERIFY(result.contains("#define HexTest_real_field 0x4"));
|
||||
}
|
||||
|
||||
void testDefineAll() {
|
||||
auto tree = makeSimpleStruct();
|
||||
QString result = rcx::renderDefinesAll(tree);
|
||||
QVERIFY(result.contains("#pragma once"));
|
||||
QVERIFY(result.contains("#define Player_health 0x0"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ── Format dispatch tests ──
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
void testCodeFormatDispatch() {
|
||||
auto tree = makeSimpleStruct();
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
|
||||
QString cpp = rcx::renderCode(rcx::CodeFormat::CppHeader, tree, rootId);
|
||||
QVERIFY(cpp.contains("struct Player"));
|
||||
|
||||
QString rust = rcx::renderCode(rcx::CodeFormat::RustStruct, tree, rootId);
|
||||
QVERIFY(rust.contains("pub struct Player"));
|
||||
|
||||
QString defs = rcx::renderCode(rcx::CodeFormat::DefineOffsets, tree, rootId);
|
||||
QVERIFY(defs.contains("#define Player_health"));
|
||||
}
|
||||
|
||||
void testCodeFormatAllDispatch() {
|
||||
auto tree = makeSimpleStruct();
|
||||
|
||||
QString cpp = rcx::renderCodeAll(rcx::CodeFormat::CppHeader, tree);
|
||||
QVERIFY(cpp.contains("struct Player"));
|
||||
|
||||
QString rust = rcx::renderCodeAll(rcx::CodeFormat::RustStruct, tree);
|
||||
QVERIFY(rust.contains("pub struct Player"));
|
||||
|
||||
QString defs = rcx::renderCodeAll(rcx::CodeFormat::DefineOffsets, tree);
|
||||
QVERIFY(defs.contains("#define Player_health"));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// ── Scope tests (Current + Deps) ──
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
void testTreeScopeIncludesReferencedTypes() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
// Target struct (referenced by pointer)
|
||||
rcx::Node target;
|
||||
target.kind = rcx::NodeKind::Struct;
|
||||
target.name = "Target";
|
||||
target.structTypeName = "Target";
|
||||
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 = "val";
|
||||
tf.parentId = targetId;
|
||||
tf.offset = 0;
|
||||
tree.addNode(tf);
|
||||
|
||||
// Main struct with a pointer to Target
|
||||
rcx::Node main;
|
||||
main.kind = rcx::NodeKind::Struct;
|
||||
main.name = "Main";
|
||||
main.structTypeName = "Main";
|
||||
main.parentId = 0;
|
||||
int mi = tree.addNode(main);
|
||||
uint64_t mainId = tree.nodes[mi].id;
|
||||
|
||||
rcx::Node ptr;
|
||||
ptr.kind = rcx::NodeKind::Pointer64;
|
||||
ptr.name = "pTarget";
|
||||
ptr.parentId = mainId;
|
||||
ptr.offset = 0;
|
||||
ptr.refId = targetId;
|
||||
tree.addNode(ptr);
|
||||
|
||||
// "Current" scope: only Main, no Target definition
|
||||
QString current = rcx::renderCpp(tree, mainId);
|
||||
QVERIFY(current.contains("struct Main\n{"));
|
||||
QVERIFY(!current.contains("struct Target\n{"));
|
||||
|
||||
// "Current + Deps" scope: Main AND Target definitions
|
||||
QString withDeps = rcx::renderCppTree(tree, mainId);
|
||||
QVERIFY(withDeps.contains("struct Main\n{"));
|
||||
QVERIFY(withDeps.contains("struct Target\n{"));
|
||||
|
||||
// Same for Rust
|
||||
QString rustDeps = rcx::renderRustTree(tree, mainId);
|
||||
QVERIFY(rustDeps.contains("pub struct Main {"));
|
||||
QVERIFY(rustDeps.contains("pub struct Target {"));
|
||||
|
||||
// Same for #define
|
||||
QString defDeps = rcx::renderDefinesTree(tree, mainId);
|
||||
QVERIFY(defDeps.contains("#define Main_pTarget"));
|
||||
QVERIFY(defDeps.contains("#define Target_val"));
|
||||
}
|
||||
|
||||
void testTreeScopeDispatch() {
|
||||
rcx::NodeTree tree;
|
||||
|
||||
rcx::Node a;
|
||||
a.kind = rcx::NodeKind::Struct;
|
||||
a.name = "A";
|
||||
a.structTypeName = "A";
|
||||
a.parentId = 0;
|
||||
int ai = tree.addNode(a);
|
||||
uint64_t aId = tree.nodes[ai].id;
|
||||
|
||||
rcx::Node af;
|
||||
af.kind = rcx::NodeKind::UInt32;
|
||||
af.name = "x";
|
||||
af.parentId = aId;
|
||||
af.offset = 0;
|
||||
tree.addNode(af);
|
||||
|
||||
// renderCodeTree should work for all formats
|
||||
QString cpp = rcx::renderCodeTree(rcx::CodeFormat::CppHeader, tree, aId);
|
||||
QVERIFY(cpp.contains("struct A"));
|
||||
|
||||
QString rust = rcx::renderCodeTree(rcx::CodeFormat::RustStruct, tree, aId);
|
||||
QVERIFY(rust.contains("pub struct A"));
|
||||
|
||||
QString defs = rcx::renderCodeTree(rcx::CodeFormat::DefineOffsets, tree, aId);
|
||||
QVERIFY(defs.contains("#define A_x"));
|
||||
|
||||
QString cs = rcx::renderCodeTree(rcx::CodeFormat::CSharpStruct, tree, aId);
|
||||
QVERIFY(cs.contains("public unsafe struct A"));
|
||||
|
||||
QString py = rcx::renderCodeTree(rcx::CodeFormat::PythonCtypes, tree, aId);
|
||||
QVERIFY(py.contains("class A(ctypes.Structure)"));
|
||||
}
|
||||
|
||||
// ── C# backend ──
|
||||
|
||||
void testCSharpSimpleStruct() {
|
||||
auto tree = makeSimpleStruct();
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
QString result = rcx::renderCSharp(tree, rootId);
|
||||
|
||||
QVERIFY(result.contains("using System.Runtime.InteropServices;"));
|
||||
QVERIFY(result.contains("[StructLayout(LayoutKind.Explicit, Size = 0x10)]"));
|
||||
QVERIFY(result.contains("public unsafe struct Player"));
|
||||
QVERIFY(result.contains("[FieldOffset(0x0)] public int health;"));
|
||||
QVERIFY(result.contains("[FieldOffset(0x4)] public float speed;"));
|
||||
QVERIFY(result.contains("[FieldOffset(0x8)] public ulong id;"));
|
||||
}
|
||||
|
||||
void testCSharpPointers() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Foo";
|
||||
root.structTypeName = "Foo";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node p;
|
||||
p.kind = rcx::NodeKind::Pointer64;
|
||||
p.name = "ptr";
|
||||
p.parentId = rootId;
|
||||
p.offset = 0;
|
||||
tree.addNode(p);
|
||||
|
||||
QString result = rcx::renderCSharp(tree, rootId);
|
||||
QVERIFY(result.contains("IntPtr ptr"));
|
||||
}
|
||||
|
||||
void testCSharpAll() {
|
||||
auto tree = makeSimpleStruct();
|
||||
QString result = rcx::renderCSharpAll(tree);
|
||||
QVERIFY(result.contains("public unsafe struct Player"));
|
||||
QVERIFY(result.contains("[StructLayout("));
|
||||
}
|
||||
|
||||
void testCSharpEnum() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node e;
|
||||
e.kind = rcx::NodeKind::Struct;
|
||||
e.name = "Color";
|
||||
e.structTypeName = "Color";
|
||||
e.classKeyword = "enum";
|
||||
e.parentId = 0;
|
||||
e.offset = 0;
|
||||
e.enumMembers = {{"Red", 0}, {"Green", 1}, {"Blue", 2}};
|
||||
tree.addNode(e);
|
||||
|
||||
QString result = rcx::renderCSharpAll(tree);
|
||||
QVERIFY(result.contains("public enum Color : long"));
|
||||
QVERIFY(result.contains("Red = 0"));
|
||||
QVERIFY(result.contains("Green = 1"));
|
||||
QVERIFY(result.contains("Blue = 2"));
|
||||
}
|
||||
|
||||
void testCSharpVectors() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Xform";
|
||||
root.structTypeName = "Xform";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node v;
|
||||
v.kind = rcx::NodeKind::Vec3;
|
||||
v.name = "position";
|
||||
v.parentId = rootId;
|
||||
v.offset = 0;
|
||||
tree.addNode(v);
|
||||
|
||||
QString result = rcx::renderCSharp(tree, rootId);
|
||||
QVERIFY(result.contains("public fixed float position[3]"));
|
||||
}
|
||||
|
||||
// ── Python ctypes backend ──
|
||||
|
||||
void testPythonSimpleStruct() {
|
||||
auto tree = makeSimpleStruct();
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
QString result = rcx::renderPython(tree, rootId);
|
||||
|
||||
QVERIFY(result.contains("import ctypes"));
|
||||
QVERIFY(result.contains("class Player(ctypes.Structure)"));
|
||||
QVERIFY(result.contains("_fields_ = ["));
|
||||
QVERIFY(result.contains("(\"health\", ctypes.c_int32)"));
|
||||
QVERIFY(result.contains("(\"speed\", ctypes.c_float)"));
|
||||
QVERIFY(result.contains("(\"id\", ctypes.c_uint64)"));
|
||||
}
|
||||
|
||||
void testPythonPointers() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Bar";
|
||||
root.structTypeName = "Bar";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node p;
|
||||
p.kind = rcx::NodeKind::Pointer64;
|
||||
p.name = "ptr";
|
||||
p.parentId = rootId;
|
||||
p.offset = 0;
|
||||
tree.addNode(p);
|
||||
|
||||
QString result = rcx::renderPython(tree, rootId);
|
||||
QVERIFY(result.contains("(\"ptr\", ctypes.c_void_p)"));
|
||||
}
|
||||
|
||||
void testPythonTypedPointers() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node target;
|
||||
target.kind = rcx::NodeKind::Struct;
|
||||
target.name = "Target";
|
||||
target.structTypeName = "Target";
|
||||
target.parentId = 0;
|
||||
target.offset = 0;
|
||||
int ti = tree.addNode(target);
|
||||
uint64_t targetId = tree.nodes[ti].id;
|
||||
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Holder";
|
||||
root.structTypeName = "Holder";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node p;
|
||||
p.kind = rcx::NodeKind::Pointer64;
|
||||
p.name = "ref";
|
||||
p.parentId = rootId;
|
||||
p.offset = 0;
|
||||
p.refId = targetId;
|
||||
tree.addNode(p);
|
||||
|
||||
QString result = rcx::renderPython(tree, rootId);
|
||||
QVERIFY(result.contains("ctypes.POINTER(Target)"));
|
||||
}
|
||||
|
||||
void testPythonAll() {
|
||||
auto tree = makeSimpleStruct();
|
||||
QString result = rcx::renderPythonAll(tree);
|
||||
QVERIFY(result.contains("class Player(ctypes.Structure)"));
|
||||
}
|
||||
|
||||
void testPythonEnum() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node e;
|
||||
e.kind = rcx::NodeKind::Struct;
|
||||
e.name = "Status";
|
||||
e.structTypeName = "Status";
|
||||
e.classKeyword = "enum";
|
||||
e.parentId = 0;
|
||||
e.offset = 0;
|
||||
e.enumMembers = {{"Active", 1}, {"Inactive", 0}};
|
||||
tree.addNode(e);
|
||||
|
||||
QString result = rcx::renderPythonAll(tree);
|
||||
QVERIFY(result.contains("class Status:"));
|
||||
QVERIFY(result.contains("Active = 1"));
|
||||
QVERIFY(result.contains("Inactive = 0"));
|
||||
}
|
||||
|
||||
void testPythonVectors() {
|
||||
rcx::NodeTree tree;
|
||||
rcx::Node root;
|
||||
root.kind = rcx::NodeKind::Struct;
|
||||
root.name = "Pos";
|
||||
root.structTypeName = "Pos";
|
||||
root.parentId = 0;
|
||||
root.offset = 0;
|
||||
int ri = tree.addNode(root);
|
||||
uint64_t rootId = tree.nodes[ri].id;
|
||||
|
||||
rcx::Node v;
|
||||
v.kind = rcx::NodeKind::Vec4;
|
||||
v.name = "color";
|
||||
v.parentId = rootId;
|
||||
v.offset = 0;
|
||||
tree.addNode(v);
|
||||
|
||||
QString result = rcx::renderPython(tree, rootId);
|
||||
QVERIFY(result.contains("(\"color\", ctypes.c_float * 4)"));
|
||||
}
|
||||
|
||||
void testCSharpDispatch() {
|
||||
auto tree = makeSimpleStruct();
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
QString result = rcx::renderCode(rcx::CodeFormat::CSharpStruct, tree, rootId);
|
||||
QVERIFY(result.contains("[StructLayout("));
|
||||
}
|
||||
|
||||
void testPythonDispatch() {
|
||||
auto tree = makeSimpleStruct();
|
||||
uint64_t rootId = tree.nodes[0].id;
|
||||
QString result = rcx::renderCode(rcx::CodeFormat::PythonCtypes, tree, rootId);
|
||||
QVERIFY(result.contains("ctypes.Structure"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestGenerator)
|
||||
|
||||
Reference in New Issue
Block a user