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.
This commit is contained in:
Matty
2026-03-03 11:32:13 -07:00
committed by IChooseYou
parent 86499e58ee
commit 9c72265901
7 changed files with 323 additions and 90 deletions

View File

@@ -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; }"

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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> 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<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
timer.start();
QVector<ScanResult> 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<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> 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<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> 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> provider,
QVector<ScanResult> 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> 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<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
QVector<ScanResult> results, int readSize,
ScanCondition condition, ValueType valueType,
const QByteArray& filterPattern,
const QByteArray& filterMask) {
QElapsedTimer timer;
@@ -557,9 +636,17 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> 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<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
uint64_t totalBytesRead = 0;
int i = 0;
// Track which results matched the filter (by original index)
QVector<bool> matched(total, !hasFilter); // if no filter, all match
// Track which results matched (by original index)
QVector<bool> 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<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> 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<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> 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<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
}
// Filter out non-matching results
if (hasFilter) {
if (needsFilter) {
QVector<ScanResult> filtered;
filtered.reserve(total);
for (int k = 0; k < total; k++) {

View File

@@ -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> provider, const ScanRequest& req);
void startRescan(std::shared_ptr<Provider> provider,
QVector<ScanResult> 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<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
QVector<ScanResult> results, int readSize,
ScanCondition condition, ValueType valueType,
const QByteArray& filterPattern,
const QByteArray& filterMask);

View File

@@ -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<int>::of(&QComboBox::currentIndexChanged),
this, &ScannerPanel::onModeChanged);
connect(m_condCombo, QOverload<int>::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<ScanResult> 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<ScanResult> 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);

View File

@@ -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<ScanResult> 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