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)
|
||||
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; }"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
161
src/scanner.cpp
161
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> 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++) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user