diff --git a/PyMemoryEditor/process/abstract.py b/PyMemoryEditor/process/abstract.py
index 56f658d..0846d84 100644
--- a/PyMemoryEditor/process/abstract.py
+++ b/PyMemoryEditor/process/abstract.py
@@ -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
diff --git a/docs/app.md b/docs/app.md
index 286d8d4..46a68c6 100644
--- a/docs/app.md
+++ b/docs/app.md
@@ -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
@@ -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
@@ -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
diff --git a/docs/guide/pointer-scan.md b/docs/guide/pointer-scan.md
index 3988e3e..0a3a5a4 100644
--- a/docs/guide/pointer-scan.md
+++ b/docs/guide/pointer-scan.md
@@ -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
@@ -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
diff --git a/docs/installation.md b/docs/installation.md
index 9a57e0c..c025fe5 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -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.
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 933e27e..e877c4a 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -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
@@ -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:
@@ -46,31 +35,32 @@ 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}")
```
@@ -78,15 +68,13 @@ 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
@@ -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
diff --git a/pyproject.toml b/pyproject.toml
index f71d997..26f839a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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]
@@ -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