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.
Files changed (167) hide show
  1. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PKG-INFO +1 -27
  2. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/__init__.py +1 -1
  3. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/cheat_table.py +114 -12
  4. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/pointer_chain_dialog.py +13 -1
  5. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/pointer_scan_dialog.py +13 -3
  6. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/scan_worker.py +58 -20
  7. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/scanner_panel.py +243 -44
  8. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/value_types.py +172 -1
  9. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/functions.py +11 -0
  10. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/types.py +12 -0
  11. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/pattern.py +36 -11
  12. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/README.md +0 -26
  13. pymemoryeditor-2.2.0/tests/app/test_app_cheat_entry.py +490 -0
  14. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_app_scan_request.py +113 -3
  15. pymemoryeditor-2.2.0/tests/app/test_app_smoke.py +654 -0
  16. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_app_value_types.py +35 -0
  17. pymemoryeditor-2.2.0/tests/platforms/test_macos_transient_reads.py +81 -0
  18. pymemoryeditor-2.1.0/tests/app/test_app_cheat_entry.py +0 -85
  19. pymemoryeditor-2.1.0/tests/app/test_app_smoke.py +0 -301
  20. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/.flake8 +0 -0
  21. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/.gitignore +0 -0
  22. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/.readthedocs.yaml +0 -0
  23. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/CODE_OF_CONDUCT.md +0 -0
  24. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/CONTRIBUTING.md +0 -0
  25. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/LICENSE +0 -0
  26. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/Makefile +0 -0
  27. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/__main__.py +0 -0
  28. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/__init__.py +0 -0
  29. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/_auto_refresh_dialog.py +0 -0
  30. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/_icon.py +0 -0
  31. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/_widgets.py +0 -0
  32. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/application.py +0 -0
  33. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/assets/icon.svg +0 -0
  34. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/cheat_entry.py +0 -0
  35. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/cheat_poll_worker.py +0 -0
  36. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/log_console_dialog.py +0 -0
  37. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/main_window.py +0 -0
  38. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/memory_map_dialog.py +0 -0
  39. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/memory_viewer_dialog.py +0 -0
  40. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/modules_dialog.py +0 -0
  41. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/open_process_dialog.py +0 -0
  42. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/results_view.py +0 -0
  43. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/scan_types.py +0 -0
  44. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/threads_dialog.py +0 -0
  45. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/enums.py +0 -0
  46. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/functions.py +0 -0
  47. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/libc.py +0 -0
  48. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/process.py +0 -0
  49. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/types.py +0 -0
  50. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/__init__.py +0 -0
  51. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/libsystem.py +0 -0
  52. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/process.py +0 -0
  53. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/__init__.py +0 -0
  54. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/abstract.py +0 -0
  55. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/errors.py +0 -0
  56. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/info.py +0 -0
  57. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/module_info.py +0 -0
  58. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/pointer_scan.py +0 -0
  59. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/region.py +0 -0
  60. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/remote_pointer.py +0 -0
  61. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/scanning.py +0 -0
  62. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/thread_info.py +0 -0
  63. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/util.py +0 -0
  64. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/py.typed +0 -0
  65. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/__init__.py +0 -0
  66. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/convert.py +0 -0
  67. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/scan.py +0 -0
  68. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/scan_numpy.py +0 -0
  69. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/__init__.py +0 -0
  70. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
  71. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
  72. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
  73. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
  74. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
  75. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/functions.py +0 -0
  76. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/process.py +0 -0
  77. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/types.py +0 -0
  78. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/SECURITY.md +0 -0
  79. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/assets/screenshots/app.png +0 -0
  80. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/assets/social-preview.html +0 -0
  81. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/assets/social-preview.png +0 -0
  82. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/_static/.gitkeep +0 -0
  83. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/_static/custom.css +0 -0
  84. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/_templates/layout.html +0 -0
  85. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/enums.md +0 -0
  86. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/errors.md +0 -0
  87. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/index.md +0 -0
  88. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/memory-region.md +0 -0
  89. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/module-info.md +0 -0
  90. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/openprocess.md +0 -0
  91. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/pointer-path.md +0 -0
  92. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/remote-pointer.md +0 -0
  93. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/thread-info.md +0 -0
  94. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/api/utilities.md +0 -0
  95. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/app.md +0 -0
  96. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/conf.py +0 -0
  97. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/contributing.md +0 -0
  98. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/funding.md +0 -0
  99. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/glossary.md +0 -0
  100. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/allocate-free.md +0 -0
  101. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/index.md +0 -0
  102. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/logging.md +0 -0
  103. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/memory-regions.md +0 -0
  104. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/modules-threads.md +0 -0
  105. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/opening-process.md +0 -0
  106. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/pattern-scan.md +0 -0
  107. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/pointer-scan.md +0 -0
  108. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/pointers.md +0 -0
  109. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/read-write.md +0 -0
  110. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/guide/searching.md +0 -0
  111. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/index.md +0 -0
  112. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/installation.md +0 -0
  113. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/license.md +0 -0
  114. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/platform-notes.md +0 -0
  115. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/quickstart.md +0 -0
  116. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/requirements.txt +0 -0
  117. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/troubleshooting.md +0 -0
  118. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/docs/why.md +0 -0
  119. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/pyproject.toml +0 -0
  120. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/__init__.py +0 -0
  121. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_app_pointer_parsing.py +0 -0
  122. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_app_widgets.py +0 -0
  123. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_auto_refresh_dialog.py +0 -0
  124. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_cheat_poll_worker.py +0 -0
  125. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_open_process_dialog.py +0 -0
  126. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/app/test_refine_scan_worker.py +0 -0
  127. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/conftest.py +0 -0
  128. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/core/__init__.py +0 -0
  129. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/core/test_errors.py +0 -0
  130. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/core/test_logger.py +0 -0
  131. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/__init__.py +0 -0
  132. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_allocate_free.py +0 -0
  133. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_bufflength_inference.py +0 -0
  134. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_editor.py +0 -0
  135. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_partial_io.py +0 -0
  136. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_read_into.py +0 -0
  137. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_str_boundary.py +0 -0
  138. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_str_decode_consistency.py +0 -0
  139. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_typed_accessors.py +0 -0
  140. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_write_safety.py +0 -0
  141. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/memory/test_write_str_bytes_width.py +0 -0
  142. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/__init__.py +0 -0
  143. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/test_bitness.py +0 -0
  144. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/test_bitness_fallback_warns.py +0 -0
  145. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/test_linux_types.py +0 -0
  146. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/test_macos_protect.py +0 -0
  147. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/platforms/test_win32_permissions.py +0 -0
  148. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/pointer/__init__.py +0 -0
  149. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/pointer/test_pointer_chain.py +0 -0
  150. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/pointer/test_pointer_scan.py +0 -0
  151. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/pointer/test_remote_pointer.py +0 -0
  152. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/__init__.py +0 -0
  153. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_memory_region.py +0 -0
  154. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_module_enumeration.py +0 -0
  155. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_partial_name_match.py +0 -0
  156. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_process_enumeration.py +0 -0
  157. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_process_lookup.py +0 -0
  158. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/process/test_thread_enumeration.py +0 -0
  159. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/__init__.py +0 -0
  160. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_chunking_integration.py +0 -0
  161. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_pattern_compile.py +0 -0
  162. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_pattern_scan.py +0 -0
  163. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_region_snapshot.py +0 -0
  164. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_scan.py +0 -0
  165. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_scan_numpy.py +0 -0
  166. {pymemoryeditor-2.1.0 → pymemoryeditor-2.2.0}/tests/scan/test_scan_properties.py +0 -0
  167. {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.1.0
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.
@@ -8,7 +8,7 @@ Supported platforms: Windows, Linux and macOS (32-bit and 64-bit).
8
8
  """
9
9
 
10
10
  __author__ = "Jean Loui Bernard Silva de Jesus"
11
- __version__ = "2.1.0"
11
+ __version__ = "2.2.0"
12
12
 
13
13
  import logging
14
14
  import sys
@@ -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 VALUE_TYPES, ValueTypeSpec, find_spec, parse_value
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
- if entry.spec.accepts_length_override:
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 = parse_value(entry.spec, text, entry.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
- if plan.spec is not None:
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 = plan.spec.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 = parse_value(
540
- spec, plan.value_text, entry.length
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].spec_label = chosen
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
- self._entries[row].length = spec.length
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
- maxValue=1024,
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 VALUE_TYPES, ValueTypeSpec, find_spec
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 VALUE_TYPES, ValueTypeSpec, find_spec
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
- if spec.is_pattern:
413
- continue # reading a value "as a pattern" is meaningless here
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 NextScanType, NO_VALUE_SCAN_TYPES, ScanType
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
- str-ignores-length override, range parsing, the no-value scan types) can be
97
- unit-tested without a ``QApplication``. The widget keeps only the bits that
98
- are genuinely UI: reading the fields and showing a ``QMessageBox`` on the
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
- # String (UTF-8) ignores the length field: pass None so parse_value derives
120
- # the buffer width from the typed text's UTF-8 byte length. Byte Array still
121
- # honours the user-set override.
122
- length_override = (
123
- length_spin_value
124
- if spec.accepts_length_override and spec.pytype is not str
125
- else None
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 current vs previous and need
129
- # no target value just the value shape (type + length).
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 = length_override if length_override is not None else spec.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, length_override)
143
- hi, hi_len = parse_value(spec, second_value_text, length_override)
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, length_override)
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.