Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions PyMemoryEditor/process/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -1094,8 +1094,8 @@ def rescan_pointer_paths(
-------
::

survivors = process.rescan_pointer_paths("scan1.json", new_address)
process.save_pointer_paths(survivors, "scan2.json")
survivors = process.rescan_pointer_paths("scan.json", new_address)
process.save_pointer_paths(survivors, "scan.json")
"""
from .pointer_scan import PointerPath

Expand Down
55 changes: 25 additions & 30 deletions docs/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ If you're new to memory editing, **start with the app** before writing code.
pip install "PyMemoryEditor[app]"
```

The `app` extra adds PySide6 and psutil to the install (psutil powers the
GUI's process picker). The library itself stays dependency-free.
The `app` extra pulls in PySide6 and other dependencies. The core library remains dependency-free.

## Launch

Expand All @@ -35,42 +34,38 @@ name or PID.

**🎯 Scanner**
- Every `ScanTypesEnum` mode
- Int8 / Int16 / Int32 / Int64, Float / Double, Boolean, String (UTF-8) and
Byte Array value types
- Range search
- AOB / byte signature search (IDA-style)
- Regex (string) search — a text regex matched against UTF-8 memory. The
Length field sets the maximum match width; matching is byte-wise, so `.`
spans one byte (use `.+` for multibyte characters)

**🔁 Refine workflow**
- All integer widths, Float, Double, Boolean, String (UTF-8), and Byte Array
- Range, AOB / byte-signature (IDA-style), and regex search

**🧲 Refine workflow**
- **First Scan → Next Scan** (Cheat Engine style)
- Six Next Scan comparisons (increased / decreased / changed / unchanged, plus
increased-by / decreased-by)
- Live progress
- Six more comparison modes (increased, decreased, changed, …)
- Live progress bar

**📋 Cheat table**
- Freeze / write values continuously
- Live value updates
- Freeze or overwrite values continuously
- Per-entry custom labels
- JSON import/export

**🔗 Pointer scan**
- Same engine as `scan_pointer_paths`
- Save scans to JSON
- Rescan / compare scans to narrow them down
- Send a resolved address straight to the Cheat Table
- JSON import / export

**🗺️ Memory map**
- All regions with R/W/X flags
- Backing file path per region (Linux; blank where the OS doesn't expose it)
- All regions with their attributes (address, size, R/W/X permissions)
- Auto-refresh as the memory layout changes
- Allocate and free memory directly from the map

**🔬 Hex viewer**
- Live dump with write-back
- Go to any address, with auto-refresh
- Live hex dump with in-place write-back
- Jump to any address, with auto-refresh

**🪵 Log console**
- Same stream as `logging.getLogger("PyMemoryEditor")`
- Pick the log level (DEBUG / INFO / WARNING / ERROR) at runtime
**📦 Modules**
- All loaded modules (DLLs / .so / .dylib) with base address, size, and path
- Auto-refresh as modules are loaded or unloaded
- Double-click to open in the Hex Viewer

**🧩 Pointer scan**
- Same engine as `scan_pointer_paths`
- Save / load scans as JSON
- Rescan and compare to narrow results down

```{admonition} Cross-platform dark theme
:class: tip
Expand All @@ -86,7 +81,7 @@ from the **Theme** button on the toolbar; your choice is remembered between runs
*First Scan*.
3. **Refine** with Next Scan after the value changes — pick *Exact Value* with
the new number, or one of the *increased / decreased / changed* shortcuts.
4. When the list is small, **double-click** a result to add it to the
4. **Double-click** a result to add it to the
**Cheat Table**.
5. **Freeze** the value with the checkbox or change it from the Cheat Table.
6. (Optional) **Run a Pointer Scan** on the result to find a chain that
Expand Down
17 changes: 14 additions & 3 deletions docs/guide/pointer-scan.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ solid pointers remain.
```python
# Run 1 — scan and save.
pointer_paths = process.scan_pointer_paths(address)
process.save_pointer_paths(pointer_paths, "scan1.json")
process.save_pointer_paths(pointer_paths, "scan.json")

# ... close the target, restart it, find the value's new address again ...

# Run 2 — keep only the saved paths that still reach it.
survivors = process.rescan_pointer_paths("scan1.json", new_address)
process.save_pointer_paths(survivors, "scan2.json")
survivors = process.rescan_pointer_paths("scan.json", new_address)
process.save_pointer_paths(survivors, "scan.json")
```

### Compare independent scans
Expand All @@ -119,6 +119,17 @@ live = path.rebase(process).to_pointer(process, pytype=int, bufflength=4)
live.value = 9999
```

### Load saved paths

Once you have a refined scan file, load it directly with
`load_pointer_paths` — no need to rescan again:

```python
paths = process.load_pointer_paths("scan.json")
pointer = paths[0].rebase(process).to_pointer(process)
pointer.write(9999)
```

## Persistence helpers

<table>
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pymemoryeditor
```

The library itself stays dependency-free — only the `app` extra pulls in its
dependencies (PySide6 and psutil, used by the GUI's process picker).
dependencies.

See the [GUI App guide](app.md) for a tour of every feature.

Expand Down
92 changes: 35 additions & 57 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
# Quick Start

Five minutes from `pip install` to overwriting a value in another process.
Five minutes from [`pip install`](installation.md) to overwriting a value in another process.

## 1. Install
## 1. Open a process

```bash
pip install PyMemoryEditor
```

(See [Installation](installation.md) for the GUI app or installing from source.)

## 2. Open a process

PyMemoryEditor exposes a single entry point: `OpenProcess`. You can target a
PyMemoryEditor exposes a single entry point: [`OpenProcess`](guide/opening-process.md). Target a
process by **name** or **PID**:

```python
Expand All @@ -32,10 +24,7 @@ with OpenProcess(name="notepad.exe") as process:
...
```

By default, `OpenProcess` opens a **read + write** handle. No special permission
flag needed for the common case.

## 3. Read and write a value
## 2. Read and write a value

The easiest way is the **typed shortcuts** — the size is baked into the method
name, so there's nothing to remember:
Expand All @@ -46,47 +35,46 @@ from PyMemoryEditor import OpenProcess
with OpenProcess(name="notepad.exe") as process:
address = 0x0005000C

value = process.read_int(address) # read a 4-byte int
value = process.read_int(address) # read a 4-byte int
print("Current:", value)

process.write_int(address, value + 7) # write it back
process.write_int(address, value + 7) # write it back
```

There's a `read_*` / `write_*` pair for every common type — `read_float`,
`read_bool`, `read_uint`, `read_string`, and more:
There's a `read_*` / `write_*` pair for every common type:

```python
name = process.read_string(address, 32) # reads a 32-byte field, returned up to the first NUL
name = process.read_string(address, 32) # reads a 32-byte field, returned up to the first NUL
```

Prefer to spell out the type yourself? The generic `read_process_memory` /
`write_process_memory` cover every case too — see
[Reading and writing memory](guide/read-write.md).
For the generic API, see [Reading and writing memory](guide/read-write.md).

## 4. Run your first scan
## 3. Run your first scan

You rarely know the address of a value up front — you **find it by scanning**.
`search_by_value` yields every address holding a given value:

```python
from PyMemoryEditor import OpenProcess

target_value = 100

with OpenProcess(name="game.exe") as process:
for address in process.search_by_value(int, value=100):
for address in process.search_by_value(int, value=target_value):
print(f"Found at 0x{address:X}")
```

That's the same operation Cheat Engine performs in its **First Scan** button.
[See the searching guide](guide/searching.md) for all eight comparison modes
and the refine workflow.

## 5. The Cheat Engine workflow
## 4. The Cheat Engine workflow

The classic loop is:

1. **Scan** for a value you can see (e.g. your health is `100`) — you get back
many candidate addresses.
1. **Scan** for a value you can see (e.g. your health is `100`)
2. **Let the value change** in the target (you take damage → `95`).
3. **Refine**: keep only the addresses that now hold the new value. Repeat
until one address remains — that's your value.
3. **Refine**: keep only the addresses that now hold the new value.
4. **Read, write or freeze** it.

```python
Expand All @@ -109,45 +97,35 @@ with OpenProcess(name="game.exe") as process:
For big targets, see [the refine-scan workflow](guide/searching.md#the-refine-scan-workflow)
to cache the region map once.

## 6. Pointer scanning — make an address survive restarts

The address you just found is **useless next launch**. The OS loads everything
somewhere new every time (ASLR), so `0x1FA3C140` today is garbage tomorrow.
The fix is a **static pointer path**: a chain that starts at a fixed location
inside a loaded module and dereferences its way to your value — so the same
recipe keeps working across restarts.
## 5. Pointer scanning — make an address survive restarts

PyMemoryEditor finds these for you. `scan_pointer_paths` is a **reverse pointer
scan** (Cheat Engine's "Pointer scan"): give it the value's address *right now*,
and it discovers the static paths that resolve to it.
Addresses change every launch (ASLR). A **pointer path** starts from a fixed
module offset and dereferences its way to your value — surviving restarts.

```python
# 1. Scan — find pointer paths that resolve to the target address.
with OpenProcess(name="game.exe") as process:
# The value lives here this run (e.g. from search_by_value above).
for path in process.scan_pointer_paths(0x1FA3C140, max_depth=4):
print(path)
# "game.exe"+0x10F4F4 -> [+0x0] -> +0x158
print(hex(path.resolve(process)))
```
# ... search for the address
paths = list(process.scan_pointer_paths(target_address, max_depth=3))
process.save_pointer_paths(paths, "health.json")

Each result is a `PointerPath` carrying the module + offsets — the part that
survives a restart. Save the reliable ones and reuse them later:
# ... restart the game, find the value's new address again ..

```python
# 2. Rescan — keep only paths that still resolve to the new address.
with OpenProcess(name="game.exe") as process:
paths = list(process.scan_pointer_paths(0x1FA3C140, max_depth=4))
process.save_pointer_paths(paths, "health.json")
# ... search for the new address again
survivors = process.rescan_pointer_paths("health.json", new_target_address)
process.save_pointer_paths(survivors, "health.json")

# ...next launch, the absolute address has changed but the path still works:
# 3. Load — use the saved paths directly.
with OpenProcess(name="game.exe") as process:
survivors = process.rescan_pointer_paths("health.json", 0x2B7C0140)
pointer = survivors[0].rebase(process).to_pointer(process)
paths = process.load_pointer_paths("health.json")
pointer = paths[0].rebase(process).to_pointer(process)
pointer.write(9999)
```

[See the pointer scan guide](guide/pointer-scan.md) for tuning the scan
(`max_depth`, `max_offset`), the multi-run refine workflow, and intersecting
independent scans with `compare_pointer_scans`.
[See the pointer scan guide](guide/pointer-scan.md) for tuning options and the
multi-run refine workflow.

## Next steps

Expand Down
6 changes: 1 addition & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ classifiers = [
requires-python = ">=3.10"
# The core library is dependency-free: process discovery and read/write/scan
# are implemented natively per platform (CreateToolhelp32Snapshot on Windows,
# /proc on Linux, libproc/Mach on macOS) via ctypes. Optional extras below add
# acceleration (NumPy) and the desktop app (PySide6 + psutil).
# /proc on Linux, libproc/Mach on macOS) via ctypes.
dependencies = []

[project.optional-dependencies]
Expand All @@ -68,9 +67,6 @@ tests = [
]
app = [
"PySide6>=6.5",
# The desktop app uses psutil's richer per-process info (username, memory
# footprint) in the process picker — beyond the pid/name the core library
# needs. The library itself does not depend on psutil.
"psutil>=5.9,<7",
]
# NOTE: NumPy is intentionally NOT listed here. The default test suite must
Expand Down
Loading