[Memhack] Scanner - Redacted-Rice/ITB-RedactedRiceExts GitHub Wiki

Scanner

The Scanner is memhack's memory scanning system, similar to memedit's scanner or Cheat Engine. It allows you to search for specific values in the game's memory, filter results, and track down memory addresses.

The scanner was optimized to minimize scan time so it is suitable for use in game as long as it is not called too frequently. Scans tend to take between 100-200 ms on my device.

Important: Direct access via Memhack structs is always preferred over scanning. Scanning is meant to be a debugging and reverse engineering tool or a last resort if you cannot reverse engineer a stable pointer chain.

Overview

The scanner works by:

  1. First Scan - Searches all readable memory for values matching your criteria
  2. Rescan - Filters previous results based on new criteria (value changed, increased, etc.)
  3. Iteration - Repeat rescans until you find the specific address you're looking for

Common use cases:

  • Finding memory addresses for unknown game data
  • Reverse engineering memory structures
  • Discovering field offsets in structs
  • Verifying struct layouts

Table of Contents

Creating Scanners

Constructor

  • Scanner memhack.dll.scanner.new(dataType, options)
Argument name Type Description
dataType string Type of data to scan for (see Data Types)
options table Optional configuration table

Creates a new scanner for the specified data type.

Data Types

Type Description Value Example
"byte" 8-bit unsigned integer (0-255) 42
"int" 32-bit signed integer 1000
"float" 32-bit floating point 3.14
"double" 64-bit floating point 3.14159265359
"bool" Boolean (1 byte) true or false
"string" Null-terminated string "Hello"
"byte_array" Array of bytes "\x01\x02\x03"
"struct" Complex structure (see Struct Scanner) StructSearch object

Scan Options

The options table supports:

Option Type Default Description
maxResults int 100000 Maximum number of results to store
alignment int 0 Memory alignment requirement (0 = no requirement, 4 = must be 4-byte aligned)
checkTiming bool false If true, logs timing information for scan operations

Example:

-- Create scanner with default options
local scanner = memhack.dll.scanner.new("int")

-- Create scanner with custom options
local scanner = memhack.dll.scanner.new("float", {
    maxResults = 10000,
    alignment = 4,
    checkTiming = true
})

Scanning Operations

First Scan

  • table scanner:firstScan(scanType, value)
Argument name Type Description
scanType string Type of scan (see Scan Types)
value varies Value to scan for (type must match scanner's dataType)

Performs the initial scan of all readable memory. Returns a table with:

  • resultCount - Number of matching addresses found
  • maxResultsReached - Boolean indicating if the max result limit was reached

Example:

local scanner = memhack.dll.scanner.new("int")
local result = scanner:firstScan("exact", 100)

LOG("Found " .. result.resultCount .. " matches")
if result.maxResultsReached then
    LOG("WARNING: Max results reached, scan may be incomplete")
end

Rescan

  • table scanner:rescan(scanType, value)
Argument name Type Description
scanType string Type of scan (see Scan Types)
value varies Value to compare against (type must match scanner's dataType)

Filters previous results based on new criteria. Returns a table with:

  • resultCount - Number of matches remaining after filter

Example:

-- First scan for exact value
scanner:firstScan("exact", 100)

-- Change the value in-game, then rescan
scanner:rescan("increased", 100)  -- Find addresses that increased

-- Change again
scanner:rescan("exact", 150)  -- Find addresses now exactly 150

Scan Types

Scan Type Description First Scan Rescan Requires Value
"exact" Find addresses with exact value ✓ ✓ Yes
"not" Find addresses NOT matching value ✓ ✓ Yes
"increased" Value increased since last scan ✗ ✓ No (value ignored)
"decreased" Value decreased since last scan ✗ ✓ No (value ignored)
"changed" Value changed since last scan ✗ ✓ No (value ignored)
"unchanged" Value unchanged since last scan ✗ ✓ No (value ignored)

Note: increased, decreased, changed, and unchanged can only be used in rescans (they require a previous scan for comparison). Note: increased, decreased can only be used for numeric type scans. They are not supported for string, byte_array, or struct.

Example:

-- Find all addresses with value 100
scanner:firstScan("exact", 100)

-- Find addresses that increased (new value doesn't matter for this scan type)
scanner:rescan("increased", 0)  -- value parameter ignored but required

-- Find addresses that are now exactly 150
scanner:rescan("exact", 150)

Working with Results

Getting Results

  • table scanner:getResults(options)
Argument name Type Description
options table Optional configuration for pagination and value reading

Options table:

Option Type Default Description
offset int 0 Starting index for pagination (0-based)
limit int 1000 Maximum number of results to return
readValues bool false If true, reads current value at each address

Returns a table with:

  • results - Array of result objects (1-indexed)
  • totalCount - Total number of results available
  • offset - Offset used for this page
  • limit - Limit used for this page

Each result object contains:

  • address - Memory address (int)
  • value - Current value at address (only if readValues was true)

Example:

-- Get first 100 results without reading values
local results = scanner:getResults({ offset = 0, limit = 100 })

LOG("Showing " .. #results.results .. " of " .. results.totalCount .. " results")
for i, result in ipairs(results.results) do
    LOG(string.format("Address: 0x%X", result.address))
end

-- Get first 10 results with current values
local results = scanner:getResults({ offset = 0, limit = 10, readValues = true })

for i, result in ipairs(results.results) do
    LOG(string.format("Address: 0x%X = %s", result.address, tostring(result.value)))
end

Result Count

  • int scanner:getResultCount()

Returns the total number of results currently in the scanner.

Example:

local count = scanner:getResultCount()
LOG("Current results: " .. count)

Scanner Management

Reset

  • void scanner:reset()

Resets the scanner to its initial state, clearing all results. Call this before starting a new scanning session.

Example:

scanner:reset()
LOG("Scanner reset, ready for new scan")

Advanced Features

Struct Scanner

The struct scanner allows searching for complex memory structures with multiple fields at specific offsets.

The struct scanner keys on a specific byte as an optimization. This does NOT need to be the first byte in the struct. You should choose a value that is most unique. For example, if you are searching for a pilot, key on the first byte of the ID string "Pilot_Hotshot" or the "P" instead of the level "1". This is because "P"'s byte value is 0x70 which is much less common in memory than 0x01 would be.

Important: Choosing a "bad"/common value for the key can really slow down the scan.

-- Create a struct scanner
local scanner = memhack.dll.scanner.new("struct")

-- Define the structure to search for
local search = memhack.dll.scanner.StructSearch.new(0x64, 0x20)  -- keyByte ("C" in name), keyOffset

-- Add fields at specific offsets
search:addField(0x04, "int", 7)
search:addField(0x10, "int", 1)
search:addField(0x20, "string", "Camila")

-- Scan for structures matching this pattern
scanner:firstScan("exact", search)

StructSearch

StructSearch is used to define structure patterns for the struct scanner.

StructSearch.new

  • StructSearch memhack.dll.scanner.StructSearch.new(keyByte, keyOffset)
Argument name Type Description
keyByte int or string Byte value to identify structs (0-255, single char, or hex string like "0x64")
keyOffset int Offset of the key byte within the struct (default: 0)

Creates a new structure search definition.

addField

  • void search:addField(offset, dataType, value)
Argument name Type Description
offset int or string Byte offset of field (int or hex string like "0x10")
dataType string Data type (same as scanner data types except "struct")
value varies Expected value at this offset

Adds a field constraint to the structure search.

Example:

local search = memhack.dll.scanner.StructSearch.new(0x42, 0)
search:addField(0x04, "int", 1000)
search:addField(0x08, "bool", true)
search:addField("0x10", "string", "Ralph")  -- offset as hex string

Examples

Scanning for Strings

-- Scan for pilot name
local scanner = memhack.dll.scanner.new("string")
scanner:firstScan("exact", "Camila")

local results = scanner:getResults({ limit = 5, readValues = true })
LOG("Found " .. results.totalCount .. " addresses with 'Camila'")

for i, result in ipairs(results.results) do
    LOG(string.format("Address: 0x%X, Value: %s", result.address, result.value))
end

Tracking Value Changes

local scanner = memhack.dll.scanner.new("int")

-- Find all values that are currently 0
scanner:firstScan("exact", 0)
LOG("Found " .. scanner:getResultCount() .. " addresses with value 0")

-- Wait for something to change in game...
scanner:rescan("changed", 0)  -- Value parameter ignored for "changed"
LOG("After change: " .. scanner:getResultCount() .. " addresses changed")

Finding a Struct

-- Define a pilot structure pattern
local search = memhack.dll.scanner.StructSearch.new(0xD4, 0)  -- vtable byte at offset 0

-- Add known field values
search:addField(0x30, "int", 50)      -- XP = 50
search:addField(0x38, "int", 2)       -- Level = 2
search:addField(0x54, "int", 1)       -- Previous timelines = 1

-- Scan for matching structures
local scanner = memhack.dll.scanner.new("struct")
scanner:firstScan("exact", search)

LOG("Found " .. scanner:getResultCount() .. " matching structs")

-- Get addresses
local results = scanner:getResults({ limit = 5 })
for i, result in ipairs(results.results) do
    LOG(string.format("Struct at: 0x%X", result.address))
    
    -- Wrap as Pilot struct to verify
    local pilot = memhack.structs.Pilot.new(result.address, true)
    if pilot then
        LOG("  Name: " .. pilot:getNameStr())
    end
end

Performance Notes

  • First scans search all readable memory and can take several seconds
  • Use maxResults to limit memory usage and improve performance
  • Use alignment for data types that must be aligned (e.g., 4 for ints on some systems)
  • Rescans are much faster as they only check previous results
  • The scanner uses parallel processing (multi-threaded) automatically

Safety Notes

  • Scanning is read-only and safe (won't modify memory)
  • Results may point to invalid memory after game state changes
  • Always validate addresses before using them with structs
  • Scanner allocates memory from a special heap to avoid detecting previous/other scanner results

Constants

Access scan types and data types through constants:

-- Scan types
memhack.dll.scanner.SCAN_TYPE.EXACT      -- "exact"
memhack.dll.scanner.SCAN_TYPE.NOT        -- "not"
memhack.dll.scanner.SCAN_TYPE.INCREASED  -- "increased"
memhack.dll.scanner.SCAN_TYPE.DECREASED  -- "decreased"
memhack.dll.scanner.SCAN_TYPE.CHANGED    -- "changed"
memhack.dll.scanner.SCAN_TYPE.UNCHANGED  -- "unchanged"

-- Data types
memhack.dll.scanner.DATA_TYPE.BYTE        -- "byte"
memhack.dll.scanner.DATA_TYPE.INT         -- "int"
memhack.dll.scanner.DATA_TYPE.FLOAT       -- "float"
memhack.dll.scanner.DATA_TYPE.DOUBLE      -- "double"
memhack.dll.scanner.DATA_TYPE.BOOL        -- "bool"
memhack.dll.scanner.DATA_TYPE.STRING      -- "string"
memhack.dll.scanner.DATA_TYPE.BYTE_ARRAY  -- "byte_array"
memhack.dll.scanner.DATA_TYPE.STRUCT      -- "struct"
memhack.dll.scanner.DATA_TYPE.ITB_STRING  -- "itb_string" (also accepts alias "itbstring")