Skip to content

[memhack] Memory Analyzer

DasKeifer edited this page Sep 30, 2026 · 3 revisions

Memory Analyzer

The Memory Analyzer is a reverse engineering tool for game structures and understanding memory patterns in Into the Breach. It allows you to capture memory snapshots, compare them across game states, and identify which bytes change under specific conditions.

Table of Contents

Basic Workflow

The typical reverse engineering workflow:

  1. Create an analyzer with estimated structure size
  2. Capture memory at known game states
  3. Compare captures to find changing bytes
  4. (Optional) Filter results to narrow down specific fields
  5. (Optional) Analyze value progressions to understand data types

This iterative process helps you map out structures field by field.

Quick API Reference

Class Methods

Management

Method Description
MemoryAnalyzer.new(id, size, options) Create new analyzer
MemoryAnalyzer.get(id) Get existing analyzer
MemoryAnalyzer.remove(id) Remove analyzer
MemoryAnalyzer.list() List all analyzer IDs
analyzer:enable() Enable analyzer
analyzer:disable() Disable analyzer
analyzer:clear() Clear all captures and results

Capturing

Method Description
analyzer:capture(baseAddress) Capture memory snapshot
analyzer:getCaptureCount() Get number of captures
analyzer:getCapture(index) Get specific capture
analyzer:removeCapture(index) Remove specific capture
analyzer:listCaptures() List all captures

Comparison

Method Description
analyzer:getChangedOnce(indices, resultId) Find bytes that changed at least once
analyzer:getChangedEvery(indices, resultId) Find bytes that changed every time
analyzer:getUnchanged(indices, resultId) Find bytes that never changed
analyzer:getCustomChanges(indices, func, resultId) Find bytes matching custom criteria
analyzer:getMatchingPattern(pattern, indices, resultId) Find bytes whose values match a pattern across captures

Value Analysis

Method Description
analyzer:addAllCapturesValues(result, ranges) Add all value progressions
analyzer:addChangedCapturesValues(result, ranges) Add only changed values
analyzer:addUniqueCapturesValues(result, ranges) Add unique value counts

Results

Method Description
analyzer:getResult(resultId) Get stored result
analyzer:removeResult(resultId) Remove stored result
analyzer:listResults() List all stored result IDs
analyzer:log(result) Log result to console
analyzer:filterResult(result, filterSpec, resultId) Filter result

Result Object Structure

{
    analyzerId = "test",
    alignment = 4,
    captureIndices = {1, 2, 3},
    filtered = {  -- Ranges that matched comparison
        {
            startOffset = 0x00,
            endOffset = 0x03,
            size = 4,
            data = {  -- Only present after add*Values() calls
                values = {{100, 80, 60}, ...},  -- Values at each alignment chunk
                uniqueCount = 3,
                uniqueValues = {100, 80, 60}
            }
        }
    },
    unfiltered = {  -- Ranges that didn't match
        -- Same structure as filtered
    }
}

Creating an Analyzer

Analyzer can be created in the code or via console. Depends on desired use as to which is better or would be easier to use.

MemoryAnalyzer.new

local analyzer = memhack.debug.MemoryAnalyzer.new(id, size, options)
Parameter Type Description
id string Unique identifier for this analyzer
size number Size in bytes to capture (must be divisible by alignment)
options table Configuration options (see below)

Options table:

Field Type Default Description
baseAddress number nil Starting address to capture from
pointerChain table {} Array of offsets to follow before capturing
alignment number 4 Byte alignment for comparisons (1, 2, 4, or 8)
enabled boolean true Whether analyzer is active on creation
rateLimit number 0 Minimum seconds between captures (0 = unlimited)

Example:

-- Simple analyzer for direct address
local analyzer = memhack.debug.MemoryAnalyzer.new("pawn", 128, {
    baseAddress = pawnAddress,
    alignment = 4
})

-- Analyzer without local var (i.e. in console)
-- Reference will be stored in Memory Analyzer for later use
memhack.debug.MemoryAnalyzer.new("pilot", 256)

Managing Analyzers

-- Retrieve existing analyzer
local analyzer = memhack.debug.MemoryAnalyzer.get("pawn")

-- List all analyzer IDs
local ids = memhack.debug.MemoryAnalyzer.list()  -- Returns: {"pawn", "pilot", ...}

-- Remove analyzer
memhack.debug.MemoryAnalyzer.remove("pawn")

-- Enable/disable analyzer
memhack.debug.MemoryAnalyzer.get("pawn"):disable()  -- Prevents captures
analyzer:enable()   -- Re-enables captures

Capturing Memory

Once you have your analyzer, you can capture the current state for later comparison

Basic Capture

-- Capture at configured base address
analyzer:capture()

-- Capture at different address
analyzer:capture(differentAddress)

-- Rate limiting (if configured)
analyzer.rateLimit = 1  -- Max 1 capture per second
analyzer:capture()  -- Works, returns non nil
analyzer:capture()  -- Logs "Rate limit exceeded" and returns nil

Managing Captures

-- Get capture count
local count = analyzer:getCaptureCount()

-- Get specific capture
local capture = analyzer:getCapture(1)

-- List all captures
local list = analyzer:listCaptures()

-- Remove specific capture
analyzer:removeCapture(1)

-- Clear all captures and results
analyzer:clear()

Comparing Captures

After you get your captures, you can compare them to search for differences. There are 3 basic comparisons and an additional API that takes a comparison function.

getChangedOnce

Finds bytes that changed at least once between any two captures.

local result = analyzer:getChangedOnce(captureIndices, resultId)
Parameter Type Description
captureIndices table/number/nil Which captures to compare (see below)
resultId string (optional) ID to store result for later retrieval

Capture indices options:

  • nil - Compare all captures
  • 3 (number) - Compare last 3 captures
  • {1, 3, 5} (table) - Compare specific captures by index

Returns: Result object with:

  • filtered - Ranges that changed at least once
  • unfiltered - Ranges that never changed

Example:

-- Compare all captures
local result = analyzer:getChangedOnce()

-- Compare last 3 captures and store result
local result = analyzer:getChangedOnce(3, "health_changes")

-- Compare specific captures
local result = analyzer:getChangedOnce({1, 5, 10})

getChangedEvery

Finds bytes that changed in every capture. If it is unchanged between any capture, this will not include it. If you are not sure, its safer to use getChangedOnce.

local result = analyzer:getChangedEvery(captureIndices, resultId)

Useful for finding counters or values that increment/decrement constantly.

Example:

local result = analyzer:getChangedEvery()

getUnchanged

Finds bytes that never changed across all captures.

local result = analyzer:getUnchanged(captureIndices, resultId)

Returns: Result object with:

  • filtered - Ranges that never changed
  • unfiltered - Ranges that changed at least once

Useful for finding constants, type IDs, or static configuration.

Example:

-- Find constant fields
local result = analyzer:getUnchanged()

getCustomChanges

Compare captures with custom logic.

local result = analyzer:getCustomChanges(captureIndices, comparatorFunc, resultId)
Parameter Type Description
comparatorFunc function function(selectedCaptures, byteIdx) -> bool

The comparator receives all selected captures and a byte index, and returns true if that byte index should be included in the filtered results.

Example:

-- Find bytes that are always zero
local result = analyzer:getCustomChanges(nil, function(captures, byteIdx)
    for _, cap in ipairs(captures) do
        if cap.data[byteIdx] ~= 0 then
            return false
        end
    end
    return true
end)

getMatchingPattern

  • result|nil analyzer:getMatchingPattern(pattern, captureIndices, resultId)
Argument name Type Description
pattern table/string Pattern describing expected value progression across captures
captureIndices table/nil Optional subset of capture indices to consider
resultId string/nil If set, stores the result under this id on the analyzer

Finds alignment sized values whose progression across captures matches pattern. Returns a result object (filtered / unfiltered) and optionally stores it as resultId.

Example:

local analyzer = memhack.debug.MemoryAnalyzer.get("pilot_xp")
-- 3 captures: exact 0, then any value named $xp, then $xp again (must match)
local result = analyzer:getMatchingPattern({0, "$xp", "$xp"}, nil, "xp_pattern")
if result then
    analyzer:log("xp_pattern")
end

Storing and Retrieving Results

Results are stored in the Memory Analyzer Object and can be retrieved later from it like Memory Analyzers.

-- Store result with ID
local result = analyzer:getChangedOnce(nil, "damage_test")

-- Later, add data using ID
analyzer:addChangedCapturesValues("damage_test")
analyzer:log("damage_test")

-- Manage stored results
local resultIds = analyzer:listResults()  -- Returns: ["damage_test", ...]
local result = analyzer:getResult("damage_test")
analyzer:removeResult("damage_test")

Adding Value Data

After comparison, add value data to see how bytes changed, not just which bytes changed. The last one called/generated are stored in the results.

addAllCapturesValues

Shows the value at each byte for every capture regardless of if it changed or not.

result = analyzer:addAllCapturesValues(result, ranges)
Parameter Type Default Description
result table/string required Result object or stored result ID
ranges string "filtered" Which ranges to add values to: "filtered", "unfiltered", or "both"

Example:

local result = analyzer:getChangedOnce()
result = analyzer:addAllCapturesValues(result)
analyzer:log(result)

addChangedCapturesValues

Shows value progression - i.e. only show value changes

result = analyzer:addChangedCapturesValues(result, ranges)

Useful for reducing noise when many captures have the same value or when doing alot of captures.

addUniqueCapturesValues

Shows unique value counts for each byte range. Does not preserve order of changes as it counts and displays only unique values.

result = analyzer:addUniqueCapturesValues(result, ranges)

Filtering Results

Narrow down results by filtering ranges or specific byte patterns. This will store the filtered results in the Memory Analyzer if the resultId is passed.

Range-Level Filtering

Filter using a custom filter function

local filtered = analyzer:filterResult(result, filterFunc, resultId)

Filter function signature: function(range, captures) -> bool

Example:

-- Keep only ranges larger than 4 bytes
local filtered = analyzer:filterResult(result, function(range, captures)
    return range.size > 4
end)

Chunk-Level Filtering

Filter by byte values using a custom filter function. This passes each range in alignment size chunks.

local filtered = analyzer:filterResult(result, {
    alignment = 4,  -- Check 4-byte chunks
    filter = function(offset, values) -> bool,
    ranges = "filtered"  -- Which ranges to check
}, resultId)
Parameter Type Description
alignment number Size of chunks to check (1, 2, 4, or 8 bytes)
filter function function(offset, values) -> bool where values is array of values at that offset across all captures
ranges string "filtered" or "unfiltered"

Example:

-- Keep only chunks where values are between 0 and 100
local filtered = analyzer:filterResult(result, {
    alignment = 4,
    filter = function(offset, values)
        for _, v in ipairs(values) do
            if v < 0 or v > 100 then
                return false
            end
        end
        return true
    end
})

Examples

Finding Pilot Xp (via console commands)

memhack.debug.MemoryAnalyzer.new("pilot_xp", 0xff)

-- Capture with pilot
memhack.debug.MemoryAnalyzer.get("pilot_xp"):capture(Game:GetSquadPilots()[1])

-- ... Gain xp ...

-- Recapture
memhack.debug.MemoryAnalyzer.get("pilot_xp"):capture(Game:GetSquadPilots()[1])

-- Find what changed
memhack.debug.MemoryAnalyzer.get("pilot_xp"):getChangedEvery("xp_changes")
memhack.debug.MemoryAnalyzer.get("pilot_xp"):addChangedCapturesValues("xp_changes")

-- Look for a byte that matches the xps we captured with
memhack.debug.MemoryAnalyzer.get("pilot_xp"):log("xp_changes")

Clone this wiki locally