From 9c722659014a2e6f4656173b782fd7752ace8137 Mon Sep 17 00:00:00 2001 From: Matty Date: Tue, 3 Mar 2026 11:32:13 -0700 Subject: [PATCH] feat: scanner unknown value + comparison rescan modes, find bar height fix Add Cheat Engine-style scan conditions: Unknown Value captures all aligned addresses as baseline, then Changed/Unchanged/Increased/Decreased narrow results by comparing current vs previous values. Exact Value mode unchanged. Also fix find bar search box height to match buttons and improve MCP bridge instructions. --- src/editor.cpp | 5 +- src/main.cpp | 5 +- src/mcp/mcp_bridge.cpp | 53 ++++++++++++-- src/scanner.cpp | 161 +++++++++++++++++++++++++++++++++-------- src/scanner.h | 60 ++++++++++----- src/scannerpanel.cpp | 122 ++++++++++++++++++++++++------- src/scannerpanel.h | 7 ++ 7 files changed, 323 insertions(+), 90 deletions(-) diff --git a/src/editor.cpp b/src/editor.cpp index d9b6972..6aa472b 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -405,7 +405,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { // Find bar (hidden by default, shown with Ctrl+F) m_findBarContainer = new QWidget(this); auto* fbLayout = new QHBoxLayout(m_findBarContainer); - fbLayout->setContentsMargins(4, 0, 0, 0); + fbLayout->setContentsMargins(4, 1, 4, 1); fbLayout->setSpacing(2); auto* findPrevBtn = new QToolButton(m_findBarContainer); findPrevBtn->setText(QStringLiteral("\u25C0")); @@ -418,6 +418,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) { findCloseBtn->setFixedSize(24, 24); m_findBar = new QLineEdit(m_findBarContainer); m_findBar->setPlaceholderText(QStringLiteral("Find...")); + m_findBar->setFixedHeight(24); fbLayout->addWidget(findPrevBtn); fbLayout->addWidget(findNextBtn); fbLayout->addWidget(findCloseBtn); @@ -889,7 +890,7 @@ void RcxEditor::applyTheme(const Theme& theme) { if (m_findBarContainer) { m_findBar->setStyleSheet( QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;" - " padding: 4px 8px; font-size: 13px; }") + " padding: 2px 6px; font-size: 13px; }") .arg(theme.backgroundAlt.name(), theme.text.name(), theme.border.name())); m_findBarContainer->setStyleSheet( QStringLiteral("QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }" diff --git a/src/main.cpp b/src/main.cpp index 5ef1518..3ed6171 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1125,7 +1125,7 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) { // Find bar with prev/next buttons (hidden by default) pane.findContainer = new QWidget; auto* fcLayout = new QHBoxLayout(pane.findContainer); - fcLayout->setContentsMargins(4, 0, 0, 0); + fcLayout->setContentsMargins(4, 1, 4, 1); fcLayout->setSpacing(2); const auto& fbTheme = ThemeManager::instance().current(); auto* ccPrevBtn = new QToolButton; @@ -1148,9 +1148,10 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) { ccCloseBtn->setStyleSheet(btnCss); pane.findBar = new QLineEdit; pane.findBar->setPlaceholderText("Find..."); + pane.findBar->setFixedHeight(24); pane.findBar->setStyleSheet( QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;" - " padding: 4px 8px; font-size: 13px; }") + " padding: 2px 6px; font-size: 13px; }") .arg(fbTheme.backgroundAlt.name(), fbTheme.text.name(), fbTheme.border.name())); fcLayout->addWidget(ccPrevBtn); fcLayout->addWidget(ccNextBtn); diff --git a/src/mcp/mcp_bridge.cpp b/src/mcp/mcp_bridge.cpp index dae263d..f90f0cd 100644 --- a/src/mcp/mcp_bridge.cpp +++ b/src/mcp/mcp_bridge.cpp @@ -203,7 +203,28 @@ QJsonObject McpBridge::handleInitialize(const QJsonValue& id, const QJsonObject& {"serverInfo", QJsonObject{ {"name", "reclass-mcp"}, {"version", "1.0.0"} - }} + }}, + {"instructions", + "You are connected to ReClass, a live memory structure editor for reverse engineering. " + "You have two types of data available:\n" + "1. STRUCTURE: The node tree defines typed fields (project.state, tree.search, tree.apply). " + "Each node has a kind (the data type: UInt32, Float, Hex64, etc.) and a name.\n" + "2. LIVE DATA: The provider reads real memory from an attached process (hex.read, hex.write). " + "node.history returns timestamped value changes with heat levels (0=static, 1=cold, 2=warm, 3=hot).\n\n" + "CRITICAL RULES:\n" + "- When labeling/identifying a field, ALWAYS change BOTH name AND kind in one tree.apply call. " + "Example: [{op:'rename',nodeId:'X',name:'health'},{op:'change_kind',nodeId:'X',kind:'Int32'}]. " + "A node named 'health' with kind Hex64 is WRONG — the kind must match the actual data type.\n" + "- 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" + "- 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. " + "Use hex.read for actual memory values and node.history for tracking changes over time." + } }; return okReply(id, result); } @@ -219,6 +240,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { tools.append(QJsonObject{ {"name", "project.state"}, {"description", "Returns project state with paginated node tree. " + "NOTE: This returns structure metadata only (kinds, names, offsets), NOT live memory values. " + "Use hex.read to read actual values and node.history to track value changes over time. " "Responses return max 'limit' nodes (default 50). " "Use depth:1 first, then parentId to drill into a struct. " "Enum/bitfield member arrays are omitted by default (counts shown instead); " @@ -249,6 +272,10 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { tools.append(QJsonObject{ {"name", "tree.apply"}, {"description", "Apply batch of tree operations atomically (undo macro). " + "IMPORTANT: When identifying/labeling a field, you MUST use BOTH rename AND change_kind " + "in the same batch. A renamed node still has its original kind (e.g. Hex64) unless you " + "explicitly change it. Example: " + "[{op:'rename',nodeId:'ID',name:'health'},{op:'change_kind',nodeId:'ID',kind:'Int32'}]. " "Each op is a JSON object with an 'op' field for the operation type and 'nodeId' (string) for the target node. " "Operations: " "remove: {op:'remove', nodeId:'ID'}. " @@ -301,10 +328,11 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { // 4. hex.read tools.append(QJsonObject{ {"name", "hex.read"}, - {"description", "Read raw bytes from provider. Returns hex dump, ASCII, and multi-type " + {"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)."}, + "unless baseRelative=true (offset is absolute virtual address in the process)."}, {"inputSchema", QJsonObject{ {"type", "object"}, {"properties", QJsonObject{ @@ -359,8 +387,10 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { {"description", "Trigger a UI action. Fallback for operations without dedicated tools. " "Actions: undo, redo, new_file, open_file, save_file, save_file_as, " "export_cpp, set_view_root, scroll_to_node, collapse_node, expand_node, " - "select_node, refresh. " - "export_cpp accepts optional nodeId to export a single struct (recommended for large projects)."}, + "select_node, refresh, reset_tracking. " + "export_cpp accepts optional nodeId to export a single struct (recommended for large projects). " + "reset_tracking clears all value change histories — use before an in-game event, " + "then check node.history afterward to see what changed."}, {"inputSchema", QJsonObject{ {"type", "object"}, {"properties", QJsonObject{ @@ -399,8 +429,11 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) { // 9. node.history tools.append(QJsonObject{ {"name", "node.history"}, - {"description", "Returns timestamped value change history (up to 10 entries) " - "for specified nodes. Requires live provider with value tracking enabled."}, + {"description", "Returns timestamped value change history (up to 10 entries) for specified nodes. " + "Use this to detect what changed after an in-game event — no need to manually snapshot memory. " + "Each node returns: entries[] with {value, timestamp}, heatLevel (0=static to 3=hot), " + "and uniqueCount. Heat level 3 means the field is actively changing. " + "Requires live provider with value tracking enabled."}, {"inputSchema", QJsonObject{ {"type", "object"}, {"properties", QJsonObject{ @@ -1180,6 +1213,12 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) { return makeTextResult("Selected node " + nodeIdStr); } + if (action == "reset_tracking") { + if (!ctrl) return makeTextResult("No active tab", true); + ctrl->resetChangeTracking(); + return makeTextResult("Value tracking reset. All histories cleared."); + } + return makeTextResult("Unknown action: " + action, true); } diff --git a/src/scanner.cpp b/src/scanner.cpp index 86c74a6..0261963 100644 --- a/src/scanner.cpp +++ b/src/scanner.cpp @@ -347,6 +347,64 @@ int naturalAlignment(ValueType type) { return 1; } +int valueSizeForType(ValueType type) { + switch (type) { + case ValueType::Int8: case ValueType::UInt8: return 1; + case ValueType::Int16: case ValueType::UInt16: return 2; + case ValueType::Int32: case ValueType::UInt32: case ValueType::Float: return 4; + case ValueType::Int64: case ValueType::UInt64: case ValueType::Double: return 8; + case ValueType::Vec2: return 8; + case ValueType::Vec3: return 12; + case ValueType::Vec4: return 16; + default: return 4; + } +} + +// ── Typed comparison for rescan conditions ── + +static int compareTyped(const QByteArray& a, const QByteArray& b, ValueType vt) { + const char* da = a.constData(); + const char* db = b.constData(); + int sz = qMin(a.size(), b.size()); + + switch (vt) { + case ValueType::Int8: + if (sz >= 1) { int8_t va, vb; memcpy(&va, da, 1); memcpy(&vb, db, 1); return (va > vb) - (va < vb); } + break; + case ValueType::UInt8: + if (sz >= 1) { uint8_t va, vb; memcpy(&va, da, 1); memcpy(&vb, db, 1); return (va > vb) - (va < vb); } + break; + case ValueType::Int16: + if (sz >= 2) { int16_t va, vb; memcpy(&va, da, 2); memcpy(&vb, db, 2); return (va > vb) - (va < vb); } + break; + case ValueType::UInt16: + if (sz >= 2) { uint16_t va, vb; memcpy(&va, da, 2); memcpy(&vb, db, 2); return (va > vb) - (va < vb); } + break; + case ValueType::Int32: + if (sz >= 4) { int32_t va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); } + break; + case ValueType::UInt32: + if (sz >= 4) { uint32_t va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); } + break; + case ValueType::Int64: + if (sz >= 8) { int64_t va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); } + break; + case ValueType::UInt64: + if (sz >= 8) { uint64_t va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); } + break; + case ValueType::Float: + if (sz >= 4) { float va, vb; memcpy(&va, da, 4); memcpy(&vb, db, 4); return (va > vb) - (va < vb); } + break; + case ValueType::Double: + if (sz >= 8) { double va, vb; memcpy(&va, da, 8); memcpy(&vb, db, 8); return (va > vb) - (va < vb); } + break; + default: + break; + } + // Fallback: byte comparison + return memcmp(da, db, sz); +} + // ── Scan engine ── ScanEngine::ScanEngine(QObject* parent) @@ -366,13 +424,15 @@ void ScanEngine::abort() { void ScanEngine::start(std::shared_ptr provider, const ScanRequest& req) { if (isRunning()) return; - if (req.pattern.isEmpty()) { - emit error(QStringLiteral("Empty pattern")); - return; - } - if (req.pattern.size() != req.mask.size()) { - emit error(QStringLiteral("Pattern and mask size mismatch")); - return; + if (req.condition != ScanCondition::UnknownValue) { + if (req.pattern.isEmpty()) { + emit error(QStringLiteral("Empty pattern")); + return; + } + if (req.pattern.size() != req.mask.size()) { + emit error(QStringLiteral("Pattern and mask size mismatch")); + return; + } } m_abort.store(false); @@ -400,14 +460,16 @@ QVector ScanEngine::runScan(std::shared_ptr prov, timer.start(); QVector results; + const bool isUnknown = (req.condition == ScanCondition::UnknownValue); - if (!prov || req.pattern.isEmpty()) + if (!prov || (!isUnknown && req.pattern.isEmpty())) return results; auto regions = prov->enumerateRegions(); qDebug() << "[scan] regions:" << regions.size() << " pattern:" << req.pattern.size() << "bytes" << " align:" << req.alignment + << " condition:" << (int)req.condition << " filterExec:" << req.filterExecutable << " filterWrite:" << req.filterWritable; @@ -422,10 +484,11 @@ QVector ScanEngine::runScan(std::shared_ptr prov, regions.append(fallback); } - const int patternLen = req.pattern.size(); - const char* pat = req.pattern.constData(); - const char* msk = req.mask.constData(); + const int patternLen = isUnknown ? req.valueSize : req.pattern.size(); + const char* pat = isUnknown ? nullptr : req.pattern.constData(); + const char* msk = isUnknown ? nullptr : req.mask.constData(); const int alignment = qMax(1, req.alignment); + const int valSize = isUnknown ? req.valueSize : patternLen; // Pre-compute total bytes for progress uint64_t totalBytes = 0; @@ -474,24 +537,38 @@ QVector ScanEngine::runScan(std::shared_ptr prov, int scanEnd = readLen - patternLen; const char* data = chunk.constData(); - for (int i = 0; i <= scanEnd; i += alignment) { - bool match = true; - for (int j = 0; j < patternLen; j++) { - if ((data[i + j] & msk[j]) != (pat[j] & msk[j])) { - match = false; - break; - } - } - if (match) { + if (isUnknown) { + // Unknown value: capture every aligned address + for (int i = 0; i <= scanEnd; i += alignment) { ScanResult r; r.address = region.base + off + (uint64_t)i; - r.regionModule = region.moduleName; - r.scanValue = QByteArray(data + i, qMin(16, readLen - i)); + r.scanValue = QByteArray(data + i, valSize); results.append(r); if (results.size() >= req.maxResults) goto done; } + } else { + // Exact pattern match + for (int i = 0; i <= scanEnd; i += alignment) { + bool match = true; + for (int j = 0; j < patternLen; j++) { + if ((data[i + j] & msk[j]) != (pat[j] & msk[j])) { + match = false; + break; + } + } + if (match) { + ScanResult r; + r.address = region.base + off + (uint64_t)i; + r.regionModule = region.moduleName; + r.scanValue = QByteArray(data + i, qMin(16, readLen - i)); + results.append(r); + + if (results.size() >= req.maxResults) + goto done; + } + } } // Advance with overlap to catch patterns that straddle chunks @@ -522,6 +599,7 @@ done: void ScanEngine::startRescan(std::shared_ptr provider, QVector results, int readSize, + ScanCondition condition, ValueType valueType, const QByteArray& filterPattern, const QByteArray& filterMask) { if (isRunning()) return; @@ -541,14 +619,15 @@ void ScanEngine::startRescan(std::shared_ptr provider, watcher->setFuture(QtConcurrent::run( [this, provider, results = std::move(results), readSize, - filterPattern, filterMask]() mutable { + condition, valueType, filterPattern, filterMask]() mutable { return runRescan(provider, std::move(results), readSize, - filterPattern, filterMask); + condition, valueType, filterPattern, filterMask); })); } QVector ScanEngine::runRescan(std::shared_ptr prov, QVector results, int readSize, + ScanCondition condition, ValueType valueType, const QByteArray& filterPattern, const QByteArray& filterMask) { QElapsedTimer timer; @@ -557,9 +636,17 @@ QVector ScanEngine::runRescan(std::shared_ptr prov, int total = results.size(); if (total == 0 || !prov) return results; - bool hasFilter = !filterPattern.isEmpty(); + bool hasExactFilter = !filterPattern.isEmpty() && condition == ScanCondition::ExactValue; + bool hasComparison = (condition == ScanCondition::Changed || + condition == ScanCondition::Unchanged || + condition == ScanCondition::Increased || + condition == ScanCondition::Decreased); + bool needsFilter = hasExactFilter || hasComparison; + qDebug() << "[rescan] start:" << total << "results, readSize:" << readSize - << "filter:" << (hasFilter ? "yes" : "no"); + << "condition:" << (int)condition + << "exactFilter:" << (hasExactFilter ? "yes" : "no") + << "comparison:" << (hasComparison ? "yes" : "no"); // Save previous values for (auto& r : results) @@ -579,8 +666,8 @@ QVector ScanEngine::runRescan(std::shared_ptr prov, uint64_t totalBytesRead = 0; int i = 0; - // Track which results matched the filter (by original index) - QVector matched(total, !hasFilter); // if no filter, all match + // Track which results matched (by original index) + QVector matched(total, !needsFilter); // if no filter, all match while (i < total && !m_abort.load()) { uint64_t spanBase = results[order[i]].address; @@ -604,8 +691,8 @@ QVector ScanEngine::runRescan(std::shared_ptr prov, int off = (int)(r.address - spanBase); r.scanValue = chunk.mid(off, readSize); - // Apply filter: compare re-read bytes against the new pattern - if (hasFilter) { + // Apply exact-value filter + if (hasExactFilter) { int patLen = filterPattern.size(); if (r.scanValue.size() >= patLen) { bool ok = true; @@ -621,6 +708,18 @@ QVector ScanEngine::runRescan(std::shared_ptr prov, matched[idx] = ok; } } + + // Apply comparison-based filter + if (hasComparison && !r.previousValue.isEmpty()) { + int cmp = compareTyped(r.scanValue, r.previousValue, valueType); + switch (condition) { + case ScanCondition::Changed: matched[idx] = (cmp != 0); break; + case ScanCondition::Unchanged: matched[idx] = (cmp == 0); break; + case ScanCondition::Increased: matched[idx] = (cmp > 0); break; + case ScanCondition::Decreased: matched[idx] = (cmp < 0); break; + default: break; + } + } } chunks++; @@ -637,7 +736,7 @@ QVector ScanEngine::runRescan(std::shared_ptr prov, } // Filter out non-matching results - if (hasFilter) { + if (needsFilter) { QVector filtered; filtered.reserve(total); for (int k = 0; k < total; k++) { diff --git a/src/scanner.h b/src/scanner.h index cfe6a21..9f988a3 100644 --- a/src/scanner.h +++ b/src/scanner.h @@ -10,26 +10,6 @@ namespace rcx { -// ── Scan request / result ── - -struct ScanRequest { - QByteArray pattern; // literal bytes to match - QByteArray mask; // 0xFF = must match, 0x00 = wildcard - - bool filterExecutable = false; // only scan +x regions - bool filterWritable = false; // only scan +w regions - - int alignment = 1; // 1 = every byte, 4 = dword, 8 = qword - int maxResults = 50000; -}; - -struct ScanResult { - uint64_t address; - QString regionModule; - QByteArray scanValue; // cached bytes at scan/update time - QByteArray previousValue; // value before last update -}; - // ── Value scan types ── enum class ValueType { @@ -41,6 +21,40 @@ enum class ValueType { HexBytes }; +// ── Scan condition (Cheat Engine-style) ── + +enum class ScanCondition { + ExactValue, // first scan + rescan: match specific bytes + UnknownValue, // first scan only: capture all aligned addresses + Changed, // rescan: current != previous + Unchanged, // rescan: current == previous + Increased, // rescan: current > previous (numeric) + Decreased // rescan: current < previous (numeric) +}; + +// ── Scan request / result ── + +struct ScanRequest { + QByteArray pattern; // literal bytes to match (empty for UnknownValue) + QByteArray mask; // 0xFF = must match, 0x00 = wildcard + + bool filterExecutable = false; // only scan +x regions + bool filterWritable = false; // only scan +w regions + + int alignment = 1; // 1 = every byte, 4 = dword, 8 = qword + int maxResults = 50000; + + ScanCondition condition = ScanCondition::ExactValue; + int valueSize = 4; // bytes per value (for unknown scans) +}; + +struct ScanResult { + uint64_t address; + QString regionModule; + QByteArray scanValue; // cached bytes at scan/update time + QByteArray previousValue; // value before last update +}; + // ── Pattern parsing ── // Parse IDA-style signature string ("48 8B ?? 05") into pattern + mask. @@ -57,6 +71,9 @@ bool serializeValue(ValueType type, const QString& input, // Natural alignment for a value type (used as default alignment for value scans). int naturalAlignment(ValueType type); +// Byte-size for a value type (used for unknown scans and rescan read size). +int valueSizeForType(ValueType type); + // ── Scan engine ── class ScanEngine : public QObject { @@ -67,6 +84,8 @@ public: void start(std::shared_ptr provider, const ScanRequest& req); void startRescan(std::shared_ptr provider, QVector results, int readSize, + ScanCondition condition = ScanCondition::ExactValue, + ValueType valueType = ValueType::Int32, const QByteArray& filterPattern = {}, const QByteArray& filterMask = {}); void abort(); @@ -82,6 +101,7 @@ private: QVector runScan(std::shared_ptr prov, const ScanRequest& req); QVector runRescan(std::shared_ptr prov, QVector results, int readSize, + ScanCondition condition, ValueType valueType, const QByteArray& filterPattern, const QByteArray& filterMask); diff --git a/src/scannerpanel.cpp b/src/scannerpanel.cpp index 2dca342..9a4b68c 100644 --- a/src/scannerpanel.cpp +++ b/src/scannerpanel.cpp @@ -93,6 +93,18 @@ ScannerPanel::ScannerPanel(QWidget* parent) m_typeCombo->setCurrentIndex(2); // default: int32 inputRow->addWidget(m_typeCombo); + m_condLabel = new QLabel(QStringLiteral("Scan:"), this); + inputRow->addWidget(m_condLabel); + + m_condCombo = new QComboBox(this); + m_condCombo->addItem(QStringLiteral("Exact Value"), (int)ScanCondition::ExactValue); + m_condCombo->addItem(QStringLiteral("Unknown Value"), (int)ScanCondition::UnknownValue); + m_condCombo->addItem(QStringLiteral("Changed"), (int)ScanCondition::Changed); + m_condCombo->addItem(QStringLiteral("Unchanged"), (int)ScanCondition::Unchanged); + m_condCombo->addItem(QStringLiteral("Increased"), (int)ScanCondition::Increased); + m_condCombo->addItem(QStringLiteral("Decreased"), (int)ScanCondition::Decreased); + inputRow->addWidget(m_condCombo); + m_valueLabel = new QLabel(QStringLiteral("Value:"), this); inputRow->addWidget(m_valueLabel); @@ -174,6 +186,8 @@ ScannerPanel::ScannerPanel(QWidget* parent) // ── Initial state: signature mode ── m_typeLabel->hide(); m_typeCombo->hide(); + m_condLabel->hide(); + m_condCombo->hide(); m_valueLabel->hide(); m_valueEdit->hide(); m_execCheck->setChecked(true); @@ -181,6 +195,8 @@ ScannerPanel::ScannerPanel(QWidget* parent) // ── Connections ── connect(m_modeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, &ScannerPanel::onModeChanged); + connect(m_condCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &ScannerPanel::onConditionChanged); connect(m_scanBtn, &QPushButton::clicked, this, &ScannerPanel::onScanClicked); connect(m_updateBtn, &QPushButton::clicked, @@ -251,12 +267,14 @@ void ScannerPanel::setEditorFont(const QFont& font) { m_valueEdit->setFont(font); m_modeCombo->setFont(font); m_typeCombo->setFont(font); + m_condCombo->setFont(font); m_statusLabel->setFont(font); m_scanBtn->setFont(font); m_gotoBtn->setFont(font); m_copyBtn->setFont(font); m_patternLabel->setFont(font); m_typeLabel->setFont(font); + m_condLabel->setFont(font); m_valueLabel->setFont(font); m_execCheck->setFont(font); m_writeCheck->setFont(font); @@ -280,14 +298,27 @@ void ScannerPanel::onModeChanged(int index) { m_typeLabel->setVisible(!isSig); m_typeCombo->setVisible(!isSig); - m_valueLabel->setVisible(!isSig); - m_valueEdit->setVisible(!isSig); + m_condLabel->setVisible(!isSig); + m_condCombo->setVisible(!isSig); + + // Show/hide value input based on condition + auto cond = (ScanCondition)m_condCombo->currentData().toInt(); + bool needsValue = !isSig && (cond == ScanCondition::ExactValue); + m_valueLabel->setVisible(needsValue); + m_valueEdit->setVisible(needsValue); // Auto-toggle filters: signatures → executable code, values → writable data m_execCheck->setChecked(isSig); m_writeCheck->setChecked(!isSig); } +void ScannerPanel::onConditionChanged(int /*index*/) { + auto cond = (ScanCondition)m_condCombo->currentData().toInt(); + bool needsValue = (cond == ScanCondition::ExactValue); + m_valueLabel->setVisible(needsValue); + m_valueEdit->setVisible(needsValue); +} + void ScannerPanel::onScanClicked() { if (m_engine->isRunning()) { m_engine->abort(); @@ -306,12 +337,14 @@ void ScannerPanel::onScanClicked() { // Build request ScanRequest req = buildRequest(); - if (req.pattern.isEmpty()) + if (req.condition != ScanCondition::UnknownValue && req.pattern.isEmpty()) return; // error already shown by buildRequest m_lastScanMode = m_modeCombo->currentIndex(); - if (m_lastScanMode == 1) + if (m_lastScanMode == 1) { m_lastValueType = (ValueType)m_typeCombo->currentData().toInt(); + m_lastCondition = req.condition; + } m_lastPattern = req.pattern; m_scanBtn->setText(QStringLiteral("Cancel")); @@ -336,11 +369,28 @@ ScanRequest ScannerPanel::buildRequest() { } else { // Value mode auto vt = (ValueType)m_typeCombo->currentData().toInt(); - if (!serializeValue(vt, m_valueEdit->text(), req.pattern, req.mask, &err)) { - m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err)); - return {}; + auto cond = (ScanCondition)m_condCombo->currentData().toInt(); + + // Comparison conditions on fresh scan → treat as unknown + if (cond == ScanCondition::Changed || cond == ScanCondition::Unchanged || + cond == ScanCondition::Increased || cond == ScanCondition::Decreased) { + cond = ScanCondition::UnknownValue; } + + req.condition = cond; req.alignment = naturalAlignment(vt); + req.valueSize = valueSizeForType(vt); + + if (cond == ScanCondition::UnknownValue) { + // No pattern needed — capture all aligned addresses + req.maxResults = 500000; + } else { + // Exact value mode + if (!serializeValue(vt, m_valueEdit->text(), req.pattern, req.mask, &err)) { + m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err)); + return {}; + } + } } req.filterExecutable = m_execCheck->isChecked(); @@ -355,10 +405,11 @@ void ScannerPanel::onScanFinished(QVector results) { m_results = std::move(results); // Bytes are cached by the engine during scan. - // Value mode: override with exact search pattern (engine caches raw chunk bytes). + // Value mode (exact): override with exact search pattern (engine caches raw chunk bytes). + // Unknown mode: keep engine-captured bytes as-is (they're the baseline). for (auto& r : m_results) { r.previousValue.clear(); - if (m_lastScanMode == 1) + if (m_lastScanMode == 1 && m_lastCondition == ScanCondition::ExactValue) r.scanValue = m_lastPattern; } @@ -425,29 +476,41 @@ void ScannerPanel::onUpdateClicked() { int readSize = (m_lastScanMode == 1) ? valueSize() : 16; - // Build filter from current input field + // Determine rescan condition + ScanCondition cond = ScanCondition::ExactValue; + if (m_lastScanMode == 1) + cond = (ScanCondition)m_condCombo->currentData().toInt(); + + // For UnknownValue on rescan, just re-read all (update only, no filter) + if (cond == ScanCondition::UnknownValue) + cond = ScanCondition::ExactValue; // with empty filter = update only + + // Build filter from current input field (only for ExactValue condition) QByteArray filterPattern, filterMask; - if (m_lastScanMode == 0) { - // Signature mode - QString err; - if (!m_patternEdit->text().trimmed().isEmpty()) { - if (!parseSignature(m_patternEdit->text(), filterPattern, filterMask, &err)) { - m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err)); - return; + if (cond == ScanCondition::ExactValue) { + if (m_lastScanMode == 0) { + // Signature mode + QString err; + if (!m_patternEdit->text().trimmed().isEmpty()) { + if (!parseSignature(m_patternEdit->text(), filterPattern, filterMask, &err)) { + m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err)); + return; + } } - } - } else { - // Value mode - QString err; - if (!m_valueEdit->text().trimmed().isEmpty()) { - auto vt = (ValueType)m_typeCombo->currentData().toInt(); - if (!serializeValue(vt, m_valueEdit->text(), filterPattern, filterMask, &err)) { - m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err)); - return; + } else { + // Value mode — exact value filter + QString err; + if (!m_valueEdit->text().trimmed().isEmpty()) { + auto vt = (ValueType)m_typeCombo->currentData().toInt(); + if (!serializeValue(vt, m_valueEdit->text(), filterPattern, filterMask, &err)) { + m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err)); + return; + } + m_lastValueType = vt; } - m_lastValueType = vt; } } + // Comparison conditions (Changed/Unchanged/Increased/Decreased) don't need a filter pattern // Update last pattern so display uses the new value if (!filterPattern.isEmpty()) @@ -460,7 +523,8 @@ void ScannerPanel::onUpdateClicked() { m_progressBar->setValue(0); m_progressBar->show(); - m_engine->startRescan(prov, m_results, readSize, filterPattern, filterMask); + m_engine->startRescan(prov, m_results, readSize, cond, m_lastValueType, + filterPattern, filterMask); } void ScannerPanel::onRescanFinished(QVector results) { @@ -666,12 +730,14 @@ void ScannerPanel::applyTheme(const Theme& theme) { theme.border.name(), theme.hover.name()); m_modeCombo->setStyleSheet(comboStyle); m_typeCombo->setStyleSheet(comboStyle); + m_condCombo->setStyleSheet(comboStyle); // Labels QPalette lp; lp.setColor(QPalette::WindowText, theme.textDim); m_patternLabel->setPalette(lp); m_typeLabel->setPalette(lp); + m_condLabel->setPalette(lp); m_valueLabel->setPalette(lp); m_statusLabel->setPalette(lp); diff --git a/src/scannerpanel.h b/src/scannerpanel.h index fac0b50..05dff50 100644 --- a/src/scannerpanel.h +++ b/src/scannerpanel.h @@ -52,6 +52,8 @@ public: QPushButton* gotoButton() const { return m_gotoBtn; } QPushButton* copyButton() const { return m_copyBtn; } ScanEngine* engine() const { return m_engine; } + QComboBox* condCombo() const { return m_condCombo; } + QLabel* condLabel() const { return m_condLabel; } signals: void goToAddress(uint64_t address); @@ -72,13 +74,17 @@ private: void populateTable(bool showPrevious); void updateComboWidth(); + void onConditionChanged(int index); + // Input widgets QComboBox* m_modeCombo; // Signature / Value QLineEdit* m_patternEdit; // Signature pattern input QComboBox* m_typeCombo; // Value type dropdown + QComboBox* m_condCombo; // Scan condition (Exact/Unknown/Changed/...) QLineEdit* m_valueEdit; // Value input QLabel* m_patternLabel; QLabel* m_typeLabel; + QLabel* m_condLabel; QLabel* m_valueLabel; // Filters @@ -103,6 +109,7 @@ private: QVector m_results; int m_lastScanMode = 0; // 0=signature, 1=value ValueType m_lastValueType = ValueType::Int32; + ScanCondition m_lastCondition = ScanCondition::ExactValue; QByteArray m_lastPattern; // serialized search value int m_preRescanCount = 0; // result count before last rescan