From 9c91ea2c7c73eeade5e10e09d4aab1ac043dd5c0 Mon Sep 17 00:00:00 2001 From: JeanExtreme002 Date: Sun, 7 Jun 2026 01:43:35 -0300 Subject: [PATCH] Streamline documentation and clean up verbose comments - Simplify quickstart guide: remove redundant install step, condense Cheat Engine workflow explanation, restructure pointer scan section into a clear 3-step save/rescan/load workflow - Update app.md feature list: consolidate scanner details, add Modules section, reword descriptions for brevity - Fix pointer-scan guide examples to use a single 'scan.json' file consistently and add a 'Load saved paths' subsection - Remove verbose inline comments from pyproject.toml extras - Fix rescan_pointer_paths docstring example (scan.json, not scan1/scan2) --- PyMemoryEditor/process/abstract.py | 4 +- docs/app.md | 55 ++++++++---------- docs/guide/pointer-scan.md | 17 +++++- docs/installation.md | 2 +- docs/quickstart.md | 92 ++++++++++++------------------ pyproject.toml | 6 +- 6 files changed, 78 insertions(+), 98 deletions(-) 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