Scanning
Scanning is how you find an address when all you know is a number on a screen. The shape of it is always the same: cast a wide net, change the value, narrow.
The cycle
1. scan:value int32 100 every address holding 100 right now
2. (make the value change in the target)
3. scan:next 95 of those, the ones now holding 95
Step 3 repeats until a handful of rows remain. A first scan is supposed to return thousands of rows — the number 100 is everywhere in a running program. What identifies the address is not the value, it is the value changing the way the thing on your screen changed.
picklock [game.exe:41902]> scan:value int32 100 --writable
Showing 20 of 3184 rows — page 1 of 160 — writable regions only (1.42 sec)
picklock [game.exe:41902]> scan:next 95
2 rows in set (0.02 sec)
--writable is worth reaching for by default: a value that changes lives in
writable memory, and skipping everything else makes the scan several times
faster. --all-regions overrides it when you are looking for something in
read-only data.
Because that restriction is easy to forget and expensive to forget, a
result set that skipped read-only memory says so in its footer — on the scan,
on every refine, and again on scan:results:
20 rows in set — writable regions only (0.02 sec)
It sits under the rows because that is where the question gets asked: you are
looking at what came back and wondering whether it is everything. You will also
see it without having typed --writable, if the writable_only setting is on
— the reason an address is missing should not be a setting you turned on last
week.
When you cannot see the number
A health bar with no digits, a timer that only moves — you cannot type the new value because you do not know it. Compare against the previous reading instead:
scan:next --changed different from last time
scan:next --unchanged the same as last time
scan:next --increased went up
scan:next --decreased went down
scan:next --increased-by 5 went up by exactly 5
scan:next --decreased-by 5 went down by exactly 5
Alternate --unchanged (while nothing is happening) with --decreased (after
taking damage) and a bar with no numbers narrows just as fast as one with them.
Comparisons are flags
Every comparison is a flag, never a bare word:
scan:value int32 --gt 50 greater than
scan:value int32 --between 10 20 inside a range, inclusive
scan:next --not-between 10 20 outside it
scan:next --ne 0 anything but zero
The value slot only ever holds a value. That is the reason: scan:next changed
searches for the word “changed”, which is a perfectly reasonable thing to
want when you are scanning text, and --changed is the comparison. Giving a
value on its own is the same as --eq, so scan:next 95 and
scan:next --eq 95 are one command.
Looking at where you are
A scan prints its first page. scan:results is how you reach the rest, and how
you see the current state between rounds:
scan:results the first page, re-read
scan:results --page 2 the next one (-p)
scan:results --all every row (-a)
scan:results --limit 50 a bigger page (-l)
It re-reads every address, so VALUE is what the target holds now, not what
the scan found, and PREVIOUS is the reading the comparisons above measure
against. Watching those two columns is usually how you spot the real row.
To take the results somewhere else:
scan:results --export results.json
That writes every result, not the page on screen.
Saying which rows are real
When you can see which rows matter, say so directly instead of inventing a comparison that happens to exclude the others:
scan:keep 1 4 7-9 keep those rows, drop the rest
scan:drop 2 the other way round
scan:reset throw the results away and start over
Ranges and lists both work. Rows renumber after either command, so #1 is
always the first surviving row.
Byte patterns and text
Two scans do not take a value at all.
AOB — an array-of-bytes signature, the standard way to find code that moves
between builds. ? is a wildcard for one byte:
scan:aob "48 8B ? ? 00"
Regex — a search over the target’s text:
scan:regex "Player[0-9]+"
scan:regex "Player[0-9]+" --length 64
Both take --max N to stop after N hits, which matters on a large target where
a loose pattern matches a great many times.
Cost, and stopping
A scan walks the whole address space, so it costs seconds rather than milliseconds, and a first scan on a multi-gigabyte process costs more than that. Two things help:
--writable(much less to walk) and--max N(stop early).pip install "picklock[speed]", which lets PyMemoryEditor vectorise the comparison loop with NumPy.
Ctrl+C stops a scan and keeps whatever it found, so an interrupted scan is still a result set you can refine.
Regions that cannot be read are skipped rather than failing the scan — a process is a moving target, and a page that vanished mid-scan is normal.
Then what
An address found by scanning is good for this run of the target only. To keep it across restarts, find the pointer path that reaches it — see Pointers.