Add MCP scanner tools, source.modules, reconnect, and constraint regions

Scanner engine:
- Add constrainRegions to ScanRequest — callers pass address ranges
  that are intersected with provider regions before scanning
- Merge overlapping/adjacent constraints to prevent duplicate results
- Fix final-chunk overlap: skip overlap advance on last chunk to avoid
  re-scanning the tail of a region

MCP tools:
- scanner.scan: value scans (int/float types) with optional region
  constraints, returns first 15 addresses
- scanner.scan_pattern: pattern/signature scans with wildcards
- source.modules: list loaded modules with base address and size
- mcp.reconnect: graceful client disconnect for IDE reconnection
- parseInteger() helper for hex string args (avoids JSON double
  precision loss on 64-bit addresses)
- Fix baseRelative semantics in hex.read/hex.write (was inverted)
- Auto-set tree.baseAddress from provider after process attach

Scanner panel:
- runValueScanAndWait() and runPatternScanAndWait() for blocking
  scan execution from MCP/automation code

Tests: 41 new test cases for constrainRegions covering gaps, partial
overlap, adjacent regions, writable filter, degenerate ranges,
overlapping constraints, boundary patterns, alignment, and value
types at region start/end positions.
This commit is contained in:
noita-player
2026-03-04 18:55:09 -08:00
parent 7b9b140823
commit 51de48a6ed
7 changed files with 1305 additions and 52 deletions

View File

@@ -492,6 +492,41 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
const bool hasRange = (req.startAddress != 0 || req.endAddress != 0) &&
req.endAddress > req.startAddress;
// If constrainRegions specified, intersect with provider regions
if (!req.constrainRegions.isEmpty()) {
// Sort and merge overlapping/adjacent constraints to avoid duplicate sub-regions
auto constraints = req.constrainRegions;
std::sort(constraints.begin(), constraints.end(),
[](const AddressRange& a, const AddressRange& b) { return a.start < b.start; });
QVector<AddressRange> merged;
for (const auto& c : constraints) {
if (c.end <= c.start) continue; // skip degenerate ranges
if (!merged.isEmpty() && c.start <= merged.last().end)
merged.last().end = qMax(merged.last().end, c.end);
else
merged.append(c);
}
QVector<MemoryRegion> clipped;
for (const auto& region : regions) {
uint64_t rEnd = region.base + region.size;
for (const auto& c : merged) {
if (c.end <= region.base || c.start >= rEnd) continue;
uint64_t iStart = qMax(region.base, c.start);
uint64_t iEnd = qMin(rEnd, c.end);
if (iEnd <= iStart) continue;
MemoryRegion sub = region;
sub.base = iStart;
sub.size = iEnd - iStart;
clipped.append(sub);
}
}
regions = std::move(clipped);
qDebug() << "[scan] constrained to" << regions.size() << "sub-regions from"
<< req.constrainRegions.size() << "address ranges ("
<< merged.size() << "after merge)";
}
// Pre-compute total bytes for progress
uint64_t totalBytes = 0;
for (const auto& r : regions) {
@@ -541,7 +576,7 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
continue;
}
const int overlap = patternLen; // need full patternLen overlap so pattern at chunk end is found
const int overlap = patternLen - 1;
QByteArray chunk(qMin((uint64_t)kChunk, regSize), Qt::Uninitialized);
uint64_t regOffset = regStart - region.base; // offset within provider region
@@ -597,9 +632,12 @@ QVector<ScanResult> ScanEngine::runScan(std::shared_ptr<Provider> prov,
}
}
// Advance with overlap to catch patterns that straddle chunks
// Advance with overlap to catch patterns that straddle chunks.
// Skip overlap on the final chunk -- nothing follows to overlap into.
uint64_t advance;
if (readLen > overlap)
if ((uint64_t)readLen >= remaining)
advance = remaining; // last chunk, no overlap needed
else if (readLen > overlap)
advance = (uint64_t)(readLen - overlap);
else
advance = 1; // prevent infinite loop on tiny regions