diff --git a/src/imports/import_pdb.cpp b/src/imports/import_pdb.cpp index c4db6eb..d16c5bb 100644 --- a/src/imports/import_pdb.cpp +++ b/src/imports/import_pdb.cpp @@ -1022,21 +1022,26 @@ PdbSymbolResult extractPdbSymbols(const QString& pdbPath, QString* errorMsg) { const char* name = nullptr; uint32_t rva = 0u; + uint32_t typeIdx = 0u; if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GDATA32) { name = record->data.S_GDATA32.name; + typeIdx = record->data.S_GDATA32.typeIndex; rva = imageSectionStream.ConvertSectionOffsetToRVA( record->data.S_GDATA32.section, record->data.S_GDATA32.offset); } else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_GTHREAD32) { name = record->data.S_GTHREAD32.name; + typeIdx = record->data.S_GTHREAD32.typeIndex; rva = imageSectionStream.ConvertSectionOffsetToRVA( record->data.S_GTHREAD32.section, record->data.S_GTHREAD32.offset); } else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LDATA32) { name = record->data.S_LDATA32.name; + typeIdx = record->data.S_LDATA32.typeIndex; rva = imageSectionStream.ConvertSectionOffsetToRVA( record->data.S_LDATA32.section, record->data.S_LDATA32.offset); } else if (record->header.kind == PDB::CodeView::DBI::SymbolRecordKind::S_LTHREAD32) { name = record->data.S_LTHREAD32.name; + typeIdx = record->data.S_LTHREAD32.typeIndex; rva = imageSectionStream.ConvertSectionOffsetToRVA( record->data.S_LTHREAD32.section, record->data.S_LTHREAD32.offset); } @@ -1046,7 +1051,7 @@ PdbSymbolResult extractPdbSymbols(const QString& pdbPath, QString* errorMsg) { if (!name || name[0] == '\0') continue; - result.symbols.push_back(PdbSymbol{QString::fromUtf8(name), rva}); + result.symbols.push_back(PdbSymbol{QString::fromUtf8(name), rva, typeIdx}); } } @@ -1232,6 +1237,119 @@ NodeTree importPdb(const QString& pdbPath, const QString& structFilter, QString* return ctx.tree; } +// ── Public API: importTypeForSymbol ── + +NodeTree importTypeForSymbol(const QString& pdbPath, + uint32_t typeIndex, + QString* typeName, + QString* errorMsg) { + auto setErr = [&](const QString& msg) { if (errorMsg) *errorMsg = msg; }; + + if (typeIndex == 0) { + setErr(QStringLiteral("Symbol has no associated type (typeIndex=0)")); + return {}; + } + + PdbFile pdb; + if (!pdb.open(pdbPath, errorMsg)) return {}; + + const TypeTable& tt = *pdb.typeTable; + + // Walk through LF_MODIFIER and LF_POINTER chains to find the underlying UDT/enum + uint32_t ti = typeIndex; + int depth = 0; + while (ti >= tt.firstIndex() && depth < 16) { + const auto* rec = tt.get(ti); + if (!rec) break; + + if (rec->header.kind == TRK::LF_MODIFIER) { + ti = rec->data.LF_MODIFIER.type; + depth++; + continue; + } + if (rec->header.kind == TRK::LF_POINTER) { + ti = rec->data.LF_POINTER.utype; + depth++; + continue; + } + break; // reached a non-wrapper type + } + + // Check if we landed on a UDT or enum + if (ti < tt.firstIndex()) { + setErr(QStringLiteral("Symbol type resolves to a primitive (typeIndex %1)") + .arg(typeIndex)); + return {}; + } + + const auto* rec = tt.get(ti); + if (!rec) { + setErr(QStringLiteral("Invalid type index %1").arg(ti)); + return {}; + } + + bool isUDT = (rec->header.kind == TRK::LF_STRUCTURE || + rec->header.kind == TRK::LF_CLASS || + rec->header.kind == TRK::LF_UNION); + bool isEnum = (rec->header.kind == TRK::LF_ENUM); + + if (!isUDT && !isEnum) { + setErr(QStringLiteral("Symbol type is not a struct/class/union/enum (kind 0x%1)") + .arg((uint16_t)rec->header.kind, 0, 16)); + return {}; + } + + // Extract the type name for the caller + if (typeName) { + const char* name = nullptr; + if (isEnum) { + name = rec->data.LF_ENUM.name; + } else if (rec->header.kind == TRK::LF_UNION) { + name = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data)); + } else { + name = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind); + } + if (name) *typeName = QString::fromUtf8(name); + } + + // If this is a forward reference, resolve to the full definition + PdbCtx ctx; + ctx.tt = &tt; + + if (isUDT) { + bool fwdref = false; + if (rec->header.kind == TRK::LF_UNION) + fwdref = rec->data.LF_UNION.property.fwdref; + else + fwdref = rec->data.LF_CLASS.property.fwdref; + + if (fwdref) { + // Build the definition index to find the real definition + ctx.buildUdtDefinitionIndex(); + const char* name = nullptr; + if (rec->header.kind == TRK::LF_UNION) + name = leafName(rec->data.LF_UNION.data, unionLeafKind(rec->data.LF_UNION.data)); + else + name = leafName(rec->data.LF_CLASS.data, rec->data.LF_CLASS.lfEasy.kind); + + if (name) { + uint32_t defTi = ctx.findUdtDefinitionIndex(rec->header.kind, name); + if (defTi != 0) + ti = defTi; + } + } + ctx.importUDT(ti); + } else { + ctx.importEnum(ti); + } + + if (ctx.tree.nodes.isEmpty()) { + setErr(QStringLiteral("Failed to import type at index %1").arg(ti)); + } + + return ctx.tree; +} + } // namespace rcx #else // !_WIN32 @@ -1259,6 +1377,11 @@ NodeTree importPdb(const QString&, const QString&, QString* errorMsg) { return {}; } +NodeTree importTypeForSymbol(const QString&, uint32_t, QString*, QString* errorMsg) { + if (errorMsg) *errorMsg = QStringLiteral("PDB import requires Windows"); + return {}; +} + } // namespace rcx #endif diff --git a/src/imports/import_pdb.h b/src/imports/import_pdb.h index f2f4f23..42bd685 100644 --- a/src/imports/import_pdb.h +++ b/src/imports/import_pdb.h @@ -10,6 +10,7 @@ namespace rcx { struct PdbSymbol { QString name; uint32_t rva; + uint32_t typeIndex = 0; // TPI type index (0 = unknown / public symbol) }; struct PdbSymbolResult { @@ -51,4 +52,12 @@ NodeTree importPdb(const QString& pdbPath, const QString& structFilter = {}, QString* errorMsg = nullptr); +// Import the type associated with a global symbol's typeIndex. +// Opens the PDB, resolves the typeIndex to a UDT/enum, and returns the imported tree. +// Returns empty tree if the symbol has no associated type or the type is a simple primitive. +NodeTree importTypeForSymbol(const QString& pdbPath, + uint32_t typeIndex, + QString* typeName = nullptr, + QString* errorMsg = nullptr); + } // namespace rcx diff --git a/src/main.cpp b/src/main.cpp index fb34341..1f9e556 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1025,8 +1025,8 @@ protected: const double r = 0.75, s = 3.0; double cx = width() / 2.0; double cy = height() / 2.0; - // 2 columns x 3 rows, centered - for (int row = -1; row <= 1; row++) { + // 2 columns x 4 rows, centered + for (int row = -2; row <= 1; row++) { p.drawEllipse(QPointF(cx - s * 0.5, cy + row * s), r, r); p.drawEllipse(QPointF(cx + s * 0.5, cy + row * s), r, r); } @@ -4776,6 +4776,8 @@ void MainWindow::createSymbolsDock() { m_symDownloadBtn = new QToolButton(titleBar); m_symDownloadBtn->setIcon(QIcon(QStringLiteral(":/vsicons/cloud-download.svg"))); m_symDownloadBtn->setIconSize(QSize(14, 14)); + m_symDownloadBtn->setText(QStringLiteral("Download All")); + m_symDownloadBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); m_symDownloadBtn->setAutoRaise(true); m_symDownloadBtn->setCursor(Qt::PointingHandCursor); m_symDownloadBtn->setToolTip(QStringLiteral("Load/Download all symbols")); @@ -4892,17 +4894,10 @@ void MainWindow::createSymbolsDock() { return; } - // Helper to load a PDB file into the symbol store + // Helper to load a PDB file into the symbol store (with type indices) auto loadPdb = [this, name](const QString& pdbPath) -> bool { - QString symErr; - auto result = rcx::extractPdbSymbols(pdbPath, &symErr); - if (result.symbols.isEmpty()) return false; - QVector> pairs; - pairs.reserve(result.symbols.size()); - for (const auto& s : result.symbols) - pairs.emplaceBack(s.name, s.rva); - int count = rcx::SymbolStore::instance().addModule( - result.moduleName, pdbPath, pairs); + int count = loadPdbIntoStore(pdbPath); + if (count <= 0) return false; setAppStatus(QStringLiteral("Loaded %1 symbols for %2").arg(count).arg(name)); rebuildSymbolsModel(); if (auto* c = activeController()) c->refresh(); @@ -5341,6 +5336,28 @@ void MainWindow::createSymbolsDock() { } } +int MainWindow::loadPdbIntoStore(const QString& pdbPath) { + QString symErr; + auto result = rcx::extractPdbSymbols(pdbPath, &symErr); + if (result.symbols.isEmpty()) return 0; + + QVector> pairs; + QHash typeIndices; + pairs.reserve(result.symbols.size()); + for (const auto& s : result.symbols) { + pairs.emplaceBack(s.name, s.rva); + if (s.typeIndex != 0) + typeIndices.insert(s.name, s.typeIndex); + } + + int count = rcx::SymbolStore::instance().addModule( + result.moduleName, pdbPath, pairs); + if (!typeIndices.isEmpty()) + rcx::SymbolStore::instance().addModuleTypeIndices( + result.moduleName, typeIndices); + return count; +} + void MainWindow::rebuildSymbolsModel() { if (!m_symbolsModel) return; m_symbolsModel->clear(); diff --git a/src/mainwindow.h b/src/mainwindow.h index 5a7a097..09f4062 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -221,6 +221,8 @@ private: void rebuildSymbolsModel(); void rebuildModulesModel(); void downloadSymbolsForProcess(); + // Load PDB symbols + typeIndices into SymbolStore. Returns symbol count. + static int loadPdbIntoStore(const QString& pdbPath); // Start page StartPageWidget* m_startPage = nullptr; diff --git a/src/mcp/mcp_bridge.cpp b/src/mcp/mcp_bridge.cpp index 34385bd..0e01608 100644 --- a/src/mcp/mcp_bridge.cpp +++ b/src/mcp/mcp_bridge.cpp @@ -5,7 +5,10 @@ #include "generator.h" #include "mainwindow.h" #include "scanner.h" +#include "symbolstore.h" +#include "imports/import_pdb.h" #include +#include #include #include #include @@ -323,8 +326,8 @@ QJsonObject McpBridge::handleInitialize(const QJsonValue& id, const QJsonObject& "- To detect what changed after an in-game event: call ui.action with action:'reset_tracking', " "then have the user perform the action, then call node.history on the relevant nodes " "to see which ones have new timestamped entries.\n" - "- hex.read offset is relative to the struct base address by default. " - "Use baseRelative=true for absolute virtual addresses in the process.\n" + "- hex.read offset is an absolute virtual address by default. " + "Use baseRelative=true to make it relative to the struct base address (0 = start of struct).\n" "- tree.apply operations are atomic (undo macro). Batch related changes into one call.\n" "- Use tree.search to quickly find nodes by name instead of paging through project.state.\n" "- project.state returns structure metadata only (kinds, names, offsets), NOT live values. " @@ -385,7 +388,11 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { "Operations: " "remove: {op:'remove', nodeId:'ID'}. " "rename: {op:'rename', nodeId:'ID', name:'newName'}. " - "insert: {op:'insert', kind:'Hex64', name:'field', parentId:'ID', offset:0}. " + "insert: {op:'insert', kind:'Hex64', name:'field', parentId:'ID', offset:0} — " + "optional fields: structTypeName, classKeyword, strLen, elementKind, arrayLen, refId, " + "ptrDepth (0=struct ptr, 1=prim*, 2=prim**), isStatic (bool), offsetExpr (string), " + "isRelative (bool, RVA pointer), " + "enumMembers ([{name:'X',value:0},...]), bitfieldMembers ([{name:'X',bitOffset:0,bitWidth:1},...]). " "change_kind: {op:'change_kind', nodeId:'ID', kind:'UInt32'}. " "change_offset: {op:'change_offset', nodeId:'ID', offset:16}. " "change_base: {op:'change_base', baseAddress:'0x400000', formula:'[0x233CA80]'} — formula is optional, enables auto-resolve on provider attach. " @@ -394,9 +401,15 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { "change_pointer_ref: {op:'change_pointer_ref', nodeId:'ID', refId:'targetID'}. " "change_array_meta: {op:'change_array_meta', nodeId:'ID', elementKind:'UInt32', arrayLen:10}. " "collapse: {op:'collapse', nodeId:'ID', collapsed:true}. " + "change_enum_members: {op:'change_enum_members', nodeId:'ID', members:[{name:'X',value:0},...]}. " + "change_offset_expr: {op:'change_offset_expr', nodeId:'ID', offsetExpr:'base + 0x10'}. " + "toggle_static: {op:'toggle_static', nodeId:'ID', isStatic:true}. " + "toggle_relative: {op:'toggle_relative', nodeId:'ID', isRelative:true}. " + "group_into_union: {op:'group_into_union', nodeIds:['ID1','ID2',...]} — groups siblings into a union. " + "dissolve_union: {op:'dissolve_union', nodeId:'ID'} — flattens a union back to parent scope. " "Insert ops get auto-assigned IDs; use $0, $1 etc. to reference them in later ops. " "Kinds: Hex8 Hex16 Hex32 Hex64 Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 " - "Float Double Bool Pointer32 Pointer64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Struct Array"}, + "Float Double Bool Pointer32 Pointer64 FuncPtr32 FuncPtr64 Vec2 Vec3 Vec4 Mat4x4 UTF8 UTF16 Struct Array"}, {"inputSchema", QJsonObject{ {"type", "object"}, {"properties", QJsonObject{ @@ -451,16 +464,20 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { {"description", "Read raw bytes from provider (live process memory). Returns hex dump, ASCII, and multi-type " "interpretations (u8/u16/u32/u64/i32/f32/f64/ptr/string heuristics). " "Use this to see what actual values are in memory at any offset. " - "Offset is tree-relative (0-based, baseAddress added automatically) " - "unless baseRelative=true (offset is absolute virtual address in the process)."}, + "By default offset is an absolute virtual address in the target process. " + "Set baseRelative=true to make offset relative to the struct base address " + "(e.g. offset=0 reads at baseAddress, offset=0x10 reads at baseAddress+0x10)."}, {"inputSchema", QJsonObject{ {"type", "object"}, {"properties", QJsonObject{ {"tabIndex", QJsonObject{{"type", "integer"}, {"description", "MDI tab index (0-based). Omit for active tab."}}}, - {"offset", QJsonObject{{"type", "integer"}}}, - {"length", QJsonObject{{"type", "integer"}}}, - {"baseRelative", QJsonObject{{"type", "boolean"}}} + {"offset", QJsonObject{{"type", "integer"}, + {"description", "Address to read from. Absolute VA by default, or relative to struct base if baseRelative=true."}}}, + {"length", QJsonObject{{"type", "integer"}, + {"description", "Number of bytes to read (1-4096, default 64)."}}}, + {"baseRelative", QJsonObject{{"type", "boolean"}, + {"description", "If true, offset is relative to the tree's base address (added automatically). Default false (offset is absolute VA)."}}} }}, {"required", QJsonArray{"offset", "length"}} }} @@ -469,15 +486,20 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { // 5. hex.write tools.append(QJsonObject{ {"name", "hex.write"}, - {"description", "Write raw bytes to provider (through undo stack). Hex string format: '4D5A9000'"}, + {"description", "Write raw bytes to provider (through undo stack). Hex string format: '4D5A9000'. " + "By default offset is an absolute virtual address. " + "Set baseRelative=true to make offset relative to the struct base address."}, {"inputSchema", QJsonObject{ {"type", "object"}, {"properties", QJsonObject{ {"tabIndex", QJsonObject{{"type", "integer"}, {"description", "MDI tab index (0-based). Omit for active tab."}}}, - {"offset", QJsonObject{{"type", "integer"}}}, - {"hexBytes", QJsonObject{{"type", "string"}}}, - {"baseRelative", QJsonObject{{"type", "boolean"}}} + {"offset", QJsonObject{{"type", "integer"}, + {"description", "Address to write to. Absolute VA by default, or relative to struct base if baseRelative=true."}}}, + {"hexBytes", QJsonObject{{"type", "string"}, + {"description", "Hex byte string to write, e.g. '4D5A9000'. Spaces allowed."}}}, + {"baseRelative", QJsonObject{{"type", "boolean"}, + {"description", "If true, offset is relative to the tree's base address. Default false (absolute VA)."}}} }}, {"required", QJsonArray{"offset", "hexBytes"}} }} @@ -650,6 +672,86 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { }} }} }); + + // symbols.load + tools.append(QJsonObject{ + {"name", "symbols.load"}, + {"description", "Load PDB symbols from a file path into the global symbol store. " + "Symbols are used for address annotations (e.g. 'ntdll!RtlInitUnicodeString') " + "and can be resolved via symbols.lookup. " + "Returns the number of symbols loaded and the module name."}, + {"inputSchema", QJsonObject{ + {"type", "object"}, + {"properties", QJsonObject{ + {"pdbPath", QJsonObject{{"type", "string"}, + {"description", "Absolute path to a .pdb file."}}}, + {"tabIndex", QJsonObject{{"type", "integer"}, + {"description", "MDI tab index (0-based). Omit for active tab. Used to refresh annotations after loading."}}} + }}, + {"required", QJsonArray{"pdbPath"}} + }} + }); + + // symbols.lookup + tools.append(QJsonObject{ + {"name", "symbols.lookup"}, + {"description", "Resolve a symbol name to an absolute virtual address in the attached process. " + "Supports qualified names like 'ntdll!RtlInitUnicodeString' and bare names. " + "Requires symbols to be loaded (via symbols.load or the UI) and a live provider."}, + {"inputSchema", QJsonObject{ + {"type", "object"}, + {"properties", QJsonObject{ + {"symbol", QJsonObject{{"type", "string"}, + {"description", "Symbol to resolve. Use 'module!name' for qualified lookup, or bare 'name' for unqualified."}}}, + {"tabIndex", QJsonObject{{"type", "integer"}, + {"description", "MDI tab index (0-based). Omit for active tab."}}} + }}, + {"required", QJsonArray{"symbol"}} + }} + }); + + // symbols.importType + tools.append(QJsonObject{ + {"name", "symbols.importType"}, + {"description", "Import the type definition for a global symbol from its PDB into the active project. " + "Given a qualified symbol like 'ntdll!g_pShimEngineModule', resolves its typeIndex from " + "the PDB, follows pointer/modifier chains to find the underlying struct/class/union/enum, " + "and imports it with full recursive child types. " + "Requires symbols to be loaded first (via symbols.load). " + "Returns the imported type name and node count, or an error if the symbol has no type info."}, + {"inputSchema", QJsonObject{ + {"type", "object"}, + {"properties", QJsonObject{ + {"symbol", QJsonObject{{"type", "string"}, + {"description", "Qualified symbol name (e.g. 'ntdll!g_pShimEngineModule'). Must include 'module!' prefix."}}}, + {"tabIndex", QJsonObject{{"type", "integer"}, + {"description", "MDI tab index (0-based). Omit for active tab."}}} + }}, + {"required", QJsonArray{"symbol"}} + }} + }); + + // node.read_value + tools.append(QJsonObject{ + {"name", "node.read_value"}, + {"description", "Read the formatted typed value for one or more nodes. Unlike hex.read (which returns raw bytes), " + "this returns the value as the user sees it in the editor: e.g. '120.0f' for Float, '0x7FF61234' for Pointer64, " + "'true' for Bool, '1.0, 2.0, 3.0' for Vec3. " + "For Hex nodes returns the hex byte preview. For Struct/Array returns the computed size. " + "Requires a live provider for meaningful values."}, + {"inputSchema", QJsonObject{ + {"type", "object"}, + {"properties", QJsonObject{ + {"nodeIds", QJsonObject{{"type", "array"}, + {"items", QJsonObject{{"type", "string"}}}, + {"description", "Array of node IDs to read values for."}}}, + {"tabIndex", QJsonObject{{"type", "integer"}, + {"description", "MDI tab index (0-based). Omit for active tab."}}} + }}, + {"required", QJsonArray{"nodeIds"}} + }} + }); + return okReply(id, QJsonObject{{"tools", tools}}); } @@ -680,6 +782,10 @@ QJsonObject McpBridge::handleToolsCall(const QJsonValue& id, const QJsonObject& else if (toolName == "scanner.scan_pattern") result = toolScannerScanPattern(args); else if (toolName == "mcp.reconnect") result = toolReconnect(args); else if (toolName == "process.info") result = toolProcessInfo(args); + else if (toolName == "symbols.load") result = toolSymbolsLoad(args); + else if (toolName == "symbols.lookup") result = toolSymbolsLookup(args); + else if (toolName == "symbols.importType") result = toolSymbolsImportType(args); + else if (toolName == "node.read_value") result = toolNodeReadValue(args); else return errReply(id, -32601, "Unknown tool: " + toolName); m_mainWindow->clearMcpStatus(); @@ -965,6 +1071,31 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) { n.strLen = qBound(1, (int)parseInteger(op.value("strLen"), 64), 1000000); n.elementKind = kindFromString(op.value("elementKind").toString("UInt8")); n.arrayLen = qBound(1, (int)parseInteger(op.value("arrayLen"), 1), 1000000); + n.ptrDepth = qBound(0, (int)parseInteger(op.value("ptrDepth"), 0), 2); + n.isStatic = op.value("isStatic").toBool(false); + n.offsetExpr = op.value("offsetExpr").toString(); + n.isRelative = op.value("isRelative").toBool(false); + // Enum members + if (op.contains("enumMembers")) { + QJsonArray emArr = op.value("enumMembers").toArray(); + for (const auto& ev : emArr) { + QJsonObject eo = ev.toObject(); + n.enumMembers.emplaceBack(eo.value("name").toString(), + (int64_t)parseInteger(eo.value("value"))); + } + } + // Bitfield members + if (op.contains("bitfieldMembers")) { + QJsonArray bmArr = op.value("bitfieldMembers").toArray(); + for (const auto& bv : bmArr) { + QJsonObject bo = bv.toObject(); + BitfieldMember bm; + bm.name = bo.value("name").toString(); + bm.bitOffset = (uint8_t)qBound(0, (int)parseInteger(bo.value("bitOffset")), 255); + bm.bitWidth = (uint8_t)qBound(1, (int)parseInteger(bo.value("bitWidth"), 1), 64); + n.bitfieldMembers.append(bm); + } + } bool refOk; QString refStr = resolvePlaceholder(op.value("refId").toString("0"), placeholders, &refOk); if (!refOk) { @@ -1118,6 +1249,89 @@ QJsonObject McpBridge::toolTreeApply(const QJsonObject& args) { skippedOps.append(QStringLiteral("op[%1]: collapse nodeId '%2' not found").arg(i).arg(nid)); } } + else if (opType == "change_enum_members") { + QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders); + int idx = tree.indexOfId(nid.toULongLong()); + if (idx >= 0) { + QVector> newMembers; + QJsonArray membersArr = op.value("members").toArray(); + for (const auto& mv : membersArr) { + QJsonObject mo = mv.toObject(); + newMembers.emplaceBack(mo.value("name").toString(), + (int64_t)parseInteger(mo.value("value"))); + } + doc->undoStack.push(new RcxCommand(ctrl, + cmd::ChangeEnumMembers{tree.nodes[idx].id, + tree.nodes[idx].enumMembers, newMembers})); + applied++; + } else { + skippedOps.append(QStringLiteral("op[%1]: change_enum_members nodeId '%2' not found").arg(i).arg(nid)); + } + } + else if (opType == "change_offset_expr") { + QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders); + int idx = tree.indexOfId(nid.toULongLong()); + if (idx >= 0) { + doc->undoStack.push(new RcxCommand(ctrl, + cmd::ChangeOffsetExpr{tree.nodes[idx].id, + tree.nodes[idx].offsetExpr, + op.value("offsetExpr").toString()})); + applied++; + } else { + skippedOps.append(QStringLiteral("op[%1]: change_offset_expr nodeId '%2' not found").arg(i).arg(nid)); + } + } + else if (opType == "toggle_static") { + QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders); + int idx = tree.indexOfId(nid.toULongLong()); + if (idx >= 0) { + bool newVal = op.value("isStatic").toBool(); + doc->undoStack.push(new RcxCommand(ctrl, + cmd::ToggleStatic{tree.nodes[idx].id, + tree.nodes[idx].isStatic, newVal})); + applied++; + } else { + skippedOps.append(QStringLiteral("op[%1]: toggle_static nodeId '%2' not found").arg(i).arg(nid)); + } + } + else if (opType == "toggle_relative") { + QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders); + int idx = tree.indexOfId(nid.toULongLong()); + if (idx >= 0) { + bool newVal = op.value("isRelative").toBool(); + doc->undoStack.push(new RcxCommand(ctrl, + cmd::ToggleRelative{tree.nodes[idx].id, + tree.nodes[idx].isRelative, newVal})); + applied++; + } else { + skippedOps.append(QStringLiteral("op[%1]: toggle_relative nodeId '%2' not found").arg(i).arg(nid)); + } + } + else if (opType == "group_into_union") { + QJsonArray idsArr = op.value("nodeIds").toArray(); + QSet ids; + for (const auto& v : idsArr) { + QString resolved = resolvePlaceholder(v.toString(), placeholders); + ids.insert(resolved.toULongLong()); + } + if (ids.size() >= 2) { + ctrl->groupIntoUnion(ids); + applied++; + } else { + skippedOps.append(QStringLiteral("op[%1]: group_into_union needs >= 2 nodeIds").arg(i)); + } + } + else if (opType == "dissolve_union") { + QString nid = resolvePlaceholder(op.value("nodeId").toString(), placeholders); + uint64_t unionId = nid.toULongLong(); + int idx = tree.indexOfId(unionId); + if (idx >= 0) { + ctrl->dissolveUnion(unionId); + applied++; + } else { + skippedOps.append(QStringLiteral("op[%1]: dissolve_union nodeId '%2' not found").arg(i).arg(nid)); + } + } else { skippedOps.append(QStringLiteral("op[%1]: unknown op '%2'").arg(i).arg(opType)); } @@ -1820,6 +2034,224 @@ QJsonObject McpBridge::toolProcessInfo(const QJsonObject& args) { return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented))); } +// ════════════════════════════════════════════════════════════════════ +// TOOL: symbols.load — load PDB symbols into the global store +// ════════════════════════════════════════════════════════════════════ + +QJsonObject McpBridge::toolSymbolsLoad(const QJsonObject& args) { + QString pdbPath = args.value("pdbPath").toString(); + if (pdbPath.isEmpty()) + return makeTextResult("pdbPath is required", true); + + if (!QFile::exists(pdbPath)) + return makeTextResult("File not found: " + pdbPath, true); + + QString symErr; + auto result = extractPdbSymbols(pdbPath, &symErr); + if (result.symbols.isEmpty()) + return makeTextResult(symErr.isEmpty() + ? QStringLiteral("No symbols found in PDB") : symErr, true); + + QVector> pairs; + QHash typeIndices; + pairs.reserve(result.symbols.size()); + for (const auto& s : result.symbols) { + pairs.emplaceBack(s.name, s.rva); + if (s.typeIndex != 0) + typeIndices.insert(s.name, s.typeIndex); + } + + int count = SymbolStore::instance().addModule(result.moduleName, pdbPath, pairs); + if (!typeIndices.isEmpty()) + SymbolStore::instance().addModuleTypeIndices(result.moduleName, typeIndices); + + // Refresh the active tab so annotations pick up new symbols + auto* tab = resolveTab(args); + if (tab && tab->ctrl) + tab->ctrl->refresh(); + + m_mainWindow->rebuildSymbolsModel(); + + QJsonObject out; + out["moduleName"] = result.moduleName; + out["symbolCount"] = count; + out["pdbPath"] = pdbPath; + return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented))); +} + +// ════════════════════════════════════════════════════════════════════ +// TOOL: symbols.lookup — resolve symbol name to address +// ════════════════════════════════════════════════════════════════════ + +QJsonObject McpBridge::toolSymbolsLookup(const QJsonObject& args) { + QString symbol = args.value("symbol").toString(); + if (symbol.isEmpty()) + return makeTextResult("symbol is required", true); + + auto* tab = resolveTab(args); + if (!tab || !tab->doc->provider) + return makeTextResult("No active tab or provider", true); + + auto* prov = tab->doc->provider.get(); + bool ok = false; + uint64_t addr = SymbolStore::instance().resolve(symbol, prov, &ok); + if (!ok || addr == 0) + return makeTextResult("Symbol not found: " + symbol, true); + + QJsonObject out; + out["symbol"] = symbol; + out["address"] = "0x" + QString::number(addr, 16).toUpper(); + return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented))); +} + +// ════════════════════════════════════════════════════════════════════ +// TOOL: symbols.importType — import type definition for a symbol +// ════════════════════════════════════════════════════════════════════ + +QJsonObject McpBridge::toolSymbolsImportType(const QJsonObject& args) { + QString symbol = args.value("symbol").toString(); + if (symbol.isEmpty()) + return makeTextResult("symbol is required", true); + + int bangIdx = symbol.indexOf('!'); + if (bangIdx <= 0 || bangIdx >= symbol.size() - 1) + return makeTextResult("Symbol must be qualified: 'module!name'", true); + + QString modPart = symbol.left(bangIdx); + + // Look up the typeIndex from the symbol store + uint32_t typeIdx = SymbolStore::instance().typeIndexForSymbol(symbol); + if (typeIdx == 0) + return makeTextResult("No type info for symbol '" + symbol + + "'. The PDB may not have been loaded with symbols.load, or this " + "is a public symbol (S_PUB32) without type metadata.", true); + + // Find the PDB path for this module + QString canonical = SymbolStore::instance().resolveAlias(modPart); + const auto* modData = SymbolStore::instance().moduleData(canonical); + if (!modData || modData->pdbPath.isEmpty()) + return makeTextResult("No PDB path found for module '" + modPart + "'", true); + + // Import the type from the PDB + QString importedTypeName; + QString importErr; + NodeTree importedTree = importTypeForSymbol(modData->pdbPath, typeIdx, + &importedTypeName, &importErr); + if (importedTree.nodes.isEmpty()) + return makeTextResult(importErr.isEmpty() + ? QStringLiteral("Failed to import type for typeIndex %1").arg(typeIdx) + : importErr, true); + + // Merge imported nodes into the active document + auto* tab = resolveTab(args); + if (!tab) + return makeTextResult("No active tab", true); + + auto& tree = tab->doc->tree; + tab->ctrl->setSuppressRefresh(true); + tab->doc->undoStack.beginMacro( + QStringLiteral("Import type for ") + symbol); + + // Map old IDs to new IDs to preserve parent-child relationships + QHash idMap; + for (const auto& node : importedTree.nodes) { + uint64_t newId = tree.reserveId(); + idMap[node.id] = newId; + } + + for (const auto& node : importedTree.nodes) { + Node copy = node; + copy.id = idMap.value(node.id, node.id); + copy.parentId = idMap.value(node.parentId, node.parentId); + if (copy.refId != 0) + copy.refId = idMap.value(node.refId, node.refId); + tab->doc->undoStack.push(new RcxCommand(tab->ctrl, + cmd::Insert{copy})); + } + + tab->doc->undoStack.endMacro(); + tab->ctrl->setSuppressRefresh(false); + tab->ctrl->refresh(); + m_mainWindow->rebuildWorkspaceModel(); + + QJsonObject out; + out["symbol"] = symbol; + out["typeName"] = importedTypeName; + out["typeIndex"] = (int)typeIdx; + out["nodesImported"] = importedTree.nodes.size(); + return makeTextResult(QString::fromUtf8(QJsonDocument(out).toJson(QJsonDocument::Indented))); +} + +// ════════════════════════════════════════════════════════════════════ +// TOOL: node.read_value — read formatted typed values for nodes +// ════════════════════════════════════════════════════════════════════ + +QJsonObject McpBridge::toolNodeReadValue(const QJsonObject& args) { + auto* tab = resolveTab(args); + if (!tab) return makeTextResult("No active tab", true); + + const auto& tree = tab->doc->tree; + auto* prov = tab->doc->provider.get(); + if (!prov) return makeTextResult("No provider", true); + + QJsonArray requestedIds = args.value("nodeIds").toArray(); + if (requestedIds.isEmpty()) + return makeTextResult("nodeIds array is required", true); + + QJsonObject result; + for (const auto& idVal : requestedIds) { + QString idStr = idVal.toString(); + uint64_t nodeId = idStr.toULongLong(); + int idx = tree.indexOfId(nodeId); + if (idx < 0) { + QJsonObject entry; + entry["error"] = "node not found"; + result[idStr] = entry; + continue; + } + + const Node& node = tree.nodes[idx]; + + // Compute absolute address + int64_t signedOff = tree.computeOffset(idx); + uint64_t addr = (signedOff >= 0) + ? tree.baseAddress + static_cast(signedOff) : 0; + + QJsonObject entry; + entry["kind"] = kindToString(node.kind); + entry["name"] = node.name; + entry["offset"] = node.offset; + entry["address"] = "0x" + QString::number(addr, 16).toUpper(); + + if (node.kind == NodeKind::Struct || node.kind == NodeKind::Array) { + // Containers don't have scalar values — return computed size + int span = tree.structSpan(node.id); + entry["computedSize"] = span; + entry["value"] = QStringLiteral("(container, size=0x%1)") + .arg(QString::number(span, 16).toUpper()); + } else if (addr != 0 && prov->isReadable(addr, node.byteSize())) { + // Read formatted value using the same formatting as the editor + int numLines = linesForKind(node.kind); + if (numLines <= 1) { + entry["value"] = fmt::readValue(node, *prov, addr, 0); + } else { + // Multi-line types (Mat4x4): return all sub-lines + QJsonArray lines; + for (int sub = 0; sub < numLines; sub++) + lines.append(fmt::readValue(node, *prov, addr, sub)); + entry["value"] = lines; + } + } else { + entry["value"] = QJsonValue(); + entry["error"] = "not readable"; + } + + result[idStr] = entry; + } + + return makeTextResult(QString::fromUtf8(QJsonDocument(result).toJson(QJsonDocument::Indented))); +} + // ════════════════════════════════════════════════════════════════════ // Notifications (call from MainWindow/Controller hooks) // ════════════════════════════════════════════════════════════════════ diff --git a/src/mcp/mcp_bridge.h b/src/mcp/mcp_bridge.h index eabc46c..eb34ed3 100644 --- a/src/mcp/mcp_bridge.h +++ b/src/mcp/mcp_bridge.h @@ -85,6 +85,10 @@ private: QJsonObject toolScannerScanPattern(const QJsonObject& args); QJsonObject toolReconnect(const QJsonObject& args); QJsonObject toolProcessInfo(const QJsonObject& args); + QJsonObject toolSymbolsLoad(const QJsonObject& args); + QJsonObject toolSymbolsLookup(const QJsonObject& args); + QJsonObject toolSymbolsImportType(const QJsonObject& args); + QJsonObject toolNodeReadValue(const QJsonObject& args); // Helpers QJsonObject makeTextResult(const QString& text, bool isError = false); diff --git a/src/symbolstore.cpp b/src/symbolstore.cpp index f7ad971..6d1256d 100644 --- a/src/symbolstore.cpp +++ b/src/symbolstore.cpp @@ -52,6 +52,26 @@ int SymbolStore::addModule(const QString& moduleName, const QString& pdbPath, return count; } +void SymbolStore::addModuleTypeIndices(const QString& moduleName, + const QHash& nameToTypeIndex) { + QString canonical = resolveAlias(moduleName); + auto it = m_modules.find(canonical); + if (it == m_modules.end()) return; + it->nameToTypeIndex = nameToTypeIndex; +} + +uint32_t SymbolStore::typeIndexForSymbol(const QString& qualifiedSymbol) const { + int bangIdx = qualifiedSymbol.indexOf('!'); + if (bangIdx <= 0 || bangIdx >= qualifiedSymbol.size() - 1) + return 0; + QString modPart = qualifiedSymbol.left(bangIdx); + QString symPart = qualifiedSymbol.mid(bangIdx + 1); + QString canonical = resolveAlias(modPart); + auto modIt = m_modules.find(canonical); + if (modIt == m_modules.end()) return 0; + return modIt->nameToTypeIndex.value(symPart, 0); +} + void SymbolStore::unloadModule(const QString& moduleName) { QString canonical = resolveAlias(moduleName); m_modules.remove(canonical); diff --git a/src/symbolstore.h b/src/symbolstore.h index e1eb545..cdc4999 100644 --- a/src/symbolstore.h +++ b/src/symbolstore.h @@ -14,6 +14,7 @@ struct PdbSymbolSet { QString pdbPath; QString moduleName; // canonical lowercase name (e.g. "ntoskrnl") QHash nameToRva; + QHash nameToTypeIndex; // symbol name → TPI typeIndex (0 = no type info) QVector> rvaToName; // sorted by RVA for binary search void sortRvaIndex() { @@ -35,6 +36,15 @@ public: int addModule(const QString& moduleName, const QString& pdbPath, const QVector>& symbols); + // Store symbol→typeIndex mapping for a previously-added module. + // Called after addModule with the typeIndex data from PdbSymbol records. + void addModuleTypeIndices(const QString& moduleName, + const QHash& nameToTypeIndex); + + // Look up the TPI typeIndex for a qualified symbol (e.g. "ntdll!g_pShimEngineModule"). + // Returns 0 if not found or no type info available. + uint32_t typeIndexForSymbol(const QString& qualifiedSymbol) const; + // Unload symbols for a module. void unloadModule(const QString& moduleName);