#pragma once #include #include #include #include #include #include #include #include #include #include #include #include namespace rcx { // ── Node kind enum ── enum class NodeKind : uint8_t { Hex8, Hex16, Hex32, Hex64, Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float, Double, Bool, Pointer32, Pointer64, Vec2, Vec3, Vec4, Mat4x4, UTF8, UTF16, Padding, Struct, Array }; // ── Kind flags (replaces repeated Hex/Padding switches) ── enum KindFlags : uint32_t { KF_None = 0, KF_HexPreview = 1 << 0, // Hex8..Hex64 + Padding (ASCII+hex layout) KF_Container = 1 << 1, // Struct/Array KF_String = 1 << 2, // UTF8/UTF16 KF_Vector = 1 << 3, // Vec2/3/4 }; // ── Unified kind metadata table (single source of truth) ── struct KindMeta { NodeKind kind; const char* name; // UI/JSON name: "Hex64", "UInt16" const char* typeName; // display name: "Hex64", "uint16_t" int size; // byte size (0 = dynamic: Struct/Array) int lines; // display line count int align; // natural alignment uint32_t flags; // KindFlags bitmask }; inline constexpr KindMeta kKindMeta[] = { // kind name typeName sz ln al flags {NodeKind::Hex8, "Hex8", "Hex8", 1, 1, 1, KF_HexPreview}, {NodeKind::Hex16, "Hex16", "Hex16", 2, 1, 2, KF_HexPreview}, {NodeKind::Hex32, "Hex32", "Hex32", 4, 1, 4, KF_HexPreview}, {NodeKind::Hex64, "Hex64", "Hex64", 8, 1, 8, KF_HexPreview}, {NodeKind::Int8, "Int8", "int8_t", 1, 1, 1, KF_None}, {NodeKind::Int16, "Int16", "int16_t", 2, 1, 2, KF_None}, {NodeKind::Int32, "Int32", "int32_t", 4, 1, 4, KF_None}, {NodeKind::Int64, "Int64", "int64_t", 8, 1, 8, KF_None}, {NodeKind::UInt8, "UInt8", "uint8_t", 1, 1, 1, KF_None}, {NodeKind::UInt16, "UInt16", "uint16_t", 2, 1, 2, KF_None}, {NodeKind::UInt32, "UInt32", "uint32_t", 4, 1, 4, KF_None}, {NodeKind::UInt64, "UInt64", "uint64_t", 8, 1, 8, KF_None}, {NodeKind::Float, "Float", "float", 4, 1, 4, KF_None}, {NodeKind::Double, "Double", "double", 8, 1, 8, KF_None}, {NodeKind::Bool, "Bool", "bool", 1, 1, 1, KF_None}, {NodeKind::Pointer32, "Pointer32", "ptr32", 4, 1, 4, KF_None}, {NodeKind::Pointer64, "Pointer64", "ptr64", 8, 1, 8, KF_None}, {NodeKind::Vec2, "Vec2", "Vec2", 8, 2, 4, KF_Vector}, {NodeKind::Vec3, "Vec3", "Vec3", 12, 3, 4, KF_Vector}, {NodeKind::Vec4, "Vec4", "Vec4", 16, 4, 4, KF_Vector}, {NodeKind::Mat4x4, "Mat4x4", "Mat4x4", 64, 4, 4, KF_None}, {NodeKind::UTF8, "UTF8", "char[]", 1, 1, 1, KF_String}, {NodeKind::UTF16, "UTF16", "wchar_t[]", 2, 1, 2, KF_String}, {NodeKind::Padding, "Padding", "pad", 1, 1, 1, KF_HexPreview}, {NodeKind::Struct, "Struct", "struct", 0, 1, 1, KF_Container}, {NodeKind::Array, "Array", "array", 0, 1, 1, KF_Container}, }; inline constexpr const KindMeta* kindMeta(NodeKind k) { for (const auto& m : kKindMeta) if (m.kind == k) return &m; return nullptr; } inline constexpr int sizeForKind(NodeKind k) { auto* m = kindMeta(k); return m ? m->size : 0; } inline constexpr int linesForKind(NodeKind k) { auto* m = kindMeta(k); return m ? m->lines : 1; } inline constexpr int alignmentFor(NodeKind k) { auto* m = kindMeta(k); return m ? m->align : 1; } inline const char* kindToString(NodeKind k) { auto* m = kindMeta(k); return m ? m->name : "Unknown"; } inline NodeKind kindFromString(const QString& s) { for (const auto& m : kKindMeta) if (s == m.name) return m.kind; return NodeKind::Hex8; } inline NodeKind kindFromTypeName(const QString& s, bool* ok = nullptr) { for (const auto& m : kKindMeta) { if (s == m.typeName) { if (ok) *ok = true; return m.kind; } } if (ok) *ok = false; return NodeKind::Hex8; } inline constexpr uint32_t flagsFor(NodeKind k) { const auto* m = kindMeta(k); return m ? m->flags : 0; } inline constexpr bool isHexPreview(NodeKind k) { return flagsFor(k) & KF_HexPreview; } inline QStringList allTypeNamesForUI(bool stripBrackets = false) { QStringList out; out.reserve(std::size(kKindMeta)); for (const auto& m : kKindMeta) { QString t = QString::fromLatin1(m.typeName); if (stripBrackets) t.remove(QStringLiteral("[]")); out << t; } out.sort(Qt::CaseInsensitive); out.removeDuplicates(); return out; } // ── Marker vocabulary ── enum Marker : int { M_CONT = 0, M_PAD = 1, M_PTR0 = 2, M_CYCLE = 3, M_ERR = 4, M_STRUCT_BG = 5, M_HOVER = 6, M_SELECTED = 7, }; // ── Provider interface ── class Provider { public: virtual ~Provider() = default; virtual uint8_t readU8 (uint64_t addr) const = 0; virtual uint16_t readU16(uint64_t addr) const = 0; virtual uint32_t readU32(uint64_t addr) const = 0; virtual uint64_t readU64(uint64_t addr) const = 0; virtual float readF32(uint64_t addr) const = 0; virtual double readF64(uint64_t addr) const = 0; virtual QByteArray readBytes(uint64_t addr, int len) const = 0; virtual bool isValid() const = 0; virtual bool isReadable(uint64_t addr, int len) const = 0; virtual int size() const = 0; virtual bool isWritable() const { return false; } virtual bool writeBytes(uint64_t addr, const QByteArray& data) { Q_UNUSED(addr); Q_UNUSED(data); return false; } }; class FileProvider : public Provider { QByteArray m_data; template T readT(uint64_t a) const { if (a + sizeof(T) > (uint64_t)m_data.size()) return T{}; T v; memcpy(&v, m_data.data() + a, sizeof(T)); return v; } public: explicit FileProvider(const QByteArray& data) : m_data(data) {} static FileProvider fromFile(const QString& path) { QFile f(path); if (f.open(QIODevice::ReadOnly)) return FileProvider(f.readAll()); return FileProvider({}); } bool isValid() const override { return !m_data.isEmpty(); } bool isReadable(uint64_t addr, int len) const override { if (len <= 0) return len == 0; if (addr > (uint64_t)m_data.size()) return false; return (uint64_t)len <= (uint64_t)m_data.size() - addr; } int size() const override { return m_data.size(); } uint8_t readU8 (uint64_t a) const override { return readT(a); } uint16_t readU16(uint64_t a) const override { return readT(a); } uint32_t readU32(uint64_t a) const override { return readT(a); } uint64_t readU64(uint64_t a) const override { return readT(a); } float readF32(uint64_t a) const override { return readT(a); } double readF64(uint64_t a) const override { return readT(a); } QByteArray readBytes(uint64_t a, int len) const override { if (a >= (uint64_t)m_data.size()) return {}; int avail = qMin(len, (int)((uint64_t)m_data.size() - a)); return m_data.mid((int)a, avail); } bool isWritable() const override { return true; } bool writeBytes(uint64_t addr, const QByteArray& data) override { if (addr + data.size() > (uint64_t)m_data.size()) return false; memcpy(m_data.data() + addr, data.data(), data.size()); return true; } }; class NullProvider : public Provider { public: uint8_t readU8 (uint64_t) const override { return 0; } uint16_t readU16(uint64_t) const override { return 0; } uint32_t readU32(uint64_t) const override { return 0; } uint64_t readU64(uint64_t) const override { return 0; } float readF32(uint64_t) const override { return 0.0f; } double readF64(uint64_t) const override { return 0.0; } QByteArray readBytes(uint64_t, int) const override { return {}; } bool isValid() const override { return false; } bool isReadable(uint64_t, int) const override { return false; } int size() const override { return 0; } }; // ── Node ── struct Node { uint64_t id = 0; NodeKind kind = NodeKind::Hex8; QString name; uint64_t parentId = 0; // 0 = root (no parent) int offset = 0; int arrayLen = 1; // Array: element count int strLen = 64; bool collapsed = false; uint64_t refId = 0; // Pointer32/64: id of Struct to expand at *ptr NodeKind elementKind = NodeKind::UInt8; // Array: element type int viewIndex = 0; // Array: current view offset (transient) int byteSize() const { switch (kind) { case NodeKind::UTF8: return strLen; case NodeKind::UTF16: return strLen * 2; case NodeKind::Padding: return qMax(1, arrayLen); case NodeKind::Array: return arrayLen * sizeForKind(elementKind); default: return sizeForKind(kind); } } QJsonObject toJson() const { QJsonObject o; o["id"] = QString::number(id); o["kind"] = kindToString(kind); o["name"] = name; o["parentId"] = QString::number(parentId); o["offset"] = offset; o["arrayLen"] = arrayLen; o["strLen"] = strLen; o["collapsed"] = collapsed; o["refId"] = QString::number(refId); o["elementKind"] = kindToString(elementKind); return o; } static Node fromJson(const QJsonObject& o) { Node n; n.id = o["id"].toString("0").toULongLong(); n.kind = kindFromString(o["kind"].toString()); n.name = o["name"].toString(); n.parentId = o["parentId"].toString("0").toULongLong(); n.offset = o["offset"].toInt(0); n.arrayLen = o["arrayLen"].toInt(1); n.strLen = o["strLen"].toInt(64); n.collapsed = o["collapsed"].toBool(false); n.refId = o["refId"].toString("0").toULongLong(); n.elementKind = kindFromString(o["elementKind"].toString("UInt8")); return n; } // Helper: is this a string-like array (char[] or wchar_t[])? bool isStringArray() const { return kind == NodeKind::Array && (elementKind == NodeKind::UInt8 || elementKind == NodeKind::UInt16); } }; // ── NodeTree ── struct NodeTree { QVector nodes; uint64_t baseAddress = 0x00400000; uint64_t m_nextId = 1; mutable QHash m_idCache; int addNode(const Node& n) { Node copy = n; if (copy.id == 0) copy.id = m_nextId++; else if (copy.id >= m_nextId) m_nextId = copy.id + 1; nodes.append(copy); m_idCache.clear(); return nodes.size() - 1; } void invalidateIdCache() const { m_idCache.clear(); } int indexOfId(uint64_t id) const { if (m_idCache.isEmpty() && !nodes.isEmpty()) { for (int i = 0; i < nodes.size(); i++) m_idCache[nodes[i].id] = i; } return m_idCache.value(id, -1); } QVector childrenOf(uint64_t parentId) const { QVector result; for (int i = 0; i < nodes.size(); i++) { if (nodes[i].parentId == parentId) result.append(i); } return result; } // Collect node + all descendants (iterative, cycle-safe) QVector subtreeIndices(uint64_t nodeId) const { int idx = indexOfId(nodeId); if (idx < 0) return {}; // Build parent→children map QHash> childMap; for (int i = 0; i < nodes.size(); i++) childMap[nodes[i].parentId].append(i); // BFS with visited guard QVector result; QSet visited; QVector stack; stack.append(nodeId); result.append(idx); visited.insert(nodeId); while (!stack.isEmpty()) { uint64_t pid = stack.takeLast(); for (int ci : childMap.value(pid)) { uint64_t cid = nodes[ci].id; if (!visited.contains(cid)) { visited.insert(cid); result.append(ci); stack.append(cid); } } } return result; } int depthOf(int idx) const { int d = 0; QSet visited; int cur = idx; while (cur >= 0 && cur < nodes.size() && nodes[cur].parentId != 0) { if (visited.contains(cur)) break; visited.insert(cur); cur = indexOfId(nodes[cur].parentId); if (cur < 0) break; d++; } return d; } int64_t computeOffset(int idx) const { int64_t total = 0; QSet visited; int cur = idx; while (cur >= 0 && cur < nodes.size()) { if (visited.contains(cur)) break; visited.insert(cur); total += nodes[cur].offset; if (nodes[cur].parentId == 0) break; cur = indexOfId(nodes[cur].parentId); } return total; } int structSpan(uint64_t structId, const QHash>* childMap = nullptr) const { int maxEnd = 0; QVector kids = childMap ? childMap->value(structId) : childrenOf(structId); for (int ci : kids) { const Node& c = nodes[ci]; int sz = (c.kind == NodeKind::Struct || c.kind == NodeKind::Array) ? structSpan(c.id, childMap) : c.byteSize(); int end = c.offset + sz; if (end > maxEnd) maxEnd = end; } return maxEnd; } // Batch selection normalizers QSet normalizePreferAncestors(const QSet& ids) const; QSet normalizePreferDescendants(const QSet& ids) const; QJsonObject toJson() const { QJsonObject o; o["baseAddress"] = QString::number(baseAddress, 16); o["nextId"] = QString::number(m_nextId); QJsonArray arr; for (const auto& n : nodes) arr.append(n.toJson()); o["nodes"] = arr; return o; } static NodeTree fromJson(const QJsonObject& o) { NodeTree t; t.baseAddress = o["baseAddress"].toString("400000").toULongLong(nullptr, 16); t.m_nextId = o["nextId"].toString("1").toULongLong(); QJsonArray arr = o["nodes"].toArray(); for (const auto& v : arr) { Node n = Node::fromJson(v.toObject()); t.nodes.append(n); if (n.id >= t.m_nextId) t.m_nextId = n.id + 1; } return t; } }; // ── LineMeta ── enum class LineKind : uint8_t { Header, Field, Continuation, Footer, ArrayElementSeparator }; struct LineMeta { int nodeIdx = -1; uint64_t nodeId = 0; int subLine = 0; int depth = 0; int foldLevel = 0; bool foldHead = false; bool foldCollapsed = false; bool isContinuation = false; bool isRootHeader = false; // true for top-level struct headers (base address editable) bool isArrayHeader = false; // true for array headers (has nav) LineKind lineKind = LineKind::Field; NodeKind nodeKind = NodeKind::Int32; NodeKind elementKind = NodeKind::UInt8; // Array element type int arrayViewIdx = 0; // Array: current view index int arrayCount = 0; // Array: total element count int arrayElementIdx = -1; // Index of this element within parent array (-1 if not array element) QString offsetText; uint32_t markerMask = 0; int effectiveTypeW = 14; // Per-line type column width used for rendering int effectiveNameW = 22; // Per-line name column width used for rendering }; // ── Layout Info ── struct LayoutInfo { int typeW = 14; // Effective type column width (default = kColType) int nameW = 22; // Effective name column width (default = kColName) }; // ── ComposeResult ── struct ComposeResult { QString text; QVector meta; LayoutInfo layout; }; // ── Command ── namespace cmd { struct OffsetAdj { uint64_t nodeId; int oldOffset, newOffset; }; struct ChangeKind { uint64_t nodeId; NodeKind oldKind, newKind; QVector offAdjs; }; struct Rename { uint64_t nodeId; QString oldName, newName; }; struct Collapse { uint64_t nodeId; bool oldState, newState; }; struct Insert { Node node; }; struct Remove { uint64_t nodeId; QVector subtree; QVector offAdjs; }; struct ChangeBase { uint64_t oldBase, newBase; }; struct WriteBytes { uint64_t addr; QByteArray oldBytes, newBytes; }; } using Command = std::variant< cmd::ChangeKind, cmd::Rename, cmd::Collapse, cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes >; // ── Column spans (for inline editing) ── struct ColumnSpan { int start = 0; // inclusive column index int end = 0; // exclusive column index bool valid = false; }; enum class EditTarget { Name, Type, Value, BaseAddress, ArrayIndex, ArrayCount }; // Column layout constants (shared with format.cpp span computation) inline constexpr int kFoldCol = 3; // 3-char fold indicator prefix per line inline constexpr int kColType = 14; // Max type column width (fits "uint64_t[999]") inline constexpr int kColName = 22; inline constexpr int kColValue = 32; inline constexpr int kColComment = 28; // "// Enter=Save Esc=Cancel" fits inline constexpr int kColBaseAddr = 12; // "0x" + up to 10 hex digits (40-bit address) inline constexpr int kSepWidth = 2; inline constexpr int kMinTypeW = 8; // Minimum type column width (fits "uint64_t") inline constexpr int kMaxTypeW = 14; // Maximum type column width (fits "uint64_t[999]") inline constexpr int kMinNameW = 8; // Minimum name column width (matches ASCII preview) inline constexpr int kMaxNameW = 22; // Maximum name column width (= kColName) inline ColumnSpan typeSpanFor(const LineMeta& lm, int typeW = kColType) { if (lm.lineKind != LineKind::Field || lm.isContinuation) return {}; int ind = kFoldCol + lm.depth * 3; return {ind, ind + typeW, true}; } inline ColumnSpan nameSpanFor(const LineMeta& lm, int typeW = kColType, int nameW = kColName) { if (lm.isContinuation || lm.lineKind != LineKind::Field) return {}; 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)) return {start, start + 8, true}; return {start, start + nameW, true}; } inline ColumnSpan valueSpanFor(const LineMeta& lm, int /*lineLength*/, int typeW = kColType, int nameW = kColName) { if (lm.lineKind == LineKind::Header || lm.lineKind == LineKind::Footer || 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 if (lm.isContinuation) { int prefixW = isHexPad ? (typeW + kSepWidth + 8 + kSepWidth) : (typeW + nameW + 4); int start = ind + prefixW; return {start, start + valWidth, true}; } if (lm.lineKind != LineKind::Field) return {}; int start = isHexPad ? (ind + typeW + kSepWidth + 8 + kSepWidth) : (ind + typeW + kSepWidth + nameW + kSepWidth); return {start, start + valWidth, true}; } inline ColumnSpan commentSpanFor(const LineMeta& lm, int lineLength, int typeW = kColType, int nameW = kColName) { 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; int start; if (lm.isContinuation) { int prefixW = isHexPad ? (typeW + kSepWidth + 8 + kSepWidth) : (typeW + nameW + 4); start = ind + prefixW + valWidth; } else { start = isHexPad ? (ind + typeW + kSepWidth + 8 + kSepWidth + valWidth) : (ind + typeW + kSepWidth + nameW + kSepWidth + valWidth); } return {start, lineLength, start < lineLength}; } // Base address span (only valid for root struct headers) // Line format: " - struct Name { base: 0x00400000" inline ColumnSpan baseAddressSpanFor(const LineMeta& lm, const QString& lineText) { if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {}; // Find "base: " after the opening brace int baseIdx = lineText.indexOf(QStringLiteral("base: ")); if (baseIdx < 0) return {}; int startPos = baseIdx + 6; // after "base: " // Value goes to end of line int endPos = lineText.size(); while (endPos > startPos && lineText[endPos-1].isSpace()) endPos--; if (endPos <= startPos) return {}; return {startPos, endPos, true}; } // Full "base: 0x..." span for coloring (includes "base: " prefix) inline ColumnSpan baseAddressFullSpanFor(const LineMeta& lm, const QString& lineText) { if (lm.lineKind != LineKind::Header || !lm.isRootHeader) return {}; int baseIdx = lineText.indexOf(QStringLiteral("base: ")); if (baseIdx < 0) return {}; int endPos = lineText.size(); while (endPos > baseIdx && lineText[endPos-1].isSpace()) endPos--; return {baseIdx, endPos, true}; } // ── Array navigation spans ── // Line format: "uint32_t[16] name { <0/16>" inline ColumnSpan arrayPrevSpanFor(const LineMeta& lm, const QString& lineText) { if (!lm.isArrayHeader) return {}; int lt = lineText.lastIndexOf('<'); if (lt < 0) return {}; return {lt, lt + 1, true}; } inline ColumnSpan arrayIndexSpanFor(const LineMeta& lm, const QString& lineText) { if (!lm.isArrayHeader) return {}; int lt = lineText.lastIndexOf('<'); int slash = lineText.indexOf('/', lt); if (lt < 0 || slash < 0) return {}; return {lt + 1, slash, true}; } inline ColumnSpan arrayCountSpanFor(const LineMeta& lm, const QString& lineText) { if (!lm.isArrayHeader) return {}; int slash = lineText.lastIndexOf('/'); int gt = lineText.indexOf('>', slash); if (slash < 0 || gt < 0) return {}; return {slash + 1, gt, true}; } inline ColumnSpan arrayNextSpanFor(const LineMeta& lm, const QString& lineText) { if (!lm.isArrayHeader) return {}; int gt = lineText.lastIndexOf('>'); if (gt < 0) return {}; return {gt, gt + 1, true}; } // ── ViewState ── struct ViewState { int scrollLine = 0; int cursorLine = 0; int cursorCol = 0; }; // ── Format function forward declarations ── namespace fmt { using TypeNameFn = QString (*)(NodeKind); void setTypeNameProvider(TypeNameFn fn); QString typeName(NodeKind kind, int colType = kColType); QString typeNameRaw(NodeKind kind); // Unpadded type name for width calculation QString fmtInt8(int8_t v); QString fmtInt16(int16_t v); QString fmtInt32(int32_t v); QString fmtInt64(int64_t v); QString fmtUInt8(uint8_t v); QString fmtUInt16(uint16_t v); QString fmtUInt32(uint32_t v); QString fmtUInt64(uint64_t v); QString fmtFloat(float v); QString fmtDouble(double v); QString fmtBool(uint8_t v); QString fmtPointer32(uint32_t v); QString fmtPointer64(uint64_t v); QString fmtNodeLine(const Node& node, const Provider& prov, uint64_t addr, int depth, int subLine = 0, const QString& comment = {}, int colType = kColType, int colName = kColName); QString fmtOffsetMargin(int64_t relativeOffset, bool isContinuation); QString fmtStructHeader(const Node& node, int depth); QString fmtStructHeaderWithBase(const Node& node, int depth, uint64_t baseAddress); QString fmtStructFooter(const Node& node, int depth, int totalSize = -1); QString fmtArrayHeader(const Node& node, int depth, int viewIdx); QString arrayTypeName(NodeKind elemKind, int count); QString validateBaseAddress(const QString& text); QString indent(int depth); QString readValue(const Node& node, const Provider& prov, uint64_t addr, int subLine); QString editableValue(const Node& node, const Provider& prov, uint64_t addr, int subLine); QByteArray parseValue(NodeKind kind, const QString& text, bool* ok); QByteArray parseAsciiValue(const QString& text, int expectedSize, bool* ok); QString validateValue(NodeKind kind, const QString& text); } // namespace fmt // ── Compose function forward declaration ── ComposeResult compose(const NodeTree& tree, const Provider& prov); } // namespace rcx