PyMemoryEditor 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PKG-INFO +1 -27
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/__init__.py +1 -1
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/cheat_table.py +114 -12
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/pointer_chain_dialog.py +13 -1
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/pointer_scan_dialog.py +13 -3
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/scan_worker.py +58 -20
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/scanner_panel.py +243 -44
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/value_types.py +172 -1
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/functions.py +11 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/types.py +12 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/pattern.py +36 -11
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/README.md +0 -26
- pymemoryeditor-2.2.0/tests/app/test_app_cheat_entry.py +490 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_app_scan_request.py +113 -3
- pymemoryeditor-2.2.0/tests/app/test_app_smoke.py +654 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_app_value_types.py +35 -0
- pymemoryeditor-2.2.0/tests/platforms/test_macos_transient_reads.py +81 -0
- pymemoryeditor-2.1.0/tests/app/test_app_cheat_entry.py +0 -85
- pymemoryeditor-2.1.0/tests/app/test_app_smoke.py +0 -301
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/.flake8 +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/.gitignore +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/.readthedocs.yaml +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/CODE_OF_CONDUCT.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/CONTRIBUTING.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/LICENSE +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/Makefile +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/__main__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/_auto_refresh_dialog.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/_icon.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/_widgets.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/application.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/assets/icon.svg +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/cheat_entry.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/cheat_poll_worker.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/log_console_dialog.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/main_window.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/memory_map_dialog.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/memory_viewer_dialog.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/modules_dialog.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/open_process_dialog.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/results_view.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/scan_types.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/threads_dialog.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/enums.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/functions.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/libc.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/process.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/types.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/libsystem.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/process.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/abstract.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/errors.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/info.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/module_info.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/pointer_scan.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/region.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/remote_pointer.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/scanning.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/thread_info.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/util.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/py.typed +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/convert.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/scan.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/scan_numpy.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/functions.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/process.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/types.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/SECURITY.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/assets/screenshots/app.png +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/assets/social-preview.html +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/assets/social-preview.png +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/_static/.gitkeep +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/_static/custom.css +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/_templates/layout.html +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/enums.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/errors.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/index.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/memory-region.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/module-info.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/openprocess.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/pointer-path.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/remote-pointer.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/thread-info.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/utilities.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/app.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/conf.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/contributing.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/funding.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/glossary.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/allocate-free.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/index.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/logging.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/memory-regions.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/modules-threads.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/opening-process.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/pattern-scan.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/pointer-scan.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/pointers.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/read-write.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/searching.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/index.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/installation.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/license.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/platform-notes.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/quickstart.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/requirements.txt +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/troubleshooting.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/why.md +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/pyproject.toml +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_app_pointer_parsing.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_app_widgets.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_auto_refresh_dialog.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_cheat_poll_worker.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_open_process_dialog.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_refine_scan_worker.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/conftest.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/core/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/core/test_errors.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/core/test_logger.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_allocate_free.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_bufflength_inference.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_editor.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_partial_io.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_read_into.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_str_boundary.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_str_decode_consistency.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_typed_accessors.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_write_safety.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_write_str_bytes_width.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/test_bitness.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/test_bitness_fallback_warns.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/test_linux_types.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/test_macos_protect.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/test_win32_permissions.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/pointer/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/pointer/test_pointer_chain.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/pointer/test_pointer_scan.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/pointer/test_remote_pointer.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_memory_region.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_module_enumeration.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_partial_name_match.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_process_enumeration.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_process_lookup.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_thread_enumeration.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/__init__.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_chunking_integration.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_pattern_compile.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_pattern_scan.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_region_snapshot.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_scan.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_scan_numpy.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_scan_properties.py +0 -0
- {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_scanning_helper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: PyMemoryEditor
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Read, write and scan process memory in a few lines of Python — Cheat Engine-style scans, pointer chains and AOB search on Windows, Linux and macOS.
|
|
5
5
|
Project-URL: Homepage, https://github.com/JeanExtreme002/PyMemoryEditor
|
|
6
6
|
Project-URL: Documentation, https://pymemoryeditor.readthedocs.io
|
|
@@ -138,32 +138,6 @@ That's it — read, write or scan another process in three lines, the same way o
|
|
|
138
138
|
|
|
139
139
|
---
|
|
140
140
|
|
|
141
|
-
## What's inside
|
|
142
|
-
|
|
143
|
-
### 🐍 The Python library
|
|
144
|
-
|
|
145
|
-
Full control over another process's memory — in a few lines of Python:
|
|
146
|
-
|
|
147
|
-
- ✅ **Read & write** values (`int`, `float`, `bool`, `str`, `bytes`)
|
|
148
|
-
- 🔍 **Value scan** with eight comparison modes
|
|
149
|
-
- 🎯 **Pattern scan** (IDA-style AOB & regex)
|
|
150
|
-
- 🔗 **Pointer chains** + a live `RemotePointer` handle
|
|
151
|
-
- 🧭 **Pointer scan** — find static pointers that survive ASLR
|
|
152
|
-
- 🗺️ **Memory map**, **modules**, **threads**
|
|
153
|
-
- 🧱 **Allocate & free** remote memory (Windows / macOS)
|
|
154
|
-
|
|
155
|
-
### 🖥️ The bundled GUI app
|
|
156
|
-
|
|
157
|
-
All the library's power — no code required:
|
|
158
|
-
- ⚡ **Zero setup** — attach to a process and start scanning in seconds
|
|
159
|
-
- 🧲 **Refine workflow** — First Scan → Next Scan with live visual feedback
|
|
160
|
-
- 📋 **Cheat table** — freeze / write values on the fly, JSON import/export
|
|
161
|
-
- 🔬 **Hex viewer** — browse raw memory and write back inline
|
|
162
|
-
- 🧩 **Pointer scan UI** — scan, export & rescan across sessions with a few clicks
|
|
163
|
-
- 🎨 **One-click access** — every feature at your fingertips, no code needed
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
|
|
167
141
|
## 📖 Documentation
|
|
168
142
|
|
|
169
143
|
Full documentation lives at **[pymemoryeditor.readthedocs.io](https://pymemoryeditor.readthedocs.io)** — installation, the Cheat Engine workflow, every method and parameter, the GUI app guide, platform notes and troubleshooting.
|
|
@@ -48,7 +48,12 @@ from PyMemoryEditor import AbstractProcess
|
|
|
48
48
|
from ._widgets import parse_hex_address, shutdown_worker_thread
|
|
49
49
|
from .cheat_entry import CheatEntry
|
|
50
50
|
from .cheat_poll_worker import TICK_INTERVAL_MS, _CheatPollWorker
|
|
51
|
-
from .value_types import
|
|
51
|
+
from .value_types import (
|
|
52
|
+
VALUE_TYPES,
|
|
53
|
+
ValueTypeSpec,
|
|
54
|
+
find_spec,
|
|
55
|
+
parse_value_for_write,
|
|
56
|
+
)
|
|
52
57
|
|
|
53
58
|
|
|
54
59
|
# Re-exported for backward compatibility with callers that imported the
|
|
@@ -197,10 +202,25 @@ class CheatTable(QWidget):
|
|
|
197
202
|
delete_shortcut.activated.connect(self._on_remove_selected)
|
|
198
203
|
|
|
199
204
|
def add_entry(self, entry: CheatEntry) -> None:
|
|
205
|
+
# Every entry enters here, whichever way it was created — promoted from
|
|
206
|
+
# a scan, from a pointer dialog, added by hand, or loaded from JSON. A
|
|
207
|
+
# zero-width buffer reads back empty on every poll tick and can't be
|
|
208
|
+
# spotted from the table, so the floor is enforced once, at the door,
|
|
209
|
+
# rather than at each of those call sites. (The AOB pattern spec is the
|
|
210
|
+
# one whose declared length is 0 — the scanner derives its real width
|
|
211
|
+
# from the pattern.)
|
|
212
|
+
entry.length = max(1, int(entry.length))
|
|
213
|
+
|
|
200
214
|
# If the address already exists, just refresh its description/type.
|
|
201
215
|
for existing in self._entries:
|
|
202
216
|
if existing.address == entry.address:
|
|
203
217
|
existing.description = entry.description or existing.description
|
|
218
|
+
# Re-promoting an address that is already in the table is the
|
|
219
|
+
# third place a spec_label changes, and the cached value has to
|
|
220
|
+
# go with it here too — _rebuild formats it through the new
|
|
221
|
+
# spec on the way out of this method.
|
|
222
|
+
if entry.spec_label != existing.spec_label:
|
|
223
|
+
_forget_value_read_as_another_type(existing)
|
|
204
224
|
existing.spec_label = entry.spec_label
|
|
205
225
|
existing.length = entry.length
|
|
206
226
|
self._rebuild()
|
|
@@ -257,7 +277,11 @@ class CheatTable(QWidget):
|
|
|
257
277
|
self._table.setItem(row, self.COL_ADDRESS, addr)
|
|
258
278
|
|
|
259
279
|
type_label = entry.spec_label
|
|
260
|
-
|
|
280
|
+
# Show the width whenever it belongs to the entry rather than to the
|
|
281
|
+
# spec. An IDA pattern declares none (length 0) yet carries a real one
|
|
282
|
+
# here — writes are refused against it — so hiding it left the user
|
|
283
|
+
# told to "widen the entry" with no way to see what it holds.
|
|
284
|
+
if entry.spec.accepts_length_override or not entry.spec.length:
|
|
261
285
|
type_label += f" · {entry.length}B"
|
|
262
286
|
type_item = QTableWidgetItem(type_label)
|
|
263
287
|
type_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
@@ -312,7 +336,9 @@ class CheatTable(QWidget):
|
|
|
312
336
|
# Treat empty as "unfreeze and clear" — no-op.
|
|
313
337
|
return
|
|
314
338
|
try:
|
|
315
|
-
value, _length =
|
|
339
|
+
value, _length = parse_value_for_write(
|
|
340
|
+
entry.spec, text, entry.length, entry.last_value
|
|
341
|
+
)
|
|
316
342
|
except ValueError as exc:
|
|
317
343
|
QMessageBox.warning(self, "Invalid Value", str(exc))
|
|
318
344
|
self._suspend_signals = True
|
|
@@ -390,6 +416,13 @@ class CheatTable(QWidget):
|
|
|
390
416
|
continue
|
|
391
417
|
entry = self._entries[row]
|
|
392
418
|
entry.last_value = value
|
|
419
|
+
# A frozen row whose baseline was dropped — its type changed,
|
|
420
|
+
# so what the old spec had read no longer means anything —
|
|
421
|
+
# re-adopts the first value read under the new one. The poll
|
|
422
|
+
# worker skips a frozen entry with no frozen_value, so without
|
|
423
|
+
# this the Active box stays ticked while nothing is written.
|
|
424
|
+
if entry.frozen and entry.frozen_value is None:
|
|
425
|
+
entry.frozen_value = value
|
|
393
426
|
self._update_value_cell(row, entry)
|
|
394
427
|
finally:
|
|
395
428
|
self._suspend_signals = False
|
|
@@ -528,16 +561,40 @@ class CheatTable(QWidget):
|
|
|
528
561
|
if plan.description is not None:
|
|
529
562
|
entry.description = plan.description
|
|
530
563
|
|
|
531
|
-
|
|
564
|
+
# What the row was showing before this plan touched it. A type
|
|
565
|
+
# change forgets it, but a write in the same pass still needs
|
|
566
|
+
# it: an IDA '?' keeps the byte that is already there, and
|
|
567
|
+
# Byte Array → AOB doesn't change what those bytes mean.
|
|
568
|
+
# parse_value_for_write ignores it when the type genuinely
|
|
569
|
+
# changed shape, so a stale int can't be misread as bytes.
|
|
570
|
+
current = entry.last_value
|
|
571
|
+
retyped = False
|
|
572
|
+
if plan.spec is not None and plan.spec.label != entry.spec_label:
|
|
573
|
+
retyped = True
|
|
532
574
|
entry.spec_label = plan.spec.label
|
|
575
|
+
_forget_value_read_as_another_type(entry)
|
|
576
|
+
if plan.spec is not None:
|
|
533
577
|
if not plan.spec.accepts_length_override:
|
|
534
|
-
entry.length
|
|
578
|
+
# `or entry.length`: the AOB pattern spec declares a
|
|
579
|
+
# length of 0 — the scanner derives a match's width from
|
|
580
|
+
# the pattern, and an entry has none to derive from — so
|
|
581
|
+
# keep the width it already has.
|
|
582
|
+
entry.length = plan.spec.length or entry.length
|
|
535
583
|
|
|
536
584
|
if plan.value_text is not None:
|
|
537
585
|
spec = entry.spec
|
|
586
|
+
# A retype to a variable-width spec re-sizes the entry from
|
|
587
|
+
# the value, rather than inheriting the width of the type it
|
|
588
|
+
# replaced. Otherwise three Int32 rows retyped to String
|
|
589
|
+
# with "hello" all fail on "the entry holds 4 — widen it
|
|
590
|
+
# first", and the bulk dialog has no width field, nor does
|
|
591
|
+
# a multi-row selection offer one: a dead end.
|
|
592
|
+
cap: Optional[int] = entry.length
|
|
593
|
+
if retyped and spec.accepts_length_override:
|
|
594
|
+
cap = None
|
|
538
595
|
try:
|
|
539
|
-
value, effective_length =
|
|
540
|
-
spec, plan.value_text,
|
|
596
|
+
value, effective_length = parse_value_for_write(
|
|
597
|
+
spec, plan.value_text, cap, current
|
|
541
598
|
)
|
|
542
599
|
except ValueError as exc:
|
|
543
600
|
failures.append((entry.address, str(exc)))
|
|
@@ -665,10 +722,15 @@ class CheatTable(QWidget):
|
|
|
665
722
|
)
|
|
666
723
|
if not ok:
|
|
667
724
|
return
|
|
668
|
-
self._entries[row]
|
|
725
|
+
entry = self._entries[row]
|
|
726
|
+
if chosen != entry.spec_label:
|
|
727
|
+
entry.spec_label = chosen
|
|
728
|
+
_forget_value_read_as_another_type(entry)
|
|
669
729
|
spec = find_spec(chosen) or VALUE_TYPES[0]
|
|
670
730
|
if not spec.accepts_length_override:
|
|
671
|
-
|
|
731
|
+
# Same as the bulk edit: the AOB pattern spec declares no width of
|
|
732
|
+
# its own, so the entry keeps the one it has.
|
|
733
|
+
entry.length = spec.length or entry.length
|
|
672
734
|
self._rebuild()
|
|
673
735
|
|
|
674
736
|
def _change_length(self, row: int) -> None:
|
|
@@ -678,7 +740,9 @@ class CheatTable(QWidget):
|
|
|
678
740
|
"Length (bytes):",
|
|
679
741
|
value=self._entries[row].length,
|
|
680
742
|
minValue=1,
|
|
681
|
-
|
|
743
|
+
# An entry already wider than the cap can still be shrunk from here;
|
|
744
|
+
# it just can't grow past it.
|
|
745
|
+
maxValue=max(MAX_ENTRY_LENGTH, self._entries[row].length),
|
|
682
746
|
)
|
|
683
747
|
if not ok:
|
|
684
748
|
return
|
|
@@ -736,6 +800,41 @@ class CheatTable(QWidget):
|
|
|
736
800
|
QMessageBox.warning(self, "Import", f"Skipped a bad entry: {exc}")
|
|
737
801
|
|
|
738
802
|
|
|
803
|
+
# Ceiling for an entry's buffer width. Entries are promoted at the width of the
|
|
804
|
+
# value scanned for, which the scanner doesn't cap, so 1024 was too tight — but
|
|
805
|
+
# the poll worker allocates this many bytes per entry on every 100 ms tick, so
|
|
806
|
+
# an unbounded field turns one typo into a multi-gigabyte allocation the tick's
|
|
807
|
+
# blanket except swallows and retries forever. A megabyte is far past any real
|
|
808
|
+
# value and still cheap to read ten times a second.
|
|
809
|
+
MAX_ENTRY_LENGTH = 1_048_576
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _forget_value_read_as_another_type(entry: CheatEntry) -> None:
|
|
813
|
+
"""Drop the cached value when ``new_spec`` can't read what produced it.
|
|
814
|
+
|
|
815
|
+
``last_value`` and ``frozen_value`` hold whatever the *previous* spec
|
|
816
|
+
decoded — an int, a str, raw bytes. Nothing waits for a fresh poll tick
|
|
817
|
+
before the new spec is used on them, and a spec's ``format`` only accepts
|
|
818
|
+
what its own ``pytype`` produces: ``_fmt_bytes(1234)`` raises TypeError from
|
|
819
|
+
inside a Qt slot, and a frozen entry would be re-published to the poll
|
|
820
|
+
worker to write the old type's value through the new type's ``pytype``.
|
|
821
|
+
The next tick refills both, so forgetting them costs a single frame.
|
|
822
|
+
|
|
823
|
+
Unconditional: even a switch that keeps the ``pytype`` can change the
|
|
824
|
+
width, and a value decoded at the old one means nothing at the new. A
|
|
825
|
+
caller that still needs the bytes — the bulk edit writes a value in the
|
|
826
|
+
same pass — must capture them before calling.
|
|
827
|
+
|
|
828
|
+
The freeze is released with it. Leaving the box ticked with no target would
|
|
829
|
+
make the next poll tick adopt whatever the address happens to hold, pinning
|
|
830
|
+
a value the user never chose; a released box is visible and re-arming it is
|
|
831
|
+
one click.
|
|
832
|
+
"""
|
|
833
|
+
entry.last_value = None
|
|
834
|
+
entry.frozen_value = None
|
|
835
|
+
entry.frozen = False
|
|
836
|
+
|
|
837
|
+
|
|
739
838
|
def prompt_for_manual_entry(parent) -> Optional[CheatEntry]:
|
|
740
839
|
"""Sequential QInputDialog flow for the "Add Address Manually" button."""
|
|
741
840
|
description, ok = QInputDialog.getText(
|
|
@@ -763,13 +862,16 @@ def prompt_for_manual_entry(parent) -> Optional[CheatEntry]:
|
|
|
763
862
|
return None
|
|
764
863
|
spec = find_spec(spec_label) or VALUE_TYPES[0]
|
|
765
864
|
|
|
865
|
+
# The AOB pattern spec declares a length of 0 (the scanner derives a match's
|
|
866
|
+
# width from the pattern), and an address added by hand has no pattern to
|
|
867
|
+
# measure — so ask for the width instead of minting a zero-byte buffer.
|
|
766
868
|
length = spec.length
|
|
767
|
-
if spec.accepts_length_override:
|
|
869
|
+
if spec.accepts_length_override or not spec.length:
|
|
768
870
|
length, ok = QInputDialog.getInt(
|
|
769
871
|
parent,
|
|
770
872
|
"Add address",
|
|
771
873
|
"Buffer length (bytes):",
|
|
772
|
-
value=spec.length,
|
|
874
|
+
value=spec.length or 4,
|
|
773
875
|
minValue=1,
|
|
774
876
|
maxValue=1024,
|
|
775
877
|
)
|
|
@@ -47,7 +47,12 @@ from PySide6.QtWidgets import (
|
|
|
47
47
|
from PyMemoryEditor import AbstractProcess
|
|
48
48
|
|
|
49
49
|
from ._widgets import parse_hex_address, parse_offsets, resolve_base_address
|
|
50
|
-
from .value_types import
|
|
50
|
+
from .value_types import (
|
|
51
|
+
VALUE_TYPES,
|
|
52
|
+
ValueTypeSpec,
|
|
53
|
+
find_spec,
|
|
54
|
+
has_readable_width,
|
|
55
|
+
)
|
|
51
56
|
|
|
52
57
|
|
|
53
58
|
# Child of "PyMemoryEditor" — surfaced by the Log Console via propagation.
|
|
@@ -216,6 +221,13 @@ class PointerChainDialog(QDialog):
|
|
|
216
221
|
|
|
217
222
|
self._value_type_combo = QComboBox()
|
|
218
223
|
for spec in VALUE_TYPES:
|
|
224
|
+
# The address is already known here, so the only spec that can't be
|
|
225
|
+
# offered is the one with no width of its own — an IDA pattern,
|
|
226
|
+
# which would read zero bytes. A regex is welcome: its width comes
|
|
227
|
+
# from the Length field and it renders the bytes as text up to the
|
|
228
|
+
# first NUL, which "String (UTF-8)" doesn't do.
|
|
229
|
+
if not has_readable_width(spec):
|
|
230
|
+
continue
|
|
219
231
|
self._value_type_combo.addItem(spec.label)
|
|
220
232
|
form.addRow("Read value as:", self._value_type_combo)
|
|
221
233
|
|
|
@@ -59,7 +59,12 @@ from ._widgets import (
|
|
|
59
59
|
parse_hex_address,
|
|
60
60
|
shutdown_worker_thread,
|
|
61
61
|
)
|
|
62
|
-
from .value_types import
|
|
62
|
+
from .value_types import (
|
|
63
|
+
VALUE_TYPES,
|
|
64
|
+
ValueTypeSpec,
|
|
65
|
+
find_spec,
|
|
66
|
+
has_readable_width,
|
|
67
|
+
)
|
|
63
68
|
|
|
64
69
|
|
|
65
70
|
_LOG = logging.getLogger(__name__)
|
|
@@ -409,8 +414,13 @@ class PointerScanDialog(TearsDownOnClose, QDialog):
|
|
|
409
414
|
# type into the cheat table on promotion.
|
|
410
415
|
self._value_type_combo = QComboBox()
|
|
411
416
|
for spec in VALUE_TYPES:
|
|
412
|
-
|
|
413
|
-
|
|
417
|
+
# The address is already known here, so the only spec that can't be
|
|
418
|
+
# offered is the one with no width of its own — an IDA pattern,
|
|
419
|
+
# which would read zero bytes. A regex is welcome: its width comes
|
|
420
|
+
# from the Length field and it renders the bytes as text up to the
|
|
421
|
+
# first NUL, which "String (UTF-8)" doesn't do.
|
|
422
|
+
if not has_readable_width(spec):
|
|
423
|
+
continue
|
|
414
424
|
self._value_type_combo.addItem(spec.label)
|
|
415
425
|
self._value_type_combo.currentTextChanged.connect(self._on_value_type_changed)
|
|
416
426
|
form.addRow("Read value as:", self._value_type_combo)
|
|
@@ -22,7 +22,12 @@ from PySide6.QtCore import QThread, Signal
|
|
|
22
22
|
|
|
23
23
|
from PyMemoryEditor import AbstractProcess, MemoryRegion, ScanTypesEnum
|
|
24
24
|
|
|
25
|
-
from .scan_types import
|
|
25
|
+
from .scan_types import (
|
|
26
|
+
DELTA_SCAN_TYPES,
|
|
27
|
+
NextScanType,
|
|
28
|
+
NO_VALUE_SCAN_TYPES,
|
|
29
|
+
ScanType,
|
|
30
|
+
)
|
|
26
31
|
from .value_types import parse_value, ValueTypeSpec
|
|
27
32
|
|
|
28
33
|
|
|
@@ -85,6 +90,7 @@ def build_scan_request(
|
|
|
85
90
|
value_text: str,
|
|
86
91
|
second_value_text: str = "",
|
|
87
92
|
length_spin_value: Optional[int] = None,
|
|
93
|
+
previous_scan_length: Optional[int] = None,
|
|
88
94
|
writeable_only: bool = False,
|
|
89
95
|
with_value: bool = True,
|
|
90
96
|
) -> ScanRequest:
|
|
@@ -93,11 +99,19 @@ def build_scan_request(
|
|
|
93
99
|
|
|
94
100
|
This is the pure core of ``ScannerPanel._build_request`` lifted out of the
|
|
95
101
|
widget so the request-assembly rules (pattern short-circuit, the
|
|
96
|
-
|
|
97
|
-
unit-tested without a ``QApplication``. The widget keeps only the
|
|
98
|
-
are genuinely UI: reading the fields and showing a ``QMessageBox``
|
|
99
|
-
``ValueError`` raised here.
|
|
100
|
-
|
|
102
|
+
value-derived width for str / bytes, range parsing, the no-value scan types)
|
|
103
|
+
can be unit-tested without a ``QApplication``. The widget keeps only the
|
|
104
|
+
bits that are genuinely UI: reading the fields and showing a ``QMessageBox``
|
|
105
|
+
on the ``ValueError`` raised here.
|
|
106
|
+
|
|
107
|
+
:param length_spin_value: the Length field. Only the regex type reads it
|
|
108
|
+
(as ``byte_length``) — every other type derives its width from the spec
|
|
109
|
+
or from the value itself.
|
|
110
|
+
:param previous_scan_length: the width the scan that produced the current
|
|
111
|
+
results used. Only the no-value comparisons read it, and only for the
|
|
112
|
+
variable-width types, whose baseline is meaningless at another width.
|
|
113
|
+
(The ``*_BY`` deltas compare against the baseline too, but they are
|
|
114
|
+
rejected outright for those types — see below.)
|
|
101
115
|
:raises ValueError: if a value/pattern fails to parse (message is
|
|
102
116
|
user-facing — the caller picks the dialog title from ``spec.is_pattern``).
|
|
103
117
|
"""
|
|
@@ -116,19 +130,35 @@ def build_scan_request(
|
|
|
116
130
|
writeable_only=writeable_only,
|
|
117
131
|
)
|
|
118
132
|
|
|
119
|
-
#
|
|
120
|
-
#
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
133
|
+
# "Increased/Decreased value BY" adds the delta to the baseline, which only
|
|
134
|
+
# means anything for a number: on str/bytes ``prev + exp`` concatenates (so
|
|
135
|
+
# the comparison is never true) and ``prev - exp`` raises TypeError, which
|
|
136
|
+
# the refine worker swallows into "doesn't match". Either way every address
|
|
137
|
+
# is dropped and the user is told nothing, so reject the combination with a
|
|
138
|
+
# message instead. Checked *after* the pattern short-circuit above: the
|
|
139
|
+
# pattern specs are bytes-typed too, but they force EXACT regardless of the
|
|
140
|
+
# scan type passed, and the comparisons this message points at are disabled
|
|
141
|
+
# in pattern mode anyway.
|
|
142
|
+
if scan_type in DELTA_SCAN_TYPES and spec.pytype in (str, bytes):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"Increased/Decreased Value By adds a numeric amount to the previous "
|
|
145
|
+
"value, which doesn't apply to %s. Use Changed Value or Unchanged "
|
|
146
|
+
"Value to compare against the previous scan." % spec.label
|
|
147
|
+
)
|
|
127
148
|
|
|
128
|
-
# Increased/Decreased/Changed/Unchanged compare
|
|
129
|
-
#
|
|
149
|
+
# Increased/Decreased/Changed/Unchanged compare the value read now against
|
|
150
|
+
# the one the *previous* scan recorded, so they need no target value — but
|
|
151
|
+
# they must re-read at the width that baseline was recorded with. Reading
|
|
152
|
+
# 16 bytes where the first scan recorded 4 yields "olá\0\0…" against "olá",
|
|
153
|
+
# which never compares equal, so every address would report as Changed.
|
|
154
|
+
# ``previous_scan_length`` carries that width for the variable-width types;
|
|
155
|
+
# the fixed-width types own theirs and ignore it.
|
|
130
156
|
if scan_type in NO_VALUE_SCAN_TYPES:
|
|
131
|
-
length =
|
|
157
|
+
length = (
|
|
158
|
+
previous_scan_length
|
|
159
|
+
if spec.accepts_length_override and previous_scan_length
|
|
160
|
+
else spec.length
|
|
161
|
+
)
|
|
132
162
|
return ScanRequest(
|
|
133
163
|
spec=spec,
|
|
134
164
|
length=int(length),
|
|
@@ -137,14 +167,22 @@ def build_scan_request(
|
|
|
137
167
|
writeable_only=writeable_only,
|
|
138
168
|
)
|
|
139
169
|
|
|
170
|
+
# Every scan that carries a value sizes its buffer from that value: the
|
|
171
|
+
# numeric types have a fixed width, and str / bytes derive theirs in
|
|
172
|
+
# parse_value (the text's UTF-8 byte length / the number of hex bytes
|
|
173
|
+
# entered). So no length override is passed here. Letting the Length field
|
|
174
|
+
# win could only break the scan — a width below the value's raises
|
|
175
|
+
# "byte string too long" from the fixed-width ctypes buffer, and a width
|
|
176
|
+
# above it NUL-pads the target, silently searching for "the value followed
|
|
177
|
+
# by zeros". Partial matching has its own value type (AOB Pattern).
|
|
140
178
|
value: Any
|
|
141
179
|
if scan_type in (ScanTypesEnum.VALUE_BETWEEN, ScanTypesEnum.NOT_VALUE_BETWEEN):
|
|
142
|
-
lo, lo_len = parse_value(spec, value_text
|
|
143
|
-
hi, hi_len = parse_value(spec, second_value_text
|
|
180
|
+
lo, lo_len = parse_value(spec, value_text)
|
|
181
|
+
hi, hi_len = parse_value(spec, second_value_text)
|
|
144
182
|
length = max(lo_len, hi_len)
|
|
145
183
|
value = (lo, hi)
|
|
146
184
|
else:
|
|
147
|
-
value, length = parse_value(spec, value_text
|
|
185
|
+
value, length = parse_value(spec, value_text)
|
|
148
186
|
|
|
149
187
|
if not with_value:
|
|
150
188
|
value = None # Used by callers that only need spec/length/scan_type.
|