mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
@@ -405,7 +405,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
|||||||
// Find bar (hidden by default, shown with Ctrl+F)
|
// Find bar (hidden by default, shown with Ctrl+F)
|
||||||
m_findBarContainer = new QWidget(this);
|
m_findBarContainer = new QWidget(this);
|
||||||
auto* fbLayout = new QHBoxLayout(m_findBarContainer);
|
auto* fbLayout = new QHBoxLayout(m_findBarContainer);
|
||||||
fbLayout->setContentsMargins(4, 0, 0, 0);
|
fbLayout->setContentsMargins(4, 1, 4, 1);
|
||||||
fbLayout->setSpacing(2);
|
fbLayout->setSpacing(2);
|
||||||
auto* findPrevBtn = new QToolButton(m_findBarContainer);
|
auto* findPrevBtn = new QToolButton(m_findBarContainer);
|
||||||
findPrevBtn->setText(QStringLiteral("\u25C0"));
|
findPrevBtn->setText(QStringLiteral("\u25C0"));
|
||||||
@@ -418,6 +418,7 @@ RcxEditor::RcxEditor(QWidget* parent) : QWidget(parent) {
|
|||||||
findCloseBtn->setFixedSize(24, 24);
|
findCloseBtn->setFixedSize(24, 24);
|
||||||
m_findBar = new QLineEdit(m_findBarContainer);
|
m_findBar = new QLineEdit(m_findBarContainer);
|
||||||
m_findBar->setPlaceholderText(QStringLiteral("Find..."));
|
m_findBar->setPlaceholderText(QStringLiteral("Find..."));
|
||||||
|
m_findBar->setFixedHeight(24);
|
||||||
fbLayout->addWidget(findPrevBtn);
|
fbLayout->addWidget(findPrevBtn);
|
||||||
fbLayout->addWidget(findNextBtn);
|
fbLayout->addWidget(findNextBtn);
|
||||||
fbLayout->addWidget(findCloseBtn);
|
fbLayout->addWidget(findCloseBtn);
|
||||||
@@ -889,7 +890,7 @@ void RcxEditor::applyTheme(const Theme& theme) {
|
|||||||
if (m_findBarContainer) {
|
if (m_findBarContainer) {
|
||||||
m_findBar->setStyleSheet(
|
m_findBar->setStyleSheet(
|
||||||
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
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()));
|
.arg(theme.backgroundAlt.name(), theme.text.name(), theme.border.name()));
|
||||||
m_findBarContainer->setStyleSheet(
|
m_findBarContainer->setStyleSheet(
|
||||||
QStringLiteral("QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }"
|
QStringLiteral("QToolButton { background: %1; color: %2; border: 1px solid %3; border-radius: 2px; }"
|
||||||
|
|||||||
@@ -1125,7 +1125,7 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
|||||||
// Find bar with prev/next buttons (hidden by default)
|
// Find bar with prev/next buttons (hidden by default)
|
||||||
pane.findContainer = new QWidget;
|
pane.findContainer = new QWidget;
|
||||||
auto* fcLayout = new QHBoxLayout(pane.findContainer);
|
auto* fcLayout = new QHBoxLayout(pane.findContainer);
|
||||||
fcLayout->setContentsMargins(4, 0, 0, 0);
|
fcLayout->setContentsMargins(4, 1, 4, 1);
|
||||||
fcLayout->setSpacing(2);
|
fcLayout->setSpacing(2);
|
||||||
const auto& fbTheme = ThemeManager::instance().current();
|
const auto& fbTheme = ThemeManager::instance().current();
|
||||||
auto* ccPrevBtn = new QToolButton;
|
auto* ccPrevBtn = new QToolButton;
|
||||||
@@ -1148,9 +1148,10 @@ MainWindow::SplitPane MainWindow::createSplitPane(TabState& tab) {
|
|||||||
ccCloseBtn->setStyleSheet(btnCss);
|
ccCloseBtn->setStyleSheet(btnCss);
|
||||||
pane.findBar = new QLineEdit;
|
pane.findBar = new QLineEdit;
|
||||||
pane.findBar->setPlaceholderText("Find...");
|
pane.findBar->setPlaceholderText("Find...");
|
||||||
|
pane.findBar->setFixedHeight(24);
|
||||||
pane.findBar->setStyleSheet(
|
pane.findBar->setStyleSheet(
|
||||||
QStringLiteral("QLineEdit { background: %1; color: %2; border: 1px solid %3;"
|
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()));
|
.arg(fbTheme.backgroundAlt.name(), fbTheme.text.name(), fbTheme.border.name()));
|
||||||
fcLayout->addWidget(ccPrevBtn);
|
fcLayout->addWidget(ccPrevBtn);
|
||||||
fcLayout->addWidget(ccNextBtn);
|
fcLayout->addWidget(ccNextBtn);
|
||||||
|
|||||||
@@ -203,7 +203,28 @@ QJsonObject McpBridge::handleInitialize(const QJsonValue& id, const QJsonObject&
|
|||||||
{"serverInfo", QJsonObject{
|
{"serverInfo", QJsonObject{
|
||||||
{"name", "reclass-mcp"},
|
{"name", "reclass-mcp"},
|
||||||
{"version", "1.0.0"}
|
{"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);
|
return okReply(id, result);
|
||||||
}
|
}
|
||||||
@@ -219,6 +240,8 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
|||||||
tools.append(QJsonObject{
|
tools.append(QJsonObject{
|
||||||
{"name", "project.state"},
|
{"name", "project.state"},
|
||||||
{"description", "Returns project state with paginated node tree. "
|
{"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). "
|
"Responses return max 'limit' nodes (default 50). "
|
||||||
"Use depth:1 first, then parentId to drill into a struct. "
|
"Use depth:1 first, then parentId to drill into a struct. "
|
||||||
"Enum/bitfield member arrays are omitted by default (counts shown instead); "
|
"Enum/bitfield member arrays are omitted by default (counts shown instead); "
|
||||||
@@ -249,6 +272,10 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
|||||||
tools.append(QJsonObject{
|
tools.append(QJsonObject{
|
||||||
{"name", "tree.apply"},
|
{"name", "tree.apply"},
|
||||||
{"description", "Apply batch of tree operations atomically (undo macro). "
|
{"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. "
|
"Each op is a JSON object with an 'op' field for the operation type and 'nodeId' (string) for the target node. "
|
||||||
"Operations: "
|
"Operations: "
|
||||||
"remove: {op:'remove', nodeId:'ID'}. "
|
"remove: {op:'remove', nodeId:'ID'}. "
|
||||||
@@ -301,10 +328,11 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
|||||||
// 4. hex.read
|
// 4. hex.read
|
||||||
tools.append(QJsonObject{
|
tools.append(QJsonObject{
|
||||||
{"name", "hex.read"},
|
{"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). "
|
"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) "
|
"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{
|
{"inputSchema", QJsonObject{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", QJsonObject{
|
{"properties", QJsonObject{
|
||||||
@@ -359,8 +387,10 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
|||||||
{"description", "Trigger a UI action. Fallback for operations without dedicated tools. "
|
{"description", "Trigger a UI action. Fallback for operations without dedicated tools. "
|
||||||
"Actions: undo, redo, new_file, open_file, save_file, save_file_as, "
|
"Actions: undo, redo, new_file, open_file, save_file, save_file_as, "
|
||||||
"export_cpp, set_view_root, scroll_to_node, collapse_node, expand_node, "
|
"export_cpp, set_view_root, scroll_to_node, collapse_node, expand_node, "
|
||||||
"select_node, refresh. "
|
"select_node, refresh, reset_tracking. "
|
||||||
"export_cpp accepts optional nodeId to export a single struct (recommended for large projects)."},
|
"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{
|
{"inputSchema", QJsonObject{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", QJsonObject{
|
{"properties", QJsonObject{
|
||||||
@@ -399,8 +429,11 @@ QJsonObject McpBridge::handleToolsList(const QJsonValue& id) {
|
|||||||
// 9. node.history
|
// 9. node.history
|
||||||
tools.append(QJsonObject{
|
tools.append(QJsonObject{
|
||||||
{"name", "node.history"},
|
{"name", "node.history"},
|
||||||
{"description", "Returns timestamped value change history (up to 10 entries) "
|
{"description", "Returns timestamped value change history (up to 10 entries) for specified nodes. "
|
||||||
"for specified nodes. Requires live provider with value tracking enabled."},
|
"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{
|
{"inputSchema", QJsonObject{
|
||||||
{"type", "object"},
|
{"type", "object"},
|
||||||
{"properties", QJsonObject{
|
{"properties", QJsonObject{
|
||||||
@@ -1180,6 +1213,12 @@ QJsonObject McpBridge::toolUiAction(const QJsonObject& args) {
|
|||||||
return makeTextResult("Selected node " + nodeIdStr);
|
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);
|
return makeTextResult("Unknown action: " + action, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
161
src/scanner.cpp
161
src/scanner.cpp
@@ -347,6 +347,64 @@ int naturalAlignment(ValueType type) {
|
|||||||
return 1;
|
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 ──
|
// ── Scan engine ──
|
||||||
|
|
||||||
ScanEngine::ScanEngine(QObject* parent)
|
ScanEngine::ScanEngine(QObject* parent)
|
||||||
@@ -366,13 +424,15 @@ void ScanEngine::abort() {
|
|||||||
void ScanEngine::start(std::shared_ptr<Provider> provider, const ScanRequest& req) {
|
void ScanEngine::start(std::shared_ptr<Provider> provider, const ScanRequest& req) {
|
||||||
if (isRunning()) return;
|
if (isRunning()) return;
|
||||||
|
|
||||||
if (req.pattern.isEmpty()) {
|
if (req.condition != ScanCondition::UnknownValue) {
|
||||||
emit error(QStringLiteral("Empty pattern"));
|
if (req.pattern.isEmpty()) {
|
||||||
return;
|
emit error(QStringLiteral("Empty pattern"));
|
||||||
}
|
return;
|
||||||
if (req.pattern.size() != req.mask.size()) {
|
}
|
||||||
emit error(QStringLiteral("Pattern and mask size mismatch"));
|
if (req.pattern.size() != req.mask.size()) {
|
||||||
return;
|
emit error(QStringLiteral("Pattern and mask size mismatch"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_abort.store(false);
|
m_abort.store(false);
|
||||||
@@ -400,14 +460,16 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
|||||||
timer.start();
|
timer.start();
|
||||||
|
|
||||||
QVector<ScanResult> results;
|
QVector<ScanResult> results;
|
||||||
|
const bool isUnknown = (req.condition == ScanCondition::UnknownValue);
|
||||||
|
|
||||||
if (!prov || req.pattern.isEmpty())
|
if (!prov || (!isUnknown && req.pattern.isEmpty()))
|
||||||
return results;
|
return results;
|
||||||
|
|
||||||
auto regions = prov->enumerateRegions();
|
auto regions = prov->enumerateRegions();
|
||||||
qDebug() << "[scan] regions:" << regions.size()
|
qDebug() << "[scan] regions:" << regions.size()
|
||||||
<< " pattern:" << req.pattern.size() << "bytes"
|
<< " pattern:" << req.pattern.size() << "bytes"
|
||||||
<< " align:" << req.alignment
|
<< " align:" << req.alignment
|
||||||
|
<< " condition:" << (int)req.condition
|
||||||
<< " filterExec:" << req.filterExecutable
|
<< " filterExec:" << req.filterExecutable
|
||||||
<< " filterWrite:" << req.filterWritable;
|
<< " filterWrite:" << req.filterWritable;
|
||||||
|
|
||||||
@@ -422,10 +484,11 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
|||||||
regions.append(fallback);
|
regions.append(fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int patternLen = req.pattern.size();
|
const int patternLen = isUnknown ? req.valueSize : req.pattern.size();
|
||||||
const char* pat = req.pattern.constData();
|
const char* pat = isUnknown ? nullptr : req.pattern.constData();
|
||||||
const char* msk = req.mask.constData();
|
const char* msk = isUnknown ? nullptr : req.mask.constData();
|
||||||
const int alignment = qMax(1, req.alignment);
|
const int alignment = qMax(1, req.alignment);
|
||||||
|
const int valSize = isUnknown ? req.valueSize : patternLen;
|
||||||
|
|
||||||
// Pre-compute total bytes for progress
|
// Pre-compute total bytes for progress
|
||||||
uint64_t totalBytes = 0;
|
uint64_t totalBytes = 0;
|
||||||
@@ -474,24 +537,38 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
|
|||||||
int scanEnd = readLen - patternLen;
|
int scanEnd = readLen - patternLen;
|
||||||
const char* data = chunk.constData();
|
const char* data = chunk.constData();
|
||||||
|
|
||||||
for (int i = 0; i <= scanEnd; i += alignment) {
|
if (isUnknown) {
|
||||||
bool match = true;
|
// Unknown value: capture every aligned address
|
||||||
for (int j = 0; j < patternLen; j++) {
|
for (int i = 0; i <= scanEnd; i += alignment) {
|
||||||
if ((data[i + j] & msk[j]) != (pat[j] & msk[j])) {
|
|
||||||
match = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (match) {
|
|
||||||
ScanResult r;
|
ScanResult r;
|
||||||
r.address = region.base + off + (uint64_t)i;
|
r.address = region.base + off + (uint64_t)i;
|
||||||
r.regionModule = region.moduleName;
|
r.scanValue = QByteArray(data + i, valSize);
|
||||||
r.scanValue = QByteArray(data + i, qMin(16, readLen - i));
|
|
||||||
results.append(r);
|
results.append(r);
|
||||||
|
|
||||||
if (results.size() >= req.maxResults)
|
if (results.size() >= req.maxResults)
|
||||||
goto done;
|
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
|
// Advance with overlap to catch patterns that straddle chunks
|
||||||
@@ -522,6 +599,7 @@ done:
|
|||||||
|
|
||||||
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
||||||
QVector<ScanResult> results, int readSize,
|
QVector<ScanResult> results, int readSize,
|
||||||
|
ScanCondition condition, ValueType valueType,
|
||||||
const QByteArray& filterPattern,
|
const QByteArray& filterPattern,
|
||||||
const QByteArray& filterMask) {
|
const QByteArray& filterMask) {
|
||||||
if (isRunning()) return;
|
if (isRunning()) return;
|
||||||
@@ -541,14 +619,15 @@ void ScanEngine::startRescan(std::shared_ptr<Provider> provider,
|
|||||||
|
|
||||||
watcher->setFuture(QtConcurrent::run(
|
watcher->setFuture(QtConcurrent::run(
|
||||||
[this, provider, results = std::move(results), readSize,
|
[this, provider, results = std::move(results), readSize,
|
||||||
filterPattern, filterMask]() mutable {
|
condition, valueType, filterPattern, filterMask]() mutable {
|
||||||
return runRescan(provider, std::move(results), readSize,
|
return runRescan(provider, std::move(results), readSize,
|
||||||
filterPattern, filterMask);
|
condition, valueType, filterPattern, filterMask);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
||||||
QVector<ScanResult> results, int readSize,
|
QVector<ScanResult> results, int readSize,
|
||||||
|
ScanCondition condition, ValueType valueType,
|
||||||
const QByteArray& filterPattern,
|
const QByteArray& filterPattern,
|
||||||
const QByteArray& filterMask) {
|
const QByteArray& filterMask) {
|
||||||
QElapsedTimer timer;
|
QElapsedTimer timer;
|
||||||
@@ -557,9 +636,17 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
|||||||
int total = results.size();
|
int total = results.size();
|
||||||
if (total == 0 || !prov) return results;
|
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
|
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
|
// Save previous values
|
||||||
for (auto& r : results)
|
for (auto& r : results)
|
||||||
@@ -579,8 +666,8 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
|||||||
uint64_t totalBytesRead = 0;
|
uint64_t totalBytesRead = 0;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
// Track which results matched the filter (by original index)
|
// Track which results matched (by original index)
|
||||||
QVector<bool> matched(total, !hasFilter); // if no filter, all match
|
QVector<bool> matched(total, !needsFilter); // if no filter, all match
|
||||||
|
|
||||||
while (i < total && !m_abort.load()) {
|
while (i < total && !m_abort.load()) {
|
||||||
uint64_t spanBase = results[order[i]].address;
|
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);
|
int off = (int)(r.address - spanBase);
|
||||||
r.scanValue = chunk.mid(off, readSize);
|
r.scanValue = chunk.mid(off, readSize);
|
||||||
|
|
||||||
// Apply filter: compare re-read bytes against the new pattern
|
// Apply exact-value filter
|
||||||
if (hasFilter) {
|
if (hasExactFilter) {
|
||||||
int patLen = filterPattern.size();
|
int patLen = filterPattern.size();
|
||||||
if (r.scanValue.size() >= patLen) {
|
if (r.scanValue.size() >= patLen) {
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
@@ -621,6 +708,18 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
|||||||
matched[idx] = ok;
|
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++;
|
chunks++;
|
||||||
@@ -637,7 +736,7 @@ QVector<ScanResult> ScanEngine::runRescan(std::shared_ptr<Provider> prov,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Filter out non-matching results
|
// Filter out non-matching results
|
||||||
if (hasFilter) {
|
if (needsFilter) {
|
||||||
QVector<ScanResult> filtered;
|
QVector<ScanResult> filtered;
|
||||||
filtered.reserve(total);
|
filtered.reserve(total);
|
||||||
for (int k = 0; k < total; k++) {
|
for (int k = 0; k < total; k++) {
|
||||||
|
|||||||
@@ -10,26 +10,6 @@
|
|||||||
|
|
||||||
namespace rcx {
|
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 ──
|
// ── Value scan types ──
|
||||||
|
|
||||||
enum class ValueType {
|
enum class ValueType {
|
||||||
@@ -41,6 +21,40 @@ enum class ValueType {
|
|||||||
HexBytes
|
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 ──
|
// ── Pattern parsing ──
|
||||||
|
|
||||||
// Parse IDA-style signature string ("48 8B ?? 05") into pattern + mask.
|
// 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).
|
// Natural alignment for a value type (used as default alignment for value scans).
|
||||||
int naturalAlignment(ValueType type);
|
int naturalAlignment(ValueType type);
|
||||||
|
|
||||||
|
// Byte-size for a value type (used for unknown scans and rescan read size).
|
||||||
|
int valueSizeForType(ValueType type);
|
||||||
|
|
||||||
// ── Scan engine ──
|
// ── Scan engine ──
|
||||||
|
|
||||||
class ScanEngine : public QObject {
|
class ScanEngine : public QObject {
|
||||||
@@ -67,6 +84,8 @@ public:
|
|||||||
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
|
void start(std::shared_ptr<Provider> provider, const ScanRequest& req);
|
||||||
void startRescan(std::shared_ptr<Provider> provider,
|
void startRescan(std::shared_ptr<Provider> provider,
|
||||||
QVector<ScanResult> results, int readSize,
|
QVector<ScanResult> results, int readSize,
|
||||||
|
ScanCondition condition = ScanCondition::ExactValue,
|
||||||
|
ValueType valueType = ValueType::Int32,
|
||||||
const QByteArray& filterPattern = {},
|
const QByteArray& filterPattern = {},
|
||||||
const QByteArray& filterMask = {});
|
const QByteArray& filterMask = {});
|
||||||
void abort();
|
void abort();
|
||||||
@@ -82,6 +101,7 @@ private:
|
|||||||
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
|
QVector<ScanResult> runScan(std::shared_ptr<Provider> prov, const ScanRequest& req);
|
||||||
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
|
QVector<ScanResult> runRescan(std::shared_ptr<Provider> prov,
|
||||||
QVector<ScanResult> results, int readSize,
|
QVector<ScanResult> results, int readSize,
|
||||||
|
ScanCondition condition, ValueType valueType,
|
||||||
const QByteArray& filterPattern,
|
const QByteArray& filterPattern,
|
||||||
const QByteArray& filterMask);
|
const QByteArray& filterMask);
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,18 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
|||||||
m_typeCombo->setCurrentIndex(2); // default: int32
|
m_typeCombo->setCurrentIndex(2); // default: int32
|
||||||
inputRow->addWidget(m_typeCombo);
|
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);
|
m_valueLabel = new QLabel(QStringLiteral("Value:"), this);
|
||||||
inputRow->addWidget(m_valueLabel);
|
inputRow->addWidget(m_valueLabel);
|
||||||
|
|
||||||
@@ -174,6 +186,8 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
|||||||
// ── Initial state: signature mode ──
|
// ── Initial state: signature mode ──
|
||||||
m_typeLabel->hide();
|
m_typeLabel->hide();
|
||||||
m_typeCombo->hide();
|
m_typeCombo->hide();
|
||||||
|
m_condLabel->hide();
|
||||||
|
m_condCombo->hide();
|
||||||
m_valueLabel->hide();
|
m_valueLabel->hide();
|
||||||
m_valueEdit->hide();
|
m_valueEdit->hide();
|
||||||
m_execCheck->setChecked(true);
|
m_execCheck->setChecked(true);
|
||||||
@@ -181,6 +195,8 @@ ScannerPanel::ScannerPanel(QWidget* parent)
|
|||||||
// ── Connections ──
|
// ── Connections ──
|
||||||
connect(m_modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
connect(m_modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
this, &ScannerPanel::onModeChanged);
|
this, &ScannerPanel::onModeChanged);
|
||||||
|
connect(m_condCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
|
this, &ScannerPanel::onConditionChanged);
|
||||||
connect(m_scanBtn, &QPushButton::clicked,
|
connect(m_scanBtn, &QPushButton::clicked,
|
||||||
this, &ScannerPanel::onScanClicked);
|
this, &ScannerPanel::onScanClicked);
|
||||||
connect(m_updateBtn, &QPushButton::clicked,
|
connect(m_updateBtn, &QPushButton::clicked,
|
||||||
@@ -251,12 +267,14 @@ void ScannerPanel::setEditorFont(const QFont& font) {
|
|||||||
m_valueEdit->setFont(font);
|
m_valueEdit->setFont(font);
|
||||||
m_modeCombo->setFont(font);
|
m_modeCombo->setFont(font);
|
||||||
m_typeCombo->setFont(font);
|
m_typeCombo->setFont(font);
|
||||||
|
m_condCombo->setFont(font);
|
||||||
m_statusLabel->setFont(font);
|
m_statusLabel->setFont(font);
|
||||||
m_scanBtn->setFont(font);
|
m_scanBtn->setFont(font);
|
||||||
m_gotoBtn->setFont(font);
|
m_gotoBtn->setFont(font);
|
||||||
m_copyBtn->setFont(font);
|
m_copyBtn->setFont(font);
|
||||||
m_patternLabel->setFont(font);
|
m_patternLabel->setFont(font);
|
||||||
m_typeLabel->setFont(font);
|
m_typeLabel->setFont(font);
|
||||||
|
m_condLabel->setFont(font);
|
||||||
m_valueLabel->setFont(font);
|
m_valueLabel->setFont(font);
|
||||||
m_execCheck->setFont(font);
|
m_execCheck->setFont(font);
|
||||||
m_writeCheck->setFont(font);
|
m_writeCheck->setFont(font);
|
||||||
@@ -280,14 +298,27 @@ void ScannerPanel::onModeChanged(int index) {
|
|||||||
|
|
||||||
m_typeLabel->setVisible(!isSig);
|
m_typeLabel->setVisible(!isSig);
|
||||||
m_typeCombo->setVisible(!isSig);
|
m_typeCombo->setVisible(!isSig);
|
||||||
m_valueLabel->setVisible(!isSig);
|
m_condLabel->setVisible(!isSig);
|
||||||
m_valueEdit->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
|
// Auto-toggle filters: signatures → executable code, values → writable data
|
||||||
m_execCheck->setChecked(isSig);
|
m_execCheck->setChecked(isSig);
|
||||||
m_writeCheck->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() {
|
void ScannerPanel::onScanClicked() {
|
||||||
if (m_engine->isRunning()) {
|
if (m_engine->isRunning()) {
|
||||||
m_engine->abort();
|
m_engine->abort();
|
||||||
@@ -306,12 +337,14 @@ void ScannerPanel::onScanClicked() {
|
|||||||
|
|
||||||
// Build request
|
// Build request
|
||||||
ScanRequest req = buildRequest();
|
ScanRequest req = buildRequest();
|
||||||
if (req.pattern.isEmpty())
|
if (req.condition != ScanCondition::UnknownValue && req.pattern.isEmpty())
|
||||||
return; // error already shown by buildRequest
|
return; // error already shown by buildRequest
|
||||||
|
|
||||||
m_lastScanMode = m_modeCombo->currentIndex();
|
m_lastScanMode = m_modeCombo->currentIndex();
|
||||||
if (m_lastScanMode == 1)
|
if (m_lastScanMode == 1) {
|
||||||
m_lastValueType = (ValueType)m_typeCombo->currentData().toInt();
|
m_lastValueType = (ValueType)m_typeCombo->currentData().toInt();
|
||||||
|
m_lastCondition = req.condition;
|
||||||
|
}
|
||||||
m_lastPattern = req.pattern;
|
m_lastPattern = req.pattern;
|
||||||
|
|
||||||
m_scanBtn->setText(QStringLiteral("Cancel"));
|
m_scanBtn->setText(QStringLiteral("Cancel"));
|
||||||
@@ -336,11 +369,28 @@ ScanRequest ScannerPanel::buildRequest() {
|
|||||||
} else {
|
} else {
|
||||||
// Value mode
|
// Value mode
|
||||||
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
||||||
if (!serializeValue(vt, m_valueEdit->text(), req.pattern, req.mask, &err)) {
|
auto cond = (ScanCondition)m_condCombo->currentData().toInt();
|
||||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
|
||||||
return {};
|
// 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.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();
|
req.filterExecutable = m_execCheck->isChecked();
|
||||||
@@ -355,10 +405,11 @@ void ScannerPanel::onScanFinished(QVector<ScanResult> results) {
|
|||||||
m_results = std::move(results);
|
m_results = std::move(results);
|
||||||
|
|
||||||
// Bytes are cached by the engine during scan.
|
// 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) {
|
for (auto& r : m_results) {
|
||||||
r.previousValue.clear();
|
r.previousValue.clear();
|
||||||
if (m_lastScanMode == 1)
|
if (m_lastScanMode == 1 && m_lastCondition == ScanCondition::ExactValue)
|
||||||
r.scanValue = m_lastPattern;
|
r.scanValue = m_lastPattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,29 +476,41 @@ void ScannerPanel::onUpdateClicked() {
|
|||||||
|
|
||||||
int readSize = (m_lastScanMode == 1) ? valueSize() : 16;
|
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;
|
QByteArray filterPattern, filterMask;
|
||||||
if (m_lastScanMode == 0) {
|
if (cond == ScanCondition::ExactValue) {
|
||||||
// Signature mode
|
if (m_lastScanMode == 0) {
|
||||||
QString err;
|
// Signature mode
|
||||||
if (!m_patternEdit->text().trimmed().isEmpty()) {
|
QString err;
|
||||||
if (!parseSignature(m_patternEdit->text(), filterPattern, filterMask, &err)) {
|
if (!m_patternEdit->text().trimmed().isEmpty()) {
|
||||||
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
|
if (!parseSignature(m_patternEdit->text(), filterPattern, filterMask, &err)) {
|
||||||
return;
|
m_statusLabel->setText(QStringLiteral("Pattern error: %1").arg(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
// Value mode — exact value filter
|
||||||
// Value mode
|
QString err;
|
||||||
QString err;
|
if (!m_valueEdit->text().trimmed().isEmpty()) {
|
||||||
if (!m_valueEdit->text().trimmed().isEmpty()) {
|
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
||||||
auto vt = (ValueType)m_typeCombo->currentData().toInt();
|
if (!serializeValue(vt, m_valueEdit->text(), filterPattern, filterMask, &err)) {
|
||||||
if (!serializeValue(vt, m_valueEdit->text(), filterPattern, filterMask, &err)) {
|
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
||||||
m_statusLabel->setText(QStringLiteral("Value error: %1").arg(err));
|
return;
|
||||||
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
|
// Update last pattern so display uses the new value
|
||||||
if (!filterPattern.isEmpty())
|
if (!filterPattern.isEmpty())
|
||||||
@@ -460,7 +523,8 @@ void ScannerPanel::onUpdateClicked() {
|
|||||||
m_progressBar->setValue(0);
|
m_progressBar->setValue(0);
|
||||||
m_progressBar->show();
|
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) {
|
void ScannerPanel::onRescanFinished(QVector<ScanResult> results) {
|
||||||
@@ -666,12 +730,14 @@ void ScannerPanel::applyTheme(const Theme& theme) {
|
|||||||
theme.border.name(), theme.hover.name());
|
theme.border.name(), theme.hover.name());
|
||||||
m_modeCombo->setStyleSheet(comboStyle);
|
m_modeCombo->setStyleSheet(comboStyle);
|
||||||
m_typeCombo->setStyleSheet(comboStyle);
|
m_typeCombo->setStyleSheet(comboStyle);
|
||||||
|
m_condCombo->setStyleSheet(comboStyle);
|
||||||
|
|
||||||
// Labels
|
// Labels
|
||||||
QPalette lp;
|
QPalette lp;
|
||||||
lp.setColor(QPalette::WindowText, theme.textDim);
|
lp.setColor(QPalette::WindowText, theme.textDim);
|
||||||
m_patternLabel->setPalette(lp);
|
m_patternLabel->setPalette(lp);
|
||||||
m_typeLabel->setPalette(lp);
|
m_typeLabel->setPalette(lp);
|
||||||
|
m_condLabel->setPalette(lp);
|
||||||
m_valueLabel->setPalette(lp);
|
m_valueLabel->setPalette(lp);
|
||||||
m_statusLabel->setPalette(lp);
|
m_statusLabel->setPalette(lp);
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ public:
|
|||||||
QPushButton* gotoButton() const { return m_gotoBtn; }
|
QPushButton* gotoButton() const { return m_gotoBtn; }
|
||||||
QPushButton* copyButton() const { return m_copyBtn; }
|
QPushButton* copyButton() const { return m_copyBtn; }
|
||||||
ScanEngine* engine() const { return m_engine; }
|
ScanEngine* engine() const { return m_engine; }
|
||||||
|
QComboBox* condCombo() const { return m_condCombo; }
|
||||||
|
QLabel* condLabel() const { return m_condLabel; }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void goToAddress(uint64_t address);
|
void goToAddress(uint64_t address);
|
||||||
@@ -72,13 +74,17 @@ private:
|
|||||||
void populateTable(bool showPrevious);
|
void populateTable(bool showPrevious);
|
||||||
void updateComboWidth();
|
void updateComboWidth();
|
||||||
|
|
||||||
|
void onConditionChanged(int index);
|
||||||
|
|
||||||
// Input widgets
|
// Input widgets
|
||||||
QComboBox* m_modeCombo; // Signature / Value
|
QComboBox* m_modeCombo; // Signature / Value
|
||||||
QLineEdit* m_patternEdit; // Signature pattern input
|
QLineEdit* m_patternEdit; // Signature pattern input
|
||||||
QComboBox* m_typeCombo; // Value type dropdown
|
QComboBox* m_typeCombo; // Value type dropdown
|
||||||
|
QComboBox* m_condCombo; // Scan condition (Exact/Unknown/Changed/...)
|
||||||
QLineEdit* m_valueEdit; // Value input
|
QLineEdit* m_valueEdit; // Value input
|
||||||
QLabel* m_patternLabel;
|
QLabel* m_patternLabel;
|
||||||
QLabel* m_typeLabel;
|
QLabel* m_typeLabel;
|
||||||
|
QLabel* m_condLabel;
|
||||||
QLabel* m_valueLabel;
|
QLabel* m_valueLabel;
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
@@ -103,6 +109,7 @@ private:
|
|||||||
QVector<ScanResult> m_results;
|
QVector<ScanResult> m_results;
|
||||||
int m_lastScanMode = 0; // 0=signature, 1=value
|
int m_lastScanMode = 0; // 0=signature, 1=value
|
||||||
ValueType m_lastValueType = ValueType::Int32;
|
ValueType m_lastValueType = ValueType::Int32;
|
||||||
|
ScanCondition m_lastCondition = ScanCondition::ExactValue;
|
||||||
QByteArray m_lastPattern; // serialized search value
|
QByteArray m_lastPattern; // serialized search value
|
||||||
int m_preRescanCount = 0; // result count before last rescan
|
int m_preRescanCount = 0; // result count before last rescan
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user